Siteplane UI

Marker

Show a line in a conversation that is not a message, such as a status, a note or a date.

Markdown
Thinking
"use client";

import {
  Marker,
  MarkerContent,
  MarkerIcon,
} from "@/siteplane/components/marker";
import { MotionShimmer } from "@/siteplane/components/motion";
import { Spinner } from "@/siteplane/components/spinner";

export default function Example() {
  return (
    <div className="w-full max-w-md">
      <Marker role="status">
        <MarkerIcon>
          <Spinner />
        </MarkerIcon>
        <MarkerContent>
          <MotionShimmer>Thinking</MotionShimmer>
        </MarkerContent>
      </Marker>
    </div>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/marker

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/bubble @siteplane/message @siteplane/motion @siteplane/spinner

Usage

import { ArrowRightLeftIcon } from "lucide-react";
import {
  Marker,
  MarkerContent,
  MarkerIcon,
} from "@/siteplane/components/marker";

<Marker>
  <MarkerIcon>
    <ArrowRightLeftIcon />
  </MarkerIcon>
  <MarkerContent>Switched to the Thorough model</MarkerContent>
</Marker>;

Composition

Marker
├── MarkerIcon (optional)
└── MarkerContent

A marker is a row of quiet text, 14 px in the tertiary color, as wide as the thread. Between turns it is a row of its own in a Message Scroller; inside a turn it sits in the content of the Message, such as the status of a reply that is on its way. For the steps an agent took, use Trace instead.

Live status

Give a line that changes role="status", so screen readers say it when it appears. For a reply that is on its way, put a Spinner in MarkerIcon and the words in MotionShimmer, which sweeps a highlight across them and stands still under reduced motion. The example at the top of this page is the Thinking status of a reply that was sent and has no words yet.

System notes

A note records something that happened in the conversation, such as a change of model or a history that was compacted. Lead it with an icon in MarkerIcon, 16 px unless the icon has a size class of its own. The icon is hidden from assistive technology, so the words carry the meaning, and it keeps to the first line when a long note wraps.

Switched to the Thorough model
Conversation compacted
"use client";

import { ArrowRightLeftIcon, FoldVerticalIcon } from "lucide-react";

import {
  Marker,
  MarkerContent,
  MarkerIcon,
} from "@/siteplane/components/marker";

export default function Example() {
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <Marker>
        <MarkerIcon>
          <ArrowRightLeftIcon />
        </MarkerIcon>
        <MarkerContent>Switched to the Thorough model</MarkerContent>
      </Marker>
      <Marker>
        <MarkerIcon>
          <FoldVerticalIcon />
        </MarkerIcon>
        <MarkerContent>Conversation compacted</MarkerContent>
      </Marker>
    </div>
  );
}

variant="border" draws a line under the note, for a note that stands apart from the turns below it, such as the note at the top of a shared conversation.

<Marker variant="border">
  <MarkerIcon>
    <UsersIcon />
  </MarkerIcon>
  <MarkerContent>Shared with the Customer success team</MarkerContent>
</Marker>

Separators

variant="separator" puts the content between two divider lines, for a date such as Today or for a break such as a summary of earlier messages. The lines are the kit's Separator without a role, so the label is read as plain text. An icon or a Button beside the content stays with it between the lines, for example Restore on a saved checkpoint.

Today
Earlier messages were summarized
"use client";

import { ArchiveIcon } from "lucide-react";

import {
  Marker,
  MarkerContent,
  MarkerIcon,
} from "@/siteplane/components/marker";

export default function Example() {
  return (
    <div className="flex w-full max-w-md flex-col gap-8">
      <Marker variant="separator">
        <MarkerContent>Today</MarkerContent>
      </Marker>
      <Marker variant="separator">
        <MarkerIcon>
          <ArchiveIcon />
        </MarkerIcon>
        <MarkerContent>Earlier messages were summarized</MarkerContent>
      </Marker>
    </div>
  );
}

Stopped response

When the person presses Stop, keep the words that arrived and add a marker after them that says the response was stopped, inside the assistant's turn. With role="status", screen readers say it once when it appears.

Summarize this week's support tickets.
Support received 128 tickets this week, up 9 percent on last week. Billing questions made up 31 percent of them, mostly about the invoice
You stopped the response.
"use client";

import { Bubble, BubbleContent } from "@/siteplane/components/bubble";
import { Marker, MarkerContent } from "@/siteplane/components/marker";
import { Message, MessageContent } from "@/siteplane/components/message";

export default function Example() {
  return (
    <div className="flex w-full max-w-md flex-col gap-8">
      <Message align="end">
        <MessageContent>
          <Bubble align="end" variant="muted">
            <BubbleContent>
              Summarize this week's support tickets.
            </BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent>
          <Bubble variant="ghost">
            <BubbleContent>
              Support received 128 tickets this week, up 9 percent on last week.
              Billing questions made up 31 percent of them, mostly about the
              invoice
            </BubbleContent>
          </Bubble>
          <Marker role="status">
            <MarkerContent>You stopped the response.</MarkerContent>
          </Marker>
        </MessageContent>
      </Message>
    </div>
  );
}

AI SDK

The lines of a marker follow from the chat's status and its events. The examples on this page show fixed lines; the code below derives them from useChat instead.

Submitted

While status is submitted, the reply has no message yet, and the Thinking status stands in for it until its first part arrives. The Message Scroller page shows it as a pending row of the thread.

// After the last message of the thread:
status === "submitted" ? (
  <Marker role="status">
    <MarkerIcon>
      <Spinner />
    </MarkerIcon>
    <MarkerContent>
      <MotionShimmer>Thinking</MotionShimmer>
    </MarkerContent>
  </Marker>
) : null;

Stopped replies

stop ends the stream, and onFinish then reports isAbort. Keep the ids of the replies that were stopped, and show the marker after them.

const [stopped, setStopped] = useState<string[]>([]);
const { messages, status, stop } = useChat({
  onFinish: ({ isAbort, message }) => {
    if (isAbort) setStopped((ids) => [...ids, message.id]);
  },
});

// In the turn of an assistant message, after its text:
stopped.includes(message.id) ? (
  <Marker role="status">
    <MarkerContent>You stopped the response.</MarkerContent>
  </Marker>
) : null;

Dates

A UIMessage carries no date. Keep one with each message, for example in its metadata, and put a separator marker before the first message of each day.

API reference

Component properties
PropTypeDefaultDescription
Marker.variantdefault | border | separatordefaultdefault is a row of text; border draws a rule under the row; separator puts the content between two divider lines.
Marker.roleARIA rolenonePass status for a line that changes, such as a live status. A separator with a label needs no role.
MarkerIconspan props—A decorative icon before the content, hidden from assistive technology. The glyph is 16 px unless it has a size class, and stays on the first line of a note that wraps.
MarkerContentspan props—The words of the marker. Long words and URLs wrap instead of widening the thread.

Marker renders a div and accepts its props, a ref and className; MarkerIcon and MarkerContent render spans. A marker takes no render prop, so an action is a Button inside it, beside the content. The type MarkerVariant is exported.

Read the installed source.

Usage notes

A marker is not a message and has no avatar or actions of its own. Use Message for a turn and Trace for the steps an agent took. Its color is quiet on purpose, so put everything that matters into its words.