Marker
Marker is a lightweight row for status text, timeline boundaries, unread
labels, and system notices. It deliberately accepts arbitrary children instead
of defining an application-specific status enum. MarkerIcon and
MarkerContent are optional typed slots for the common icon-and-label shape;
direct children remain available for custom composition.
Marker is a layout and loading primitive. It does not own a notification
record, an unread count, a click action, or a live status store. Compose those
with application state and existing Badge, Button, Link, or navigation
components.
#Import
use gpui_kit::{ParentElement as _, StyleRefinement, Styled as _};
use gpui_kit::component::{
ActiveTheme as _, Icon, IconName, Sizable as _,
badge::Badge,
button::{Button, ButtonVariants as _},
marker::{
Marker, MarkerAlignment, MarkerContent, MarkerIcon, MarkerLoadingStyle, MarkerVariant,
},
shimmer::{ShimmerStyle, ShimmerText},
spinner::Spinner,
};
use std::time::Duration;// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Marker, MarkerHighlight } from "marker.slint";use iced::widget::{Space, column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Color, Element, Length, Task, Theme};#Anatomy and basic usage
The typed form keeps icon and content style targets independent:
Marker::new()
.icon(MarkerIcon::new().child(Icon::new(IconName::CircleCheck)))
.content(MarkerContent::new().text("Online"))Direct children are useful when a marker needs an application-specific layout:
Marker::new()
.child(Icon::new(IconName::Info))
.child("Conversation archived")The default state is:
| Property | Default | Meaning |
|---|---|---|
| Variant | Plain | A full-width status row without divider decoration. |
| Alignment | unset | Separator centers its label; every other variant starts at the leading edge. |
| Loading | false | No automatic loading effect. |
| Loading style | Spinner | Used when loading is enabled. |
| Icon slot | absent | A spinner is inserted only for spinner loading with no icon. |
| Content | absent | Add text or arbitrary child content. |
| Row minimum height | rems(1.) | Follows the shared typography scale. |
| Row gap | gap_2() | Shared compact spacing. |
Use MarkerContent::text(...) for text that should receive the loading shimmer.
Use .child(...) for arbitrary elements or text that should keep its own
rendering behavior.
#Variants
#Plain
// The Slint example's first box: the default text run, and the highlight
// band inline in the middle of it. The band has its own two-pixel inset, so
// the runs butt together at zero spacing.
let plain = panel(
column![
row![
marker("Drag a file here, or "),
highlight("browse"),
marker(" to upload."),
]
.spacing(0.0)
.align_y(Alignment::Center),
row![
marker("Messages are "),
highlight("end-to-end encrypted"),
marker("."),
]
.spacing(0.0)
.align_y(Alignment::Center),
marker("Conversation archived"),
]
.spacing(8.0)
.align_x(Alignment::Start),
);Plain is the default compact status row:
Marker::new()
.text_color(cx.theme().success)
.icon(MarkerIcon::new().child(Icon::new(IconName::CircleCheck)))
.content(MarkerContent::new().text("Synced"))VerticalLayout {
spacing: 8px;
alignment: start;
HorizontalLayout {
spacing: 2px;
alignment: start;
Marker {
text: "Drag a file here, or ";
}
MarkerHighlight {
text: "browse";
}
Marker {
text: " to upload.";
}
}
HorizontalLayout {
spacing: 2px;
alignment: start;
Marker {
text: "Messages are ";
}
MarkerHighlight {
text: "end-to-end encrypted";
}
Marker {
text: ".";
}
}
}The library does not define Online, Read, Typing, or Synced values.
The application supplies the words, icon, and semantic color so the same
primitive can serve different domains.
#Separator
// Slint draws its separator from two `Rectangle`s rather than from the
// marker, and this is the same construction: the stacked form the page's
// `MarkerVariant::Separator` names, and the inline form beside it.
let separator = panel(
column![
column![
rule(),
container(muted_label("Today"))
.width(Length::Fill)
.center_x(Length::Fill),
rule(),
]
.spacing(6.0)
.width(Length::Fill),
row![
rule(),
muted_label("The answer stopped before it finished."),
rule(),
]
.spacing(12.0)
.align_y(Alignment::Center)
.width(Length::Fill),
]
.spacing(12.0),
);Separator adds a flexible line on each side of the content:
Marker::new()
.with_variant(MarkerVariant::Separator)
.content(MarkerContent::new().text("Today"))VerticalLayout {
spacing: 12px;
alignment: start;
// Two rules with the label between them. Stacked rather than
// inline: a stretched 1px rule inside a horizontal row does not
// lay out at this size.
VerticalLayout {
spacing: 6px;
alignment: center;
Rectangle {
horizontal-stretch: 1;
height: 1px;
background: Theme.border;
}
Text {
text: "Today";
color: Theme.muted-foreground;
font-size: Theme.font-size-xs;
horizontal-alignment: center;
}
Rectangle {
horizontal-stretch: 1;
height: 1px;
background: Theme.border;
}
}
HorizontalLayout {
spacing: 12px;
alignment: center;
Rectangle {
min-width: 12px;
horizontal-stretch: 1;
min-height: 1px;
background: Theme.border;
}
Text {
text: "The answer stopped before it finished.";
color: Theme.muted-foreground;
font-size: Theme.font-size-xs;
vertical-alignment: center;
}
Rectangle {
min-width: 12px;
horizontal-stretch: 1;
min-height: 1px;
background: Theme.border;
}
}
}The line is an internal 1-pixel decorative element. The label remains the
semantic content. Use separator_style(...) to refine the two lines without
having to recreate their layout:
Marker::new()
.with_variant(MarkerVariant::Separator)
.separator_style(
StyleRefinement::default()
.bg(cx.theme().ring),
)
.content(MarkerContent::new().text("Yesterday"))#Border
Border adds a semantic bottom border and compact bottom padding:
Marker::new()
.with_variant(MarkerVariant::Border)
.icon(MarkerIcon::new().child(Icon::new(IconName::Info)))
.content(MarkerContent::new().text("3 unread messages"))The border is a visual boundary. Keep the unread count and meaning in text so the state does not depend on color or a line alone.
#Alignment
// A marker spans the row; the alignment decides where its children sit in
// it. Start, centre and end, which is the page's own three values.
let alignment = panel(
column![
container(marker("Delivered")).width(Length::Fill),
container(marker("The answer stopped before it finished."))
.width(Length::Fill)
.center_x(Length::Fill),
container(marker("Read at 14:02"))
.width(Length::Fill)
.align_x(Alignment::End),
]
.spacing(8.0),
);A marker spans the full row. alignment(...) decides where its children sit
inside that row and how wrapped text lines align:
Marker::new()
.alignment(MarkerAlignment::Center)
.icon(MarkerIcon::new().child(Icon::new(IconName::Info)))
.content(MarkerContent::new().text("Messages are end-to-end encrypted"));
Marker::new()
.alignment(MarkerAlignment::End)
.content(MarkerContent::new().text("Delivered"))VerticalLayout {
spacing: 8px;
alignment: start;
HorizontalLayout {
horizontal-stretch: 1;
Marker {
text: "Delivered";
}
}
HorizontalLayout {
horizontal-stretch: 1;
alignment: end;
Marker {
text: "Read at 14:02";
}
}
}Unset, Separator centers its label between the two lines and every other
variant starts at the leading edge. An explicit alignment applies to any
variant; a separator then keeps only the line on the far side of its label, so
Start draws the trailing line and End the leading one. Centered notices are the usual shape for a transcript’s system rows,
such as a stopped answer or a failed request with a retry action; an End
marker trails a delivery state under an outgoing message.
#Loading styles
// iced's marker has no `loading` flag, so the loading row composes the
// catalog's own spinning `Icon` ahead of the label, which is what the Slint
// example does with `Icon { spinning: true }`.
let loading = panel(
column![
row![
Icon::<Message>::new(svg::Handle::from_memory(LOADER))
.size(12.0)
.color(ink.muted_foreground)
.spinning(true),
marker("Loading messages…"),
]
.spacing(8.0)
.align_y(Alignment::Center),
row![
Icon::<Message>::new(svg::Handle::from_memory(LOADER))
.size(12.0)
.color(ink.muted_foreground)
.spinning(true),
marker("Thinking…"),
]
.spacing(8.0)
.align_y(Alignment::Center),
]
.spacing(8.0)
.align_x(Alignment::Start),
);Set loading(true) without changing the marker’s variant or normal layout:
Marker::new()
.loading(true)
.with_loading_style(MarkerLoadingStyle::Spinner)
.content(MarkerContent::new().text("Loading messages…"));
Marker::new()
.loading(true)
.with_loading_style(MarkerLoadingStyle::Shimmer)
.content(MarkerContent::new().text("Thinking…"))VerticalLayout {
spacing: 8px;
alignment: start;
HorizontalLayout {
spacing: 8px;
alignment: center;
Icon {
name: "loader-circle";
size: 12px;
spinning: true;
color: Theme.muted-foreground;
}
Marker {
text: "Loading messages…";
}
}
HorizontalLayout {
spacing: 8px;
alignment: center;
Icon {
name: "loader-circle";
size: 12px;
spinning: true;
color: Theme.muted-foreground;
}
Marker {
text: "Thinking…";
}
}
}Spinner behavior is intentionally predictable:
Spinneris the defaultMarkerLoadingStyle.- If loading uses
Spinnerand noMarkerIconwas supplied, a compactSpinner::new().xsmall()is inserted automatically. - If the application supplies
MarkerIcon, that icon wins and no automatic spinner is added. MarkerVariant::Separatorstill renders its divider lines while loading.MarkerVariant::Borderstill renders its border while loading.
Shimmer is text-aware when content was added with .text(...):
Marker::new()
.loading(true)
.with_loading_style(MarkerLoadingStyle::Shimmer)
.content(MarkerContent::new().text("Generating a response…"))Arbitrary MarkerContent children are still supported. When there is no typed
text child, the content slot receives a gentle opacity animation instead. Icons
and separator lines stay static. When reduced motion is enabled, text is
rendered without animation and the marker remains readable.
#Shimmer configuration
Use one ShimmerStyle for a marker’s text effect:
Marker::new()
.loading(true)
.with_loading_style(MarkerLoadingStyle::Shimmer)
.with_shimmer_style(
ShimmerStyle::new()
.duration(Duration::from_secs(3))
.highlight_color(cx.theme().primary)
.spread(0.45)
.reverse(true)
.once(false),
)
.content(MarkerContent::new().text("Processing files…"))The ShimmerStyle defaults are a two-second repeating sweep, theme-aware
highlight color, 0.3 normalized spread, left-to-right direction, and looping.
duration(...) clamps values below one millisecond. spread(...) accepts a
relative f32 (clamped to 0.05..=1.0) or an absolute Pixels half-width;
non-finite values leave the current spread unchanged.
reverse(true) changes the direction, and once(true) stops after one sweep.
For a marker-independent loading label, use ShimmerText directly:
ShimmerText::new("Uploading report.pdf…")
.with_shimmer_style(ShimmerStyle::new().spread(0.4))
.text_sm()
.text_color(cx.theme().muted_foreground)ShimmerText inherits typography and text color through Styled, preserves
wrapping and truncation, and uses the active theme’s background and foreground
to keep the highlight readable in light and dark modes.
#Icons, content, and interactive children
// The Slint example's `With icon` box: a marker led by a status glyph. The
// page's `MarkerIcon` slot is what this stands in for; its first sample is
// the info tone and a plain marker.
let with_icon = panel(
column![
status_row(
INFO,
ink.muted_foreground,
"Messages are end-to-end encrypted"
),
status_row(CIRCLE_CHECK, ink.success, "Delivered"),
status_row(
TRIANGLE_ALERT,
ink.warning,
"The answer stopped before it finished."
),
]
.spacing(8.0)
.align_x(Alignment::Start),
);MarkerIcon is a compact size_4() slot. MarkerContent is a min_w_0()
slot, so a long label can choose its own wrapping or truncation:
Marker::new()
.icon(MarkerIcon::new().child(Icon::new(IconName::Bell)))
.content(
MarkerContent::new()
.child("Unread notifications")
.child(Badge::new().count(3)),
)Interactive children are allowed, but Marker does not make the row itself a
control:
Marker::new()
.content(
MarkerContent::new()
.text("New messages")
.child(Button::new("open-messages").ghost().xsmall().label("Open")),
)VerticalLayout {
spacing: 8px;
alignment: start;
HorizontalLayout {
spacing: 6px;
alignment: center;
Icon {
name: "info";
size: 14px;
color: Theme.muted-foreground;
}
Marker {
text: "Messages are end-to-end encrypted";
}
}
HorizontalLayout {
spacing: 6px;
alignment: center;
Icon {
name: "circle-check";
size: 14px;
color: Theme.success;
}
Marker {
text: "Delivered";
}
}
HorizontalLayout {
spacing: 6px;
alignment: center;
Icon {
name: "triangle-alert";
size: 14px;
color: Theme.warning;
}
Marker {
text: "The answer stopped before it finished.";
}
}
}Use Button for an in-app command and Link for a URL. Keep focus and action
semantics on those controls. If a whole marker should be clickable, compose a
semantic control around the content at the application boundary instead of
adding a click listener to this layout element.
#Icons, content, and interactive children — 2 of 2
// The Slint example's `Links and buttons` box, and the page's interactive
// child: a marker beside the control, with the action left to the caller.
let links = panel(
row![
marker("Stopped before it finished."),
Button::new("Retry")
.variant(ButtonVariant::Link)
.on_press(Message::Pressed("Retry")),
]
.spacing(8.0)
.align_y(Alignment::Center),
);#Custom styling and theme tokens
Marker, MarkerIcon, and MarkerContent implement Styled. Refinements are
applied after the default layout and theme colors:
Marker::new()
.px_3()
.py_2()
.rounded(cx.theme().radius)
.bg(cx.theme().accent)
.text_color(cx.theme().accent_foreground)
.icon(MarkerIcon::new().child(Icon::new(IconName::Star)))
.content(MarkerContent::new().text("Pinned message"))The separator lines have a separate StyleRefinement, so their color and
height can be customized without changing the content or marker’s own surface:
Marker::new()
.with_variant(MarkerVariant::Separator)
.separator_style(
StyleRefinement::default()
.bg(cx.theme().border),
)
.content(MarkerContent::new().text("New day"))Prefer semantic theme roles (muted_foreground, border, ring, accent,
and their foreground tokens) to raw colors. Radius, spacing, typography, and
separator geometry follow the shared design scale; typed style refinements can
adapt a marker to a denser toolbar or a larger empty-state boundary.
#Accessibility and motion guidance
- Include the status, boundary, or unread count in text. Icons, border lines, opacity, and color are supporting cues only.
- A marker is presentational by default. Set
.id(...)and.role(Role::Status)on a row that reports streaming or loading progress so assistive technology announces its updates; the role needs the stable identity an id provides. - Keep interactive content in
ButtonorLinkso it receives keyboard focus, activation, and disabled state. For the currentButtonAPI, use a visible.label(...)when the action needs an accessible name; a tooltip is supplemental. - Do not use
Markeras an unlabeled icon-only status. Add a visible or accessible text label when the icon has meaning. MarkerContent::text(...)remains visible when reduced motion is enabled; only the shimmer frame updates are skipped. Arbitrary children also retain their static content.- Loading text should describe the operation (“Generating…”, “Uploading…”) rather than communicate only through animation.
- Keep sufficient contrast after custom styling in both light and dark themes.
#When to use another component
- Use
Badgefor only a count, dot, or short classification. - Use
Separator::horizontal().label(...)when the product needs only a labeled divider and no marker loading or icon composition. - Use
Tagfor a standalone labeled status that is not part of a conversation row. - Use
h_flex()when the row has no shared marker behavior. - Use
MessageorBubblewhen the content is a conversational message with sender identity or a message surface.
#API reference
#Marker
| Method | Default | Purpose |
|---|---|---|
new() | Plain, not loading, spinner style | Create a marker. |
with_variant(MarkerVariant) | Plain | Choose plain, separator, or border treatment. |
alignment(MarkerAlignment) | unset: Separator centers, others start | Place the children at the leading edge, the center, or the trailing edge. |
loading(bool) | false | Enable or disable loading rendering. |
with_loading_style(MarkerLoadingStyle) | Spinner | Choose spinner or shimmer. |
with_shimmer_style(ShimmerStyle) | default style | Configure text shimmer. |
separator_style(StyleRefinement) | theme border line | Refine separator lines. |
id(ElementId) | none | Give the marker a stable identity for the accessibility tree. |
role(Role) | presentational | Announce the row to assistive technology, e.g. Role::Status for streaming updates; requires id(...). |
icon(MarkerIcon) | none | Add a typed icon slot. |
content(MarkerContent) | none | Add a typed content slot. |
.child(element) | — | Add arbitrary children. |
Styled methods | compact themed row | Refine the marker’s layout, colors, and typography. |
#MarkerIcon
| Method | Default | Purpose |
|---|---|---|
new() | empty size_4() slot | Create an icon slot. |
.child(element) | — | Add an icon, badge, spinner, or custom element. |
Styled methods | size_4() compact slot | Refine icon geometry and layout. |
#MarkerContent
| Method | Default | Purpose |
|---|---|---|
new() | empty min_w_0() slot | Create content. |
text(text) | static text until loading is enabled | Add text that can receive shimmer. |
.child(element) | — | Add arbitrary rich content. |
Styled methods | inherited text and compact layout | Refine wrapping, colors, spacing, and typography. |
#Related types
- [
MarkerVariant] —Plain,Separator, andBorder. - [
MarkerAlignment] —Start,Center, andEnd. - [
MarkerLoadingStyle] —SpinnerorShimmer. - [
ShimmerStyle] and [ShimmerText] — reusable loading text controls.