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;
#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
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"))
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
Separator adds a flexible line on each side of the content:
Marker::new()
.with_variant(MarkerVariant::Separator)
.content(MarkerContent::new().text("Today"))
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 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"))
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
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…"))
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
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")),
)
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.
#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.