'layout.shell': {
kind: 'semantic',
description: 'The frame of an app with a sidebar: one layout.aside beside the main column. Used once, around the signed-in part of the app',
refs: ['arrange.sidebar'],
notFor: [
{ context: 'a page without a sidebar', instead: 'layout.page' },
{ context: 'one page with its own section nav, like settings', instead: 'layout.subnav' },
],
},
'layout.aside': {
kind: 'semantic',
description:
'Content beside the main column: a docs sidebar, filters, a table of contents',
refs: ['arrange.stack'],
binding: { element: 'aside', classes: 'gap-1' },
notFor: [{ context: 'the main content', instead: 'layout.article' }],
},
'nav.page': {
kind: 'semantic',
description:
'A link to a top-level page in site navigation. Pass current on the page you are on',
refs: ['control.menu'],
binding: { classes: 'font-normal' },
notFor: [
{
context: 'switching between views of the same content',
instead: 'nav.view',
},
],
},
'heading.page': {
kind: 'semantic',
description:
'The one title of a page: Settings, Sign in, Privacy policy, a profile name',
refs: ['typography.title', 'tone.default'],
binding: { classes: 'font-medium', element: 'h1' },
notFor: [
{
context: 'a second title on the same page',
instead: 'heading.section',
},
{ context: 'a marketing headline', instead: 'heading.pitch' },
],
},
'text.count': {
kind: 'semantic',
description:
'A number that sums something up: unread notifications, items waiting',
refs: ['typography.label', 'tone.default'],
binding: { element: 'span', classes: 'tabular-nums' },
notFor: [
{
context: 'other numbers about an item, like dates or sizes',
instead: 'text.meta',
},
],
},
'layout.table': {
kind: 'semantic',
description: 'Records with the same fields, compared across: members, invoices, API keys. Write plain thead, tbody, tr, th scope="col" and td inside, with no classes; data-off on a tr greys out a row that is switched off. data-head="bar" sets the header row on a bar and pads the outer columns, for a table that runs edge to edge in a panel with no padding. With more columns than fit on a phone, wrap it in a layout.scroller. data-arrival makes its rows rise into place; give each tr a growing animationDelay to stagger them',
refs: ['arrange.table'],
binding: { element: 'table', classes: 'data-[arrival]:[&_tbody_tr]:animate-[showcase-in_0.5s_ease-out_both] motion-reduce:data-[arrival]:[&_tbody_tr]:animate-none data-[head=bar]:[&_thead_tr]:border-0 data-[head=bar]:[&_thead_th]:bg-surface-hover data-[head=bar]:[&_thead_th]:py-2.5 data-[head=bar]:[&_:is(th,td):first-child]:pl-5 data-[head=bar]:[&_:is(th,td):last-child]:pr-5 data-[head=bar]:[&_tbody_tr:last-child]:border-b' },
notFor: [{ context: 'items read one after another, like a feed or notifications', instead: 'layout.list' }],
},
'text.meta': {
kind: 'semantic',
description:
'Small facts about an item: timestamps, authors, @handles, file sizes',
refs: ['typography.small', 'tone.muted'],
binding: { element: 'span', classes: 'tabular-nums' },
notFor: [
{ context: 'a sentence explaining something', instead: 'text.caption' },
],
},
'choice.open': {
kind: 'semantic',
description: 'Shows what is picked and opens the choices to change it: a sort order, sources. The trigger of a Popover picker',
refs: ['control.select'],
notFor: [{ context: 'one of a few options shown on the page', instead: 'choice.option' }],
},
'action.remove': {
kind: 'semantic',
description:
'Takes an item out of a list without destroying it: remove a source, unsubscribe',
refs: ['control.quiet'],
keywords: ['remove', 'detach'],
notFor: [
{ context: 'permanently deleting data', instead: 'action.delete' },
],
},
'layout.form': {
kind: 'semantic',
description: 'A form: renders <form> and stacks its fields',
refs: ['arrange.stack'],
binding: { element: 'form', classes: 'gap-4' },
notFor: [
{
context: 'a single field with its label and messages',
instead: 'layout.field',
},
],
},
'layout.field': {
kind: 'semantic',
description: 'One form field: its label, input, help text and error',
refs: ['arrange.stack'],
binding: { classes: 'gap-1.5' },
notFor: [{ context: 'a setting switched or picked in place, with its label beside the control', instead: 'layout.setting' }],
},
'text.label': {
kind: 'semantic',
description:
'A few words that name a value: field labels, setting names, column headers. Use as="label" htmlFor=... for form fields',
refs: ['typography.label', 'tone.default'],
binding: { element: 'span' },
notFor: [{ context: 'full sentences', instead: 'text.caption' }],
},
'field.email': {
kind: 'semantic',
description: 'An email address',
refs: ['input.default'],
binding: {
behavior: { type: 'email', inputMode: 'email', autoComplete: 'email' },
},
notFor: [{ context: 'usernames or handles', instead: 'field.text' }],
},
'field.text': {
kind: 'semantic',
description: 'Short free text on one line: names, titles, handles',
refs: ['input.default'],
binding: { behavior: { type: 'text' } },
notFor: [
{ context: 'more than one line', instead: 'field.multiline' },
],
},
'text.caption': {
kind: 'semantic',
description:
'A supporting sentence next to something else: helper text under a field, a note under an image',
refs: ['typography.small', 'tone.muted'],
binding: { element: 'p', classes: 'leading-relaxed' },
notFor: [
{ context: 'a few words that name a value', instead: 'text.label' },
{ context: 'dates, authors or file sizes', instead: 'text.meta' },
{ context: 'error or success messages', instead: 'feedback.error' },
],
},
'feedback.error': {
kind: 'semantic',
description:
'Something failed or input is invalid. Announced to screen readers immediately',
refs: ['typography.label', 'tone.negative'],
binding: { element: 'p', ariaRole: 'alert' },
},
'action.add': {
kind: 'semantic',
description:
'Puts an existing thing into a list or collection: add a source, add a member',
refs: ['control.secondary'],
keywords: ['add', 'include', 'import'],
notFor: [
{
context: 'making something that did not exist',
instead: 'action.create',
},
],
},
'action.save': {
kind: 'semantic',
description:
'Keeps changes the user made to something that already exists',
refs: ['control.primary'],
keywords: ['save', 'update', 'apply'],
notFor: [
{ context: 'making something new', instead: 'action.create' },
],
},
'feedback.success': {
kind: 'semantic',
description: 'An action the user took worked',
refs: ['typography.label', 'tone.positive'],
binding: { element: 'p', ariaRole: 'status' },
notFor: [
{
context: 'neutral status like loading or empty',
instead: 'feedback.info',
},
],
},
'layout.list': {
kind: 'semantic',
description:
'A list of items of the same kind: articles, people, notifications. Children are layout.item',
refs: ['arrange.stack'],
binding: { element: 'ul', classes: 'divide-y divide-input' },
notFor: [
{ context: 'records compared by the same fields, like a members table', instead: 'layout.table' },
{
context: 'equal items shown side by side as a grid',
instead: 'layout.collection',
},
],
},
'layout.item': {
kind: 'semantic',
description: 'One entry in a layout.list',
refs: ['arrange.stack'],
binding: { element: 'li', classes: 'gap-1 py-3' },
notFor: [
{ context: 'a term beside what it means: a prop, a token, a rule', instead: 'layout.term' },
{ context: 'a situation beside the intent it calls for', instead: 'layout.situation' },
{
context: 'a standalone box that is not in a list',
instead: 'layout.panel',
},
],
},
'text.body': {
kind: 'semantic',
description:
'Running text: sentences and paragraphs that explain something',
refs: ['typography.body', 'tone.soft'],
binding: { element: 'p' },
notFor: [
{
context: 'the introduction right under a page title',
instead: 'text.lead',
},
],
},
'action.signout': {
kind: 'semantic',
description: 'Ends the current session and signs the person out',
refs: ['control.quiet'],
keywords: ['signout', 'logout'],
notFor: [{ context: 'going to the sign-in page', instead: 'action.navigate' }],
},