Skip to content

Meta line

A 12px line of metadata whose parts are separated by gaps, never by dots.

Status
Beta
Category
Content
Adoption
Not used yet
import { Meta } from "@oration/canon/components/meta";
packages/canon/src/components/meta.tsx
  • Northwind Freight

    Net 3014 open invoicesLast paid Sep 25
    $38,210.00
  • Halcyon

    Net 456 open invoicesLast paid Sep 18
    $12,904.50
  • Orchard Street

    Net 152 open invoicesLast paid Sep 25
    $2,980.00
import { Meta } from "@oration/canon/components/meta";import { MonogramTile } from "@oration/canon/components/monogram-tile";export function Hero() {    const suppliers = [        {            name: "Northwind Freight",            color: "teal" as const,            terms: "Net 30",            open: 14,            lastPaid: "Sep 25",            balance: "$38,210.00",        },        {            name: "Halcyon",            color: "violet" as const,            terms: "Net 45",            open: 6,            lastPaid: "Sep 18",            balance: "$12,904.50",        },        {            name: "Orchard Street",            color: "amber" as const,            terms: "Net 15",            open: 2,            lastPaid: "Sep 25",            balance: "$2,980.00",        },    ];    return (        <ul className="flex w-full max-w-xl flex-col rounded-xl bg-card p-2 text-left shadow-border">            {suppliers.map((supplier) => (                <li                    key={supplier.name}                    className="flex items-center gap-3 rounded-lg px-2 py-2.5"                >                    <MonogramTile                        name={supplier.name}                        color={supplier.color}                        size="lg"                    />                    <div className="min-w-0 flex-1">                        <p className="truncate text-13 font-medium">                            {supplier.name}                        </p>                        <Meta>                            <span>{supplier.terms}</span>                            <span className="tabular-nums">                                {supplier.open} open invoices                            </span>                            <span>Last paid {supplier.lastPaid}</span>                        </Meta>                    </div>                    <span className="text-13 font-medium tabular-nums">                        {supplier.balance}                    </span>                </li>            ))}        </ul>    );}

Usage#

Meta line is the 12px line of small facts under a name in a list row: Net 30, 14 open invoices, Last paid Sep 25. Its parts are separated by a 12px gap and wrap onto a second line, never joined with dots. It exists so the product never writes Net 30 · 14 open invoices · Last paid Sep 25; each part is its own span and layout does the separating. The other thing to get right is the parts themselves: short noun phrases with their numbers, not bare figures that need a legend.

When to use

  • Under a row's name in a list or settings table: a supplier's terms and open invoices, a passkey's added and last-used dates.
  • For a webhook or integration's status, event count and delivery rate, with the status as a status label.
  • Under a card title for the two or three facts that tell similar items apart.
  • Anywhere you're tempted to join facts with a middle dot.

When not to use

  • For headline figures under a page or report title. Use Inline stat strip
  • For metrics with a change and a trend. Use Stat strip
  • For a sentence that describes the item. Write it as a description line in 13px.
  • For values people filter or sort by across many rows. Give them table columns. Use Data grid
  • For option values such as stage or tier on their own. Use Tag

The Layout Over Punctuation Rule

Metadata is never joined into dot-separated strings (A · B · C). Separate it with layout, commas or words. Meta is the layout.

The Tabular Figures Rule

Counts, amounts and rates in a meta line are tabular-nums, so a column of rows lines up. Meta doesn't set it for you.

The Label-Beside-Color Rule

A status in a meta line is a status label, dot plus text, never a colored dot alone.

Anatomy#

Delivering6 events99.4% delivered
  1. Line. A wrapping flex row in 12px Slate Meta, centered vertically.
  2. Status part. Optional, usually first: a status label, dot plus text, at 12px.
  3. Part. A span holding one fact as a short phrase: 6 events, Last paid Sep 25. It can hold an icon or tags.
  4. Gap. 12px between parts and 2px between lines when it wraps. The only separator.

Examples#

With a status

A webhook's status leads as a status label, then its event count and delivery rate. A failing rate can take Signal Red because the label beside it already says Failing.

https://erp.cedarline.example/hooks/payments

Posts settled payments to the ERP

Delivering6 events99.4% delivered, 1,208 in 7 days

https://hooks.halcyon.example/remittance

Sends remittance advice to Halcyon

