Marker
A quiet inline note with an optional icon, rule or bottom border.
- Status
- Experimental
- Level
- Atom
- Category
- Content
- Adoption
- Not used yet
import { Marker } from "@oration/canon/components/marker";packages/canon/src/components/marker.tsxActivity
- Maya Okafor approved INV-209319:42 AM
- Northwind Freight uploaded a new W-98:15 AM
- Priya Raman flagged a duplicate invoice4:03 PM
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
separatorfor a label between hairlines in a feed or list: Today, Yesterday, or. - With
borderfor a small group label over a short list: Earlier this week, Northwind Freight invoices. - With a
MarkerIconwhen 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
Hairlines, not boxes
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#
- Container. A full-width flex row, 14px Slate Meta, with an 8px gap and a 16px minimum height. Renders a
<div>unlessrenderswaps it. - Icon.
MarkerIcon, anaria-hidden16px box. Icons inside it without asize-*class are sized to 16px. - Content.
MarkerContent, the text. It wraps long words and styles direct child links with an underline. - Rules. With
separator, a hairline before and after the content that fills the space. Withborder, 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.
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.
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.
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.
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#
| State | Treatment |
|---|---|
| Rest | Static text. Marker has no interactive states of its own. |
| Link hover | A link inside the content is underlined and turns Graphite Ink on hover. |
| Wrapping | Long content wraps inside MarkerContent. In the separator variant the content doesn't shrink, so keep it short. |
Behavior#
Markeris built with Base UIuseRender, sorender={<p />}orrender={<li />}changes the element while keeping the classes. Props are merged withmergeProps.- The
separatorvariant draws its rules with::beforeand::after, eachflex-1, so the content centers between them.MarkerContentbecomesflex-noneand centered. - The
bordervariant addsborder-band 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-variantthrough its render state, andMarkerContentreads it withgroup-data-[variant=separator]/marker.
Do and don't#
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.
MarkerIconisaria-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#
| Token | Used for |
|---|---|
--muted-foreground | Text color (Slate Meta) |
--foreground | Link hover |
--border | Separator and bottom rules |
text-sm, gap-2, min-h-4 | 14px text, 8px gap, 16px minimum height |
size-4 | 16px icon box |
API reference#
Marker
The row. Also exported: markerVariants, the class recipe.
Other props spread onto <div> via Base UI useRender.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "separator" | "border" | "default" | Plain, between two hairlines, or over one hairline. |
render | ReactElement | (props, state) => ReactElement | No default | Render as another element, such as <h3 /> or <li />. |
className | string | No default | Merged 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>.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | No default | Merged last. |
MarkerContent
The text of the marker.
Other props spread onto <span>.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | No default | Merged 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.