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::{ParentElement as _, StyleRefinement, Styled as _};
use gpui_component::{
ActiveTheme as _, Icon, IconName, Sizable as _,
badge::Badge,
button::{Button, ButtonVariants as _},
marker::{Marker, 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. |
| 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.
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. |
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. - [
MarkerLoadingStyle] —SpinnerorShimmer. - [
ShimmerStyle] and [ShimmerText] — reusable loading text controls.