Skip to content

Marker

A quiet inline note with an optional icon, rule or bottom border.

Level
Atom
Category
Content
Adoption
Not used yet
import { Marker } from "@oration/canon/components/marker";
packages/canon/src/components/marker.tsx

Activity

Today
  • Maya Okafor approved INV-209319:42 AM
  • Northwind Freight uploaded a new W-98:15 AM
Friday, Sep 25
  • Priya Raman flagged a duplicate invoice4:03 PM
Synced from NetSuite 4 minutes ago.
import { Marker, MarkerContent, MarkerIcon } from "@oration/canon/components/marker";import { toast } from "@oration/canon/components/toast";import { RefreshCwIcon } from "lucide-react";export function Hero() {    const days = [        {            day: "Today",            events: [                {                    who: "Maya Okafor",                    what: "approved INV-20931",                    at: "9:42 AM",                },                {                    who: "Northwind Freight",                    what: "uploaded a new W-9",                    at: "8:15 AM",                },            ],        },        {            day: "Friday, Sep 25",            events: [                {                    who: "Priya Raman",                    what: "flagged a duplicate invoice",                    at: "4:03 PM",                },            ],        },    ];    return (        <div className="flex w-full max-w-md flex-col gap-3 rounded-xl bg-card p-4 shadow-border">            <p className="text-sm font-medium text-foreground">Activity</p>            {days.map((group) => (                <div key={group.day} className="flex flex-col gap-1">                    <Marker variant="separator" className="text-xs">                        <MarkerContent>{group.day}</MarkerContent>                    </Marker>                    <ul className="flex flex-col">                        {group.events.map((event) => (                            <li                                key={event.what}                                className="flex h-9 items-center justify-between gap-3 text-13"                            >                                <span className="min-w-0 truncate text-foreground">                                    <span className="font-medium">                                        {event.who}                                    </span>{" "}                                    {event.what}                                </span>                                <span className="shrink-0 text-muted-foreground tabular-nums">                                    {event.at}                                </span>                            </li>                        ))}                    </ul>                </div>            ))}            <Marker className="text-[13px]">                <MarkerIcon>                    <RefreshCwIcon />                </MarkerIcon>                <MarkerContent>                    Synced from NetSuite 4 minutes ago.{" "}                    <button                        type="button"                        className="underline underline-offset-3 hover:text-foreground"                        onClick={() =>                            toast.add({                                title: "Sync started",                                description: "NetSuite, 1,208 suppliers.",                            })                        }                    >                        Sync now                    </button>                </MarkerContent>            </Marker>        </div>    );}

Usage#

Marker is a quiet line of Slate Meta text that annotates what's around it: a sync note under a table, a day divider in an activity feed, a small heading over a group. It comes in three variants: plain with an optional icon, separator with a hairline on each side, and border with a hairline underneath. It is meta, not a message, so the mistake is using it for anything the person must act on; that is an alert or inline field help.

When to use

  • For a note under or beside content: Synced from NetSuite 4 minutes ago, Only admins can change approval rules.
  • With separator for a label between hairlines in a feed or list: Today, Yesterday, or.
  • With border for a small group label over a short list: Earlier this week, Northwind Freight invoices.
  • With a MarkerIcon when a 16px icon helps the note scan: a lock for permissions, a clock for timing.

When not to use

  • For a warning or error the person has to act on. Use Alert
  • For help and errors tied to a form field. Use Field
  • For a plain rule with no text. Use Separator
  • For a section title with an action on the right. Use Section header
  • For a timestamp and author line on a record. Use Meta line

Meta is Slate

Marker text is Slate Meta, the color of timestamps, counts and helper text. It sits below the content it annotates in the hierarchy and never competes with a heading.

Hairlines, not boxes

The separator and border variants draw 1px hairlines in --border. Don't wrap a marker in a card or well to make it stand out; if it needs to stand out, it isn't a marker.

Anatomy#

Only admins can change approval rules
Today
Earlier this week
  1. Container. A full-width flex row, 14px Slate Meta, with an 8px gap and a 16px minimum height. Renders a <div> unless render swaps it.
  2. Icon. MarkerIcon, an aria-hidden 16px box. Icons inside it without a size-* class are sized to 16px.
  3. Content. MarkerContent, the text. It wraps long words and styles direct child links with an underline.
  4. Rules. With separator, a hairline before and after the content that fills the space. With border, one hairline underneath with 8px of padding above it.

Examples#

Variants

Plain for a note, separator for a label between two hairlines, border for a label over one.

