Marker
The quiet lines between turns: what the system did, where the day changed, what is being worked on.
- Today
- 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
npm i mihirmodi-ui motionThe 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
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
| Prop | Type | Notes |
|---|---|---|
| variant"note" | "separator" | "status" | "note" | "separator" | "status"= "note" | A system note, a labelled break in time, or work in progress. |
| activeboolean | boolean= false | Status only. The dot breathes while true; after, it holds still or `icon` takes its place. |
| iconReactNode | ReactNode | A small decorative mark before the words. |
| as"div" | "li" | "div" | "li"= "div" | `li` inside a ChatThread, which is a list. |
| labelstring | string | Separator 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.
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>
);
}