Failing2 events61% delivered, 44 in 7 days
import { Meta } from "@oration/canon/components/meta";import { StatusLabel } from "@oration/canon/components/status-dot";export function WithStatus() {    return (        <div className="flex w-full max-w-xl flex-col gap-4 rounded-xl bg-card p-4 text-left shadow-border">            <div className="min-w-0">                <p className="truncate font-mono text-xs">                    https://erp.cedarline.example/hooks/payments                </p>                <p className="mt-0.5 truncate text-13 text-muted-foreground">                    Posts settled payments to the ERP                </p>                <Meta className="mt-1">                    <StatusLabel tone="success" className="text-xs">                        Delivering                    </StatusLabel>                    <span>6 events</span>                    <span className="tabular-nums">                        99.4% delivered, 1,208 in 7 days                    </span>                </Meta>            </div>            <div className="min-w-0">                <p className="truncate font-mono text-xs">                    https://hooks.halcyon.example/remittance                </p>                <p className="mt-0.5 truncate text-13 text-muted-foreground">                    Sends remittance advice to Halcyon                </p>                <Meta className="mt-1">                    <StatusLabel tone="danger" className="text-xs">                        Failing                    </StatusLabel>                    <span>2 events</span>                    <span className="text-destructive tabular-nums">                        61% delivered, 44 in 7 days                    </span>                </Meta>            </div>        </div>    );}

Icons and tags

A part can carry a 12px icon or a pair of tags. The gap still does the separating.

Orchard Street

Toronto, ON9:00 AM to 5:00 PM ETPreferredNet 15
import { Meta } from "@oration/canon/components/meta";import { Tag } from "@oration/canon/components/tag";import { ClockIcon, MapPinIcon } from "lucide-react";export function WithIconsAndTags() {    return (        <div className="flex w-full max-w-xl flex-col gap-1 text-left">            <p className="text-13 font-medium">Orchard Street</p>            <Meta>                <span className="flex items-center gap-1">                    <MapPinIcon aria-hidden="true" className="size-3" />                    Toronto, ON                </span>                <span className="flex items-center gap-1">                    <ClockIcon aria-hidden="true" className="size-3" />                    9:00 AM to 5:00 PM ET                </span>                <span className="flex gap-1">                    <Tag color="green">Preferred</Tag>                    <Tag>Net 15</Tag>                </span>            </Meta>        </div>    );}

Wrapping

In a narrow card, whole parts move to a second line 2px below. Nothing truncates and no separator is left dangling.

Remittance questions

Assigned to Aisha Bello9 waitingFirst response in 12 min
import { Meta } from "@oration/canon/components/meta";export function Wrapping() {    return (        <div className="w-full max-w-64 rounded-xl bg-card p-4 text-left shadow-border">            <p className="text-13 font-medium">Remittance questions</p>            <Meta className="mt-0.5">                <span>Assigned to Aisha Bello</span>                <span className="tabular-nums">9 waiting</span>                <span>First response in 12 min</span>            </Meta>        </div>    );}

In settings rows

Passkeys in account security: the name, then when it was added and last used. Removing one offers Undo.

MacBook Pro

Added Aug 3Last used today

YubiKey 5C

Added Mar 12Last used Sep 21
import { Button } from "@oration/canon/components/button";import { Meta } from "@oration/canon/components/meta";import { toast } from "@oration/canon/components/toast";import { FingerprintIcon, KeyRoundIcon } from "lucide-react";import * as React from "react";export function SettingsRows() {    const [passkeys, setPasskeys] = React.useState([        {            id: "pk_1",            name: "MacBook Pro",            icon: FingerprintIcon,            added: "Aug 3",            lastUsed: "today",        },        {            id: "pk_2",            name: "YubiKey 5C",            icon: KeyRoundIcon,            added: "Mar 12",            lastUsed: "Sep 21",        },    ]);    const remove = (id: string) => {        const before = passkeys;        const target = passkeys.find((passkey) => passkey.id === id);        setPasskeys((current) =>            current.filter((passkey) => passkey.id !== id),        );        toast.add({            title: `Removed ${target?.name ?? "passkey"}`,            actionProps: {                children: "Undo",                onClick: () => setPasskeys(before),            },        });    };    return (        <div className="flex w-full max-w-xl flex-col divide-y divide-border rounded-xl bg-card text-left shadow-border">            {passkeys.map((passkey) => (                <div                    key={passkey.id}                    className="flex items-center gap-3 px-4 py-3"                >                    <span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">                        <passkey.icon aria-hidden="true" className="size-4" />                    </span>                    <div className="min-w-0 flex-1">                        <p className="truncate text-sm font-medium">                            {passkey.name}                        </p>                        <Meta>                            <span>Added {passkey.added}</span>                            <span>Last used {passkey.lastUsed}</span>                        </Meta>                    </div>                    <Button                        type="button"                        variant="ghost"                        size="sm"                        onClick={() => remove(passkey.id)}                    >                        Remove                    </Button>                </div>            ))}            {passkeys.length === 0 ? (                <p className="px-4 py-3 text-13 text-muted-foreground">                    No passkeys. You sign in with your password and a code.                </p>            ) : null}        </div>    );}

States#

States
StateTreatment
One lineParts sit on one line with 12px gaps.
WrappedWhen the row is narrow, whole parts move to the next line 2px below. A part never breaks in the middle unless it's longer than the row.
Emphasized partA part that's a problem can take a semantic text color, such as a failing delivery rate in Signal Red, beside a status label that says so.

Behavior#

  • Meta is a styled div; every prop spreads onto it. Put each fact in its own child element so the gap can separate them.
  • Text nodes directly inside Meta become flex items too, so Net 30 · 14 invoices as one string is still one part. Split it.
  • It wraps instead of truncating. If a row must stay one line, truncate the longest part yourself with truncate and a title.
  • Parts can be interactive, such as a link to the queue, but the line itself is not.

Do and don't#

Northwind Freight

Net 3014 open invoicesLast paid Sep 25
Do. Set each fact in its own part and let the gap separate them.

Northwind Freight

Net 30 · 14 open invoices · Last paid Sep 25

Don't. Join them with middle dots. Net 30 · 14 open invoices · Last paid Sep 25 breaks the Layout Over Punctuation Rule.

Halcyon

6 open invoices27 days to payLast paid Sep 18
Do. Write each part as a phrase with its noun: 6 open invoices, 27 days to pay.

Halcyon

627d9/18
Don't. Show bare figures such as 6, 27d and 9/18. Nobody knows which number is which.

ERP payments hook

Failing61% delivered
Do. Show a status as a dot with its label: Failing.

ERP payments hook

61% delivered
Don't. Show a red dot alone. It fails in grayscale and for anyone who can't tell red from green.

Content#

  • Two to four parts. More than that belongs in columns or a detail view.
  • Each part is a short phrase, sentence case, no trailing period: Net 30, Added Aug 3, Last used today.
  • Numbers carry their noun and are tabular: 14 open invoices, 99.4% delivered.
  • Dates are short: Sep 25, today, Mar 12. Keep the same form across rows.
  • Commas and words are fine inside a part: 99.4% delivered, 1,208 in 7 days, Assigned to Aisha Bello.
  • Put the most distinguishing fact first, and a status first of all.

Accessibility#

  • Parts are separated only visually. A screen reader reads the line as one run of words, so write parts that still make sense back to back: Net 30, 14 open invoices, Last paid Sep 25 reads fine; 6 27d 9/18 doesn't.
  • Where two parts could run together, such as two numbers, add a visually hidden comma or write the noun into each part.
  • Icons inside parts are decorative and aria-hidden="true"; the text carries the fact.
  • Keep the line in Slate Meta. Don't lighten it to Faint Slate or with opacity; 12px text needs the contrast.
  • A status part is a status label, so the state is read aloud as a word, not a color.

Design tokens#

Design tokens
TokenUsed for
--muted-foregroundText color of the line
text-xs12px size with a 16px line height

API reference#

Meta

The meta line: flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground.

Other props spread onto <div>.

Props of Meta
PropTypeDefaultDescription
childrenReactNodeNo defaultOne element per fact.
classNamestringNo defaultMerged after the defaults, such as mt-1. Use text-[13px] rather than text-13 to change the size; the merge drops text-13.

Known gaps#

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

Only the settings kit imports Meta, and re-exports it to settings pages. Other lists hand-roll the same flex gap-x-3 text-xs text-muted-foreground line.

The app still has about 20 dot-joined strings that Meta should replace, in the contact center (callback rows, team cells, the monitor sheet, swap requests) and ticketing (view rows, report insights).

Meta has no data-slot and doesn't set tabular-nums, so numbers in it only align if each call site adds it.

Parts are separated for the eye only; there's no built-in separator for screen readers.