Synced from NetSuite 4 minutes ago
Today
Earlier this week
import { Marker, MarkerContent } from "@oration/canon/components/marker";export function Variants() {    return (        <div className="flex w-full max-w-sm flex-col gap-6">            <Marker>                <MarkerContent>                    Synced from NetSuite 4 minutes ago                </MarkerContent>            </Marker>            <Marker variant="separator">                <MarkerContent>Today</MarkerContent>            </Marker>            <Marker variant="border">                <MarkerContent>Earlier this week</MarkerContent>            </Marker>        </div>    );}

With an icon

MarkerIcon holds a 16px icon that helps the note scan. It is hidden from screen readers, so the text says everything.

Only admins can change approval rules
Payment runs go out at 2:00 PM CT
W-9 received from Halcyon on Sep 21
import { Marker, MarkerContent, MarkerIcon } from "@oration/canon/components/marker";import { ClockIcon, FileCheckIcon, LockIcon } from "lucide-react";export function WithIcon() {    return (        <div className="flex w-full max-w-sm flex-col gap-3">            <Marker>                <MarkerIcon>                    <LockIcon />                </MarkerIcon>                <MarkerContent>                    Only admins can change approval rules                </MarkerContent>            </Marker>            <Marker>                <MarkerIcon>                    <ClockIcon />                </MarkerIcon>                <MarkerContent>Payment runs go out at 2:00 PM CT</MarkerContent>            </Marker>            <Marker>                <MarkerIcon>                    <FileCheckIcon />                </MarkerIcon>                <MarkerContent>                    W-9 received from Halcyon on Sep 21                </MarkerContent>            </Marker>        </div>    );}

Group label

The border variant rendered as an <h3> over each group, so the groups are headings for screen readers. text-[13px] matches the 13px rows.

Due this week

  • Northwind FreightINV-20931$48,210.50
  • Halcyon LogisticsINV-20944$12,780.00

Due next week

  • Orchard Street SupplyINV-20952$6,395.25
import { Marker, MarkerContent } from "@oration/canon/components/marker";export function GroupLabel() {    const groups = [        {            label: "Due this week",            invoices: [                {                    id: "INV-20931",                    supplier: "Northwind Freight",                    amount: "$48,210.50",                },                {                    id: "INV-20944",                    supplier: "Halcyon Logistics",                    amount: "$12,780.00",                },            ],        },        {            label: "Due next week",            invoices: [                {                    id: "INV-20952",                    supplier: "Orchard Street Supply",                    amount: "$6,395.25",                },            ],        },    ];    return (        <div className="flex w-full max-w-md flex-col gap-5">            {groups.map((group) => (                <section key={group.label} className="flex flex-col gap-1">                    <Marker                        variant="border"                        className="text-[13px]"                        render={(props) => <h3 {...props} />}                    >                        <MarkerContent>{group.label}</MarkerContent>                    </Marker>                    <ul className="flex flex-col">                        {group.invoices.map((invoice) => (                            <li                                key={invoice.id}                                className="flex h-9 items-center gap-3 text-13"                            >                                <span className="flex-1 text-foreground">                                    {invoice.supplier}                                </span>                                <span className="font-mono text-xs text-muted-foreground">                                    {invoice.id}                                </span>                                <span className="w-24 text-right text-foreground tabular-nums">                                    {invoice.amount}                                </span>                            </li>                        ))}                    </ul>                </section>            ))}        </div>    );}

Or divider

A one-word separator between two ways to do the same thing, here at 12px between sign-in options.

or
import { Button } from "@oration/canon/components/button";import { Input } from "@oration/canon/components/input";import { Label } from "@oration/canon/components/label";import { Marker, MarkerContent } from "@oration/canon/components/marker";import { toast } from "@oration/canon/components/toast";import { SendIcon } from "lucide-react";import * as React from "react";export function OrDivider() {    const id = React.useId();    return (        <div className="flex w-full max-w-xs flex-col gap-4">            <Button                type="button"                variant="outline"                onClick={() => toast.add({ title: "Redirecting to Google" })}            >                Continue with Google            </Button>            <Marker variant="separator" className="text-xs">                <MarkerContent>or</MarkerContent>            </Marker>            <div className="flex flex-col gap-2">                <Label htmlFor={id}>Work email</Label>                <Input id={id} type="email" placeholder="maya@cedarline.com" />            </div>            <Button                type="button"                onClick={() =>                    toast.add({                        title: "Check your inbox",                        description: "We sent a sign-in link.",                    })                }            >                <SendIcon data-icon="inline-start" aria-hidden="true" />                Email me a link            </Button>        </div>    );}

Under a table

A count note under a list, inset 4px to line up with the rows' text.

