← All components

Marker

The quiet lines between turns: what the system did, where the day changed, what is being worked on.

  1. Which plans renew in October, and what are they worth?

The idea

None of these is anyone speaking, so none wears a bubble or a mark. They are faint 12px text and hairlines, a step below the conversation, easy to read past and there when looked for. A note is centred because it belongs to neither side; a status sits where the answer will start, with the same breathing dot as the thinking line, and it is a live region so the change from searching to read is heard.

Composes ChatMessage.

Install

terminal1 line
npm i mihirmodi-ui motion

The tokens, the theme script and the routing provider are a one-time job — see Setup. Or skip the package entirely and take the source at the foot of this page.

Usage

Marker — usage11 lines
import { ChatMessage, ChatThread, Marker } from "mihirmodi-ui";

<ChatThread>
  <Marker as="li" variant="separator">Today</Marker>
  <ChatMessage from="user">Which plans renew in October?</ChatMessage>
  <Marker as="li" icon={<SwitchIcon />}>Switched to Atlas Deep for this question</Marker>
  {/* A live region: the change from working to done is announced. */}
  <Marker as="li" variant="status" active={searching}>
    {searching ? "Searching 4 sources…" : "Read 4 sources"}
  </Marker>
</ChatThread>

Props

PropNotes
variant"note" | "separator" | "status"A system note, a labelled break in time, or work in progress.
activebooleanStatus only. The dot breathes while true; after, it holds still or `icon` takes its place.
iconReactNodeA small decorative mark before the words.
as"div" | "li"`li` inside a ChatThread, which is a list.
labelstringSeparator only: the accessible name when the label is not plain text.

Source

Exactly what the package ships — this is read out of the source at build time, not a copy. Take it and adapt it if you would rather not add a dependency.

packages/ui/src/components/chat/Marker.tsx83 lines
import type { ReactNode } from "react";

export type MarkerProps = {
  /**
   * `note` — something the system did ("Model switched to Atlas 2").
   * `separator` — a labelled break in time ("Today").
   * `status` — work in progress ("Searching 4 sources…"), announced as it changes.
   */
  variant?: "note" | "separator" | "status";
  children: ReactNode;
  /** A small mark before the words. Decorative — the words carry the meaning. */
  icon?: ReactNode;
  /** `status` only: still working. The dot breathes while true; after, it holds still, or `icon` takes its place. */
  active?: boolean;
  /** `separator` only: the accessible name, when `children` is not plain text. */
  label?: string;
  /** `li` inside a ChatThread, which is a list; `div` anywhere else. */
  as?: "div" | "li";
  className?: string;
};

/**
 * The quiet lines between turns: what the system did, when the day changed,
 * what is being worked on right now.
 *
 * None of them is anyone speaking, so none of them wears a bubble or a mark —
 * they are faint 12px text and hairlines, a step below the conversation they
 * sit in, readable when looked for and easy to read past. A note is centred
 * because it belongs to neither side; a status sits at the left, where the
 * answer it is working toward will begin.
 *
 * A status that is still running carries the same breathing dot as
 * ChatThinking, an opacity pulse rather than a spinner, and it is a live region
 * so "Searching 4 sources…" becoming "Read 4 sources" is heard as well as seen.
 */
export function Marker({ variant = "note", children, icon, active = false, label, as = "div", className = "" }: MarkerProps) {
  const Tag = as;
  const mark = icon && (
    <span aria-hidden className="flex h-3.5 w-3.5 shrink-0 items-center justify-center [&>svg]:h-3.5 [&>svg]:w-3.5">
      {icon}
    </span>
  );

  if (variant === "separator") {
    return (
      <Tag
        role="separator"
        aria-label={label ?? (typeof children === "string" ? children : undefined)}
        className={`flex items-center gap-3 text-[12px] tabular-nums text-faint ${className}`}
      >
        <span aria-hidden className="h-0 flex-1 border-t-[0.5px] border-gray-alpha-400" />
        <span className="flex shrink-0 items-center gap-1.5">
          {mark}
          {children}
        </span>
        <span aria-hidden className="h-0 flex-1 border-t-[0.5px] border-gray-alpha-400" />
      </Tag>
    );
  }

  if (variant === "status") {
    return (
      <Tag role="status" aria-live="polite" className={`flex items-center gap-2 text-[13px] text-quiet ${className}`}>
        {/* While running it is always the dot; once settled, the icon if there is one. */}
        {active || !mark ? (
          <span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center" aria-hidden>
            <span className={`h-1.5 w-1.5 rounded-full ${active ? "thinking-dot bg-ink" : "bg-gray-500"}`} />
          </span>
        ) : (
          mark
        )}
        <span className="min-w-0">{children}</span>
      </Tag>
    );
  }

  return (
    <Tag className={`flex items-center justify-center gap-1.5 text-center text-[12px] text-faint ${className}`}>
      {mark}
      <span className="min-w-0">{children}</span>
    </Tag>
  );
}