Northwind Freight
Halcyon Logistics
Orchard Street Supply
Showing 3 of 48 suppliers in the Friday run
import { Marker, MarkerContent } from "@oration/canon/components/marker";export function UnderATable() {    return (        <div className="flex w-full max-w-md flex-col gap-2">            <div className="overflow-hidden rounded-xl bg-card shadow-border">                {[                    "Northwind Freight",                    "Halcyon Logistics",                    "Orchard Street Supply",                ].map((name) => (                    <div                        key={name}                        className="flex h-9 items-center border-b border-border px-3 text-13 text-foreground last:border-b-0"                    >                        {name}                    </div>                ))}            </div>            <Marker className="px-1 text-[13px]">                <MarkerContent>                    Showing 3 of 48 suppliers in the Friday run                </MarkerContent>            </Marker>        </div>    );}

States#

States
StateTreatment
RestStatic text. Marker has no interactive states of its own.
Link hoverA link inside the content is underlined and turns Graphite Ink on hover.
WrappingLong content wraps inside MarkerContent. In the separator variant the content doesn't shrink, so keep it short.

Behavior#

  • Marker is built with Base UI useRender, so render={<p />} or render={<li />} changes the element while keeping the classes. Props are merged with mergeProps.
  • The separator variant draws its rules with ::before and ::after, each flex-1, so the content centers between them. MarkerContent becomes flex-none and centered.
  • The border variant adds border-b and 8px of bottom padding; put it directly above the list it labels.
  • Items are vertically centered. With content that wraps to two lines, the icon centers on the block rather than the first line.
  • The root exposes data-variant through its render state, and MarkerContent reads it with group-data-[variant=separator]/marker.

Do and don't#

Synced from NetSuite 4 minutes ago
Do. Keep a marker to one short line of meta: when something synced, who can change it, which day a group is.
Update the bank account before Friday or 48 suppliers won't be paid
Don't. Put a warning or a call to action in a marker. Slate text at 14px reads as a footnote, and people skip it.
Yesterday
Do. Use the separator variant for day dividers in a feed, with one word or a date.
Everything below happened before the payment run was rescheduled
Don't. Put a sentence between the rules. The rules shrink to nothing and the divider reads as a paragraph.

Content#

  • Write meta as a fragment in sentence case, no trailing period for one clause: Synced from NetSuite 4 minutes ago.
  • Relative days in feed dividers: Today, Yesterday, then dates: Thursday, Sep 24.
  • Say who and when rather than what happened generally: Edited by Priya Raman at 9:42 AM.
  • Permission notes name the role: Only admins can change approval rules.

Accessibility#

  • Marker renders a plain <div> with no role. For a group label, render it as a heading (render={<h3 />}) so screen reader users can jump between groups.
  • The separator rules are pseudo-elements and aren't announced; only the text is read.
  • MarkerIcon is aria-hidden. The text has to carry the meaning on its own.
  • Slate Meta on white passes 4.5:1 at 14px; don't dim it further with opacity.
  • Links inside the content are underlined so they don't rely on color.

Design tokens#

Design tokens
TokenUsed for
--muted-foregroundText color (Slate Meta)
--foregroundLink hover
--borderSeparator and bottom rules
text-sm, gap-2, min-h-414px text, 8px gap, 16px minimum height
size-416px icon box

API reference#

Marker

The row. Also exported: markerVariants, the class recipe.

Other props spread onto <div> via Base UI useRender.

Props of Marker
PropTypeDefaultDescription
variant"default" | "separator" | "border""default"Plain, between two hairlines, or over one hairline.
renderReactElement | (props, state) => ReactElementNo defaultRender as another element, such as <h3 /> or <li />.
classNamestringNo defaultMerged into the variant classes. Use text-[13px], not text-13, to change the size.

MarkerIcon

A 16px, aria-hidden icon box.

Other props spread onto <span>.

Props of MarkerIcon
PropTypeDefaultDescription
classNamestringNo defaultMerged last.

MarkerContent

The text of the marker.

Other props spread onto <span>.

Props of MarkerContent
PropTypeDefaultDescription
classNamestringNo defaultMerged last.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

Nothing in apps/web imports Marker yet. Feed dividers, sync notes and group labels are written by hand where they appear.

Passing text-13 through className is read as a color by cn from the cn package, which drops text-muted-foreground and leaves the text at 14px in Graphite Ink. Use text-[13px].

The default is 14px, while DESIGN.md sets dense meta at 13px and Caption at 12px.

Items are centered, so on a wrapped two-line note the icon floats to the middle instead of sitting on the first line.