` (pixel width on a field; use `fullWidth` in a sized `Grid.Item`) ## Related * [Stack](/components/stack/) * [Grid](/components/grid/) * [Card](/components/card/) * [Divider](/components/divider/) * [Typography](/foundations/typography/)
# Typography
> Two families do the work: a serif display face for headings and a humanist sans for everything else, with a monospace reserved for counts and code. The type sca
Two families do the work: a serif display face for headings and a humanist sans for everything else, with a monospace reserved for counts and code. The type scale is six sizes (`--fontSize-0` to `--fontSize-5`) and two line heights. App code reaches the scale only through `Heading` and `Text`; it never sets `font-family`, `font-size`, or `line-height` itself. ## Rules [Section titled “Rules”](#rules) 1. `--fontFamily-display` (serif) is for headings only, and it reaches the page only through `Heading`. Components that own a title slot (`Dialog` `title`, `Table.Title`, `EmptyState` `title`) apply it internally. App code never sets a serif on any other element. 2. `--fontFamily-body` (humanist sans) is the default for body copy, controls, labels, and table cells. `Text` and every control apply it; app code does not restate it. 3. `--fontFamily-mono` is for counts, identifiers, hashes, and code. `Badge` applies it to counts. In app code it appears only on `
` or `` content, never on prose or labels. 4. The size scale is the only set of font sizes on screen. Each step has a job (see the table); nothing is set to a size between steps. 5. `Heading` `level` maps 1 to 4 onto `--fontSize-5` (36), `--fontSize-4` (28), `--fontSize-3` (20), `--fontSize-2` (16). `Text` `size` maps `small`, `medium`, `large` onto `--fontSize-0` (12), `--fontSize-1` (14), `--fontSize-2` (16). Nothing in app code is larger than `--fontSize-5`. 6. `Heading` uses `--lineHeight-tight` (1.2); `Text` and body copy use `--lineHeight-default` (1.5). Single-line controls are tight. App code does not set `line-height`. 7. Body paragraphs run 60 to 75 characters per line. Line length is controlled with layout (`Grid` `columns`, `Grid.Item` `span`, a capped container), never by shrinking the font. 8. `Heading` is for text that structures the page (page title, section, card, dialog). `Text` is for everything else (copy, captions, descriptions, cell content, inline emphasis). If a piece of text is not a section title it is `Text`, even when it is prominent (use `weight="semibold"`). 9. One `level={1}` per page. Heading ranks follow reading order without skipping. When the visual size and the document rank differ, keep the rank honest with `as` (``). 10. Raw `` to `` never appear in app code; they are `Heading`. A raw ` ` or `` is acceptable only when it carries no styling at all; anything styled is `Text`. 11. Weight comes through the `weight` prop of `Text`: `normal` (`--fontWeight-normal`, 400), `medium` (`--fontWeight-medium`, 500), `semibold` (`--fontWeight-semibold`, 600). `Heading` is semibold by itself (`level={4}` is medium). `--fontWeight-bold` (700) is reserved for component internals (`Banner` and `Toast` titles) and is never set by app code. Bold is not used for emphasis inside running copy; `Text as="strong"` or `weight="medium"` marks a label or a value. 12. `truncate` (on `Heading` and `Text`) is for single-line overflow in constrained cells and cards where the full text is reachable elsewhere. It is never applied to error messages, validation text, or a page title. 13. Uppercase and tracked type (`--letterSpacing-tag`) belong to component internals: `Table.Header` and a `Divider` with a text child. `Tag` text is sentence case, not caps. App code never sets `text-transform` or `letter-spacing`. 14. All headings, labels, and buttons are sentence case (see content.md). ## The scale [Section titled “The scale”](#the-scale) | Token | Size | Used for | | -------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------- | | `--fontSize-0` | 12px | `Text size="small"`: captions, timestamps, `FormControl.Caption`, `FormControl.Validation`, `Tag size="small"`, `Badge`, `Table.Header` | | `--fontSize-1` | 14px | `Text size="medium"` (default): body copy, control text, `Table.Cell`, `Tag size="medium"`, menu items | | `--fontSize-2` | 16px | `Text size="large"`: lead paragraphs; `Heading level={4}`: card and group titles | | `--fontSize-3` | 20px | `Heading level={3}`: section titles, `Table.Title`, `Dialog` title | | `--fontSize-4` | 28px | `Heading level={2}`: major page sections | | `--fontSize-5` | 36px | `Heading level={1}`: the page title, one per page | ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ `Newsroom ` then `Recent stories ` (ranks in order, sizes from the scale) * ✅ `Filters ` (a smaller look without lying about the outline) * ✅ `Every story in one place, from pitch to publish. ` (a lead paragraph on the body scale) * ✅ `Edited by Ada Lovelace ` (caption on the smallest step) * ❌ `Newsroom ` (raw heading, off-scale size, hardcoded family) * ❌ `` (a size between steps and a line height that is not a token) * ❌ `Recent stories ` as a section title (a section title is a `Heading`) * ❌ `Total ` inside every summary card (multiple level-1 headings; card titles are `level={4}`) ## Related * [Heading](/components/heading/) * [Text](/components/text/) * [Content](/foundations/content/) * [Layout](/foundations/layout/) * [Color](/foundations/color/)
# Components
> Every tenet-ui component: status, accessibility review state, and links to API, guidelines and live examples.
41 components ship in `tenet-ui@0.4.0`. Every component imports from `tenet-ui` and needs the single stylesheet import described in [Getting started](/getting-started/). | Component | Status | A11y reviewed | Subcomponents | Stories | | -------------------------------------------------- | ---------- | ------------- | ------------- | ------- | | [Accordion](/components/accordion/) | stable | yes | 1 | 2 | | [Avatar](/components/avatar/) | stable | yes | 0 | 4 | | [Badge](/components/badge/) | stable | yes | 0 | 4 | | [Banner](/components/banner/) | stable | yes | 0 | 5 | | [Breadcrumbs](/components/breadcrumbs/) | stable | yes | 0 | 2 | | [Button](/components/button/) | stable | yes | 0 | 7 | | [Calendar](/components/calendar/) | stable | yes | 0 | 4 | | [Card](/components/card/) | stable | yes | 0 | 2 | | [Checkbox](/components/checkbox/) | stable | yes | 0 | 3 | | [DataTable](/components/data-table/) | stable | yes | 0 | 2 | | [DatePicker](/components/date-picker/) | stable | yes | 0 | 4 | | [Dialog](/components/dialog/) | stable | yes | 0 | 1 | | [Divider](/components/divider/) | stable | yes | 0 | 3 | | [EmptyState](/components/empty-state/) | stable | yes | 0 | 6 | | [FormControl](/components/form-control/) | stable | yes | 3 | 3 | | [Grid](/components/grid/) | stable | yes | 1 | 7 | | [Heading](/components/heading/) | stable | yes | 0 | 7 | | [Icon](/components/icon/) | stable | yes | 0 | 6 | | [IconButton](/components/icon-button/) | stable | yes | 0 | 5 | | [Label](/components/label/) | deprecated | yes | 0 | 3 | | [Link](/components/link/) | stable | yes | 0 | 5 | | [Menu](/components/menu/) | stable | yes | 0 | 1 | | [NumberInput](/components/number-input/) | stable | yes | 0 | 4 | | [Pagination](/components/pagination/) | stable | yes | 0 | 2 | | [Popover](/components/popover/) | stable | yes | 0 | 2 | | [ProgressBar](/components/progress-bar/) | stable | yes | 0 | 4 | | [RadioGroup](/components/radio-group/) | stable | yes | 1 | 2 | | [SegmentedControl](/components/segmented-control/) | stable | yes | 0 | 3 | | [Select](/components/select/) | stable | yes | 0 | 3 | | [Skeleton](/components/skeleton/) | stable | yes | 0 | 5 | | [Spinner](/components/spinner/) | stable | yes | 0 | 3 | | [Stack](/components/stack/) | stable | yes | 1 | 3 | | [Switch](/components/switch/) | stable | yes | 0 | 5 | | [Table](/components/table/) | stable | yes | 9 | 4 | | [Tabs](/components/tabs/) | stable | yes | 3 | 1 | | [Tag](/components/tag/) | stable | yes | 0 | 6 | | [Text](/components/text/) | stable | yes | 0 | 11 | | [Textarea](/components/textarea/) | stable | yes | 0 | 2 | | [TextInput](/components/text-input/) | stable | yes | 0 | 4 | | [Toast](/components/toast/) | stable | yes | 2 | 1 | | [Tooltip](/components/tooltip/) | stable | yes | 0 | 2 |
# Accordion
> When to use: progressively disclosing stacked sections of content (FAQs, settings groups) so the page stays scannable. For switching between peer views use Tabs
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { Accordion } from 'tenet-ui';
```
Source: [src/components/Accordion/Accordion.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/Accordion/Accordion.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** progressively disclosing stacked sections of content (FAQs, settings groups) so the page stays scannable. For switching between peer views use `Tabs`; for a single show/hide, a lone disclosure button is enough. ## Rules [Section titled “Rules”](#rules) * Compose `Accordion.Item`s, each with a unique `value` and a `title`. Each item is a real `` controlling a labelled `region` — keyboard and screen-reader support come for free (Enter/Space toggle; Up/Down/Home/End move between headers). * Use `type="single"` (default) when only one section should be open; `type="multiple"` when several can. Control with `value` + `onValueChange`, or uncontrolled with `defaultValue`. * Put the most important section first; don’t bury critical content behind a collapsed header by default. * Don’t nest accordions deeply — one level keeps it comprehensible. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ `… ` * ❌ A `` that toggles `display` with no `aria-expanded`/region. * ❌ Hiding must-see content (errors, primary actions) inside a collapsed item. ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) ### `Accordion` | Prop | Type | Default | Description | | --------------- | ------------------------------------- | -------- | -------------------------------------------------------------------- | | `type` | `'single' \| 'multiple'`AccordionType | `single` | `single` allows one open item (collapsible); `multiple` allows many. | | `value` | `string[]` | — | Controlled set of open item values. | | `defaultValue` | `string[]` | `[]` | Uncontrolled initial open item values. | | `onValueChange` | `(value: string[]) => void` | — | Called with the new set of open values. | | `children` | `ReactNode` | — | | | `className` | `string` | — | | ### `Accordion.Item` | Prop | Type | Default | Description | | ----------- | ----------- | ------- | ---------------------------------------- | | `value` \* | `string` | — | Unique value identifying this item. | | `title` \* | `ReactNode` | — | Header text shown on the trigger button. | | `disabled` | `boolean` | `false` | Disable this item. | | `children` | `ReactNode` | — | | | `className` | `string` | — | | \* required ## Related components * [Tabs](/components/tabs/) * [Card](/components/card/) * [Stack](/components/stack/) * [Heading](/components/heading/)
# Avatar
> When to use: representing a person or entity (user, org, repo) with an image or initials.
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { Avatar } from 'tenet-ui';
```
Also accepts every prop of `Omit
, 'children'>`. Source: [src/components/Avatar/Avatar.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/Avatar/Avatar.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** representing a person or entity (user, org, repo) with an image or initials. ## Rules [Section titled “Rules”](#rules) * Always pass `name` — it generates the initials **and** the accessible label, so the avatar is never an unlabelled image to a screen reader. The decorative ` ` itself gets empty alt. * Provide `src` when you have a photo; the component falls back to initials automatically if it’s missing or fails to load (no broken-image icon). * Use `shape="circle"` for people and `shape="rounded"` for orgs/repos so the two read differently at a glance. * Pick `size` from the scale rather than overriding width/height, so avatars line up with adjacent controls. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ ` ` * ✅ ` ` * ❌ ` ` — no label, no initials fallback. * ❌ A raw ` ` with no alt strategy and no fallback. ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | --------- | ------------------------------------------------------ | -------- | ------------------------------------------------------------------------------ | | `src` | `string` | — | Image URL. When omitted (or it fails to load), initials from `name` are shown. | | `name` \* | `string` | — | Person/entity name — used for initials and as the accessible label. | | `size` | `'small' \| 'medium' \| 'large' \| 'xlarge'`AvatarSize | `medium` | Size from the scale. | | `shape` | `'circle' \| 'rounded'`AvatarShape | `circle` | Circle (people) or rounded square (orgs/repos). | \* required ## Related components * [Badge](/components/badge/) * [Tag](/components/tag/) * [Tooltip](/components/tooltip/) * [Skeleton](/components/skeleton/)
# Badge
> When to use: a compact count or presence dot attached to something else — unread counts on a nav item, a "new" dot on an avatar. For a labelled status word on a
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { Badge } from 'tenet-ui';
```
Also accepts every prop of `HTMLAttributes`. Source: [src/components/Badge/Badge.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/Badge/Badge.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** a compact count or presence dot attached to something else — unread counts on a nav item, a “new” dot on an avatar. For a labelled status word on a row (passing / failing) use `Tag`; for a contextual message use `Banner`. ## Rules [Section titled “Rules”](#rules) * Use `count` for quantities; it collapses to `max+` (default `99+`) past the threshold — don’t render “1284” in a badge. * Use `dot` for pure presence/attention with no number. * Pick `variant` by meaning. `danger` for things demanding attention (errors, overdue), `accent` for neutral counts. * A badge is decorative-adjacent: if the count conveys meaning not otherwise on screen, make sure the surrounding control has an accessible name that includes it (e.g. `aria-label="Notifications, 3 unread"`). ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ ` ` * ✅ ` ` * ❌ Using a Badge for a status word — that’s `Tag` (a Badge is a count or a dot; a Tag is a word). * ❌ Relying on the badge alone to convey critical info to screen-reader users. **See also:** `Tag` for words; system guideline `data-display` (Badge vs Tag, and the deprecated `Label`). ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | ---------- | --------------------------------------------------------------------------- | --------- | ---------------------------------------------------------------- | | `variant` | `'default' \| 'accent' \| 'success' \| 'danger' \| 'attention'`BadgeVariant | `default` | Semantic color scheme. | | `count` | `number` | — | Numeric count. Rendered with a `max+` overflow when above `max`. | | `max` | `number` | `99` | Overflow threshold for `count` (e.g. 99 -> "99+"). | | `dot` | `boolean` | `false` | Render a bare status dot (no text). | | `children` | `ReactNode` | — | Custom content (overrides count). | ## Related components * [Tag](/components/tag/) * [Avatar](/components/avatar/) * [IconButton](/components/icon-button/) * [Text](/components/text/) * [Label](/components/label/)deprecated
# Banner
> When to use: an inline, contextual message anchored to the content it describes — a form-level error, a page notice, a "you're approaching your limit" warning.
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { Banner } from 'tenet-ui';
```
Also accepts every prop of `Omit, 'title'>`. Source: [src/components/Banner/Banner.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/Banner/Banner.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** an inline, contextual message anchored to the content it describes — a form-level error, a page notice, a “you’re approaching your limit” warning. For transient floating feedback use a toast; for a one-word status on a table row use `Tag`. ## Rules [Section titled “Rules”](#rules) * Pick `variant` by meaning: `info` (clay), `success` (olive), `warning` (ochre), `danger` (brick). Don’t pick by color. * `danger` renders `role="alert"` (interrupts assistive tech); the others render `role="status"` (announced politely). Don’t override the role to make a non-critical message shout. * Lead with a short `title`; keep the body to a sentence or two with a clear next step. * Only add `onDismiss` when the message is non-critical — never let users dismiss a blocking error they still need to fix. * Colors come from the semantic tint tokens — never hardcode a background. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ `3 tests are failing. ` * ✅ `Your changes are live. ` * ❌ A `` hand-rolled alert — off-system, no role, no AA guarantee. * ❌ A dismiss button on a required-fix error. **See also:** `Toast` (transient), `Dialog` (blocking decision), `FormControl.Validation` (one field); system guideline `feedback` has the decision table. ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | ----------- | ----------------------------------------------------------- | ------------- | -------------------------------------------------------- | | `variant` | `'info' \| 'success' \| 'warning' \| 'danger'`BannerVariant | `info` | Semantic tone. Pick by meaning, not color. | | `title` | `ReactNode` | — | Optional bold lead-in line. | | `icon` | `ReactNode` | `per variant` | Replace the default leading icon. | | `hideIcon` | `boolean` | `false` | Hide the leading icon entirely. | | `onDismiss` | `() => void` | — | When provided, renders a dismiss button that calls this. | | `children` | `ReactNode` | — | Body content. | ## Related components * [Toast](/components/toast/) * [Dialog](/components/dialog/) * [FormControl](/components/form-control/) * [EmptyState](/components/empty-state/)
# Breadcrumbs
> When to use: showing where the current page sits in a hierarchy and letting users jump back up it. For switching between peer views use Tabs; for paging through
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { Breadcrumbs } from 'tenet-ui';
```
Also accepts every prop of `Omit
, 'children'>`. Source: [src/components/Breadcrumbs/Breadcrumbs.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/Breadcrumbs/Breadcrumbs.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** showing where the current page sits in a hierarchy and letting users jump back up it. For switching between peer views use `Tabs`; for paging through results use `Pagination`. ## Rules [Section titled “Rules”](#rules) * Pass `items` root-first; the **last** item is the current page and is rendered as text with `aria-current="page"` (not a link) — don’t pass it an `href`. * It renders a `` landmark with an ordered list. Give a distinct `aria-label` if the page has more than one nav. * The separator is decorative (`aria-hidden`); don’t encode meaning in it. * Keep labels short; truncate long middle segments rather than wrapping to many lines. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ ` ` * ❌ Giving the last (current) item an `href`. * ❌ Using breadcrumbs as the primary navigation of a flat site with no hierarchy. ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | ------------ | ------------------ | ------------ | ------------------------------------------------------------------------ | | `items` \* | `BreadcrumbItem[]` | — | Ordered trail, root first. The last item is treated as the current page. | | `aria-label` | `string` | `Breadcrumb` | Accessible name for the nav landmark. | | `separator` | `ReactNode` | `/` | Separator glyph between items. | \* required ## Related types ### `BreadcrumbItem` | Member | Type | Default | Description | | ---------- | ----------- | ------- | ---------------------------------------------------------------- | | `label` \* | `ReactNode` | — | Visible label. | | `href` | `string` | — | Link target. Omit for the current page (rendered as plain text). | ## Related components * [Link](/components/link/) * [Pagination](/components/pagination/) * [Tabs](/components/tabs/) * [Heading](/components/heading/)
# Button
> When to use: a Button triggers an action (submit, save, delete, open). For navigation to another page, use a link, not a Button.
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { Button } from 'tenet-ui';
```
Also accepts every prop of `ButtonHTMLAttributes`. Source: [src/components/Button/Button.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/Button/Button.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** a Button triggers an action (submit, save, delete, open). For navigation to another page, use a link, not a Button. ## Rules [Section titled “Rules”](#rules) * Use exactly **one** `variant="primary"` per surface — the single most important action. * Use `variant="default"` for secondary actions, `variant="invisible"` for low-emphasis / tertiary actions. * Use `variant="danger"` **only** for destructive actions (delete, remove); pair it with a confirmation. * **Never** hardcode colors, padding, or sizes. All colors come from `fgColor` / `bgColor` / `borderColor` tokens; all spacing from the `space` scale; heights from `control.*`. * Always provide a visible text label. Icon-only buttons must use `IconButton` with an `aria-label`; icons come from `tenet-ui/icons` and go in `leadingIcon` / `trailingIcon` (decorative). * Do not nest interactive elements (links, buttons) inside a Button. * Use `fullWidth` to fill the container (inside a `Stack.Item` or `Grid.Item`); `block` is the deprecated 0.3.0 name and warns. * Labels are verbs that name the outcome (“Save changes”, “Delete article”), sentence case, never “Submit” or “OK”. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ `Save changes ` * ❌ `Save ` — raw element, hardcoded color, off-scale padding. * ❌ `` — `kind` is not a prop; the prop is `variant`. **See also:** `IconButton` for icon-only actions, `Link` for navigation; system guidelines `content` (button labels) and `color` (one primary per surface). ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | ----------------- | ---------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `variant` | `'default' \| 'primary' \| 'danger' \| 'invisible'`ButtonVariant | `default` | Visual style of the button. Use `primary` for the single most important action per surface. | | `size` | `'small' \| 'medium' \| 'large'`ButtonSize | `medium` | Control height / padding scale. | | `leadingIcon` | `ReactNode` | — | Icon rendered before the label. | | `trailingIcon` | `ReactNode` | — | Icon rendered after the label. | | `fullWidth` | `boolean` | — | Stretch the button to fill the available width. | | `block`deprecated | `boolean` | — | Stretch the button to fill the available width.**Deprecated since 0.4.0.** Use `fullWidth` instead (renamed in 0.4.0). `block` keeps working until 0.5.0. | | `children` | `ReactNode` | — | Button label. | Deprecated props keep working for one minor release; the replacement is named in the row. ## Related components * [IconButton](/components/icon-button/) * [Link](/components/link/) * [Dialog](/components/dialog/) * [Menu](/components/menu/) * [Icon](/components/icon/) * [EmptyState](/components/empty-state/) * [Popover](/components/popover/) * [SegmentedControl](/components/segmented-control/) * [Spinner](/components/spinner/)
# Calendar
> When to use: an always-visible month grid for picking a day — e.g. inline in a booking flow, or hosted in a popover by DatePicker. For a compact field that open
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { Calendar } from 'tenet-ui';
```
Source: [src/components/Calendar/Calendar.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/Calendar/Calendar.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** an always-visible month grid for picking a day — e.g. inline in a booking flow, or hosted in a popover by `DatePicker`. For a compact field that opens on demand, reach for `DatePicker` (which wraps this). ## Rules [Section titled “Rules”](#rules) * Give it an accessible name via `aria-label` (e.g. “Departure date”). * Control it with `value` + `onChange`, or leave uncontrolled with `defaultValue`. `onChange` hands you a real `Date`. * Constrain selectable days with `min` / `max` — out-of-range days are disabled and the month-nav arrows stop at the boundary. * Set `weekStartsOn={1}` for Monday-first locales. Month and weekday names follow `locale` (defaults to the runtime locale) — don’t hardcode English. * It implements the ARIA grid + roving-tabindex keyboard model (arrows, Home/End, PageUp/PageDown, Enter/Space). **Don’t** rebuild this with a plain table of clickable ``s. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ `
` * ✅ controlled: `
` * ❌ A grid of `
` cells — no keyboard, no roles, no roving focus. * ❌ Formatting the month/weekday labels yourself in English — pass `locale` and let `Intl` do it. ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | --------------- | ----------------------- | ------------------- | -------------------------------------------------------------------------------------- | | `value` | `Date \| null` | — | Controlled selected date (null = nothing selected). | | `defaultValue` | `Date \| null` | — | Uncontrolled initial selected date. | | `onChange` | `(date: Date) => void` | — | Called with the chosen date. | | `month` | `Date` | — | Controlled visible month (any day within the month). | | `defaultMonth` | `Date` | `selected \| today` | Uncontrolled initial visible month. Defaults to the selected date's month, else today. | | `onMonthChange` | `(month: Date) => void` | — | Called when the visible month changes (nav buttons / keyboard paging). | | `min` | `Date` | — | Earliest selectable day (inclusive). | | `max` | `Date` | — | Latest selectable day (inclusive). | | `weekStartsOn` | `WeekStart` | `0` | First day of the week: 0 = Sunday (default), 1 = Monday. | | `locale` | `string` | `runtime locale` | BCP-47 locale for month / weekday formatting. Defaults to the runtime locale. | | `className` | `string` | — | | | `aria-label` | `string` | — | | ## Related components * [DatePicker](/components/date-picker/) * [Popover](/components/popover/) * [FormControl](/components/form-control/)
# Card
> When to use: to group related content on a bordered, rounded surface (a panel, a list container, a pinned item). Card is the design-system "box" — use it instea
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { Card } from 'tenet-ui';
```
Also accepts every prop of `HTMLAttributes
`. Source: [src/components/Card/Card.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/Card/Card.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** to group related content on a bordered, rounded surface (a panel, a list container, a pinned item). `Card` is the design-system “box” — use it instead of a styled `` with hardcoded border, radius, or shadow. ## Rules [Section titled “Rules”](#rules) * Use `variant="default"` for surfaces that sit on the page, `variant="muted"` for recessed/secondary surfaces (e.g. a toolbar strip). * Use `padding` from the `space` scale; use `padding="none"` when children manage their own padding (e.g. a list whose rows are padded individually). * **Never** hardcode `border`, `border-radius`, `box-shadow`, or `background` — they come from `borderColor`, `borderRadius`, `shadow`, and `bgColor` tokens. * For internal layout, compose a `Stack` inside the `Card`; don’t add flex styles to the Card itself. * A `Card` is for a discrete, self-contained object (a summary tile, a settings group with its own actions, a list container). Content that flows with the page is a plain section: `Heading` + `Stack`, no card. Never nest a `Card` in a `Card`, and never wrap a whole page in one. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ `
… ` * ❌ `
` — reinvented surface, hardcoded everything. **See also:** `Stack` and `Grid` for layout inside and around cards; system guideline `layout` states the Card vs plain section test. ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | ---------- | ------------------------------------------------------------ | --------- | ----------------------------------------------------------------------------------- | | `variant` | `'default' \| 'muted'`CardVariant | `default` | Surface style. `default` sits on the page; `muted` is a recessed/secondary surface. | | `padding` | `'none' \| 'condensed' \| 'normal' \| 'spacious'`CardPadding | `normal` | Inner padding, from the `space` scale. | | `children` | `ReactNode` | — | | ## Related components * [Stack](/components/stack/) * [Grid](/components/grid/) * [Heading](/components/heading/) * [Divider](/components/divider/) * [Accordion](/components/accordion/) * [EmptyState](/components/empty-state/) * [Skeleton](/components/skeleton/)
# Checkbox
> When to use: an independent on/off choice (or several non-exclusive ones). Use Checkbox with its label, not a raw
, 'type'>`. Source: [src/components/Checkbox/Checkbox.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/Checkbox/Checkbox.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** an independent on/off choice (or several non-exclusive ones). Use `Checkbox` with its `label`, not a raw `
` or a styled toggle `
`. ## Rules [Section titled “Rules”](#rules) * Always pass a `label` (it is associated to the input automatically); add `caption` for help text. * For a set of mutually exclusive options use `RadioGroup`, not multiple checkboxes. * Control with `checked` + `onChange`, or leave uncontrolled with `defaultChecked`. * The tick color comes from the accent token (`accent-color`) — **never** restyle it with hardcoded colors. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ `
` * ❌ `
` — not focusable, not a real input, no label. ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | --------------- | ----------- | ------- | ------------------------------------------------------------------------------- | | `label` | `ReactNode` | — | Visible label rendered next to the box. | | `caption` | `ReactNode` | — | Optional helper text under the label. | | `invalid` | `boolean` | `false` | Mark the field as invalid (sets `aria-invalid`). | | `indeterminate` | `boolean` | `false` | Tri-state "partially checked" visual (mixed); also sets `aria-checked="mixed"`. | ## Related components * [Switch](/components/switch/) * [RadioGroup](/components/radio-group/) * [FormControl](/components/form-control/)
# DataTable
> When to use: rendering a list of records where users need to search, filter, or sort without you wiring it all by hand. It's built on the Table primitives plus
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { DataTable } from 'tenet-ui';
```
Source: [src/components/DataTable/DataTable.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/DataTable/DataTable.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** rendering a list of records where users need to **search, filter, or sort** without you wiring it all by hand. It’s built on the `Table` primitives plus `TextInput` + `Select`, so it stays on-system. For a bespoke layout (grouped rows, custom toolbars, server-driven paging), drop down to `Table.*` directly. ## Rules [Section titled “Rules”](#rules) * Describe the table with `columns` (id, header, optional `accessor`/`cell`, `align`, `sortable`, `filterable`) + `data`. Don’t hand-roll `
`/` `. * `accessor` returns the **raw** value used for sorting + filtering; `cell` is only for display (e.g. wrapping a status in a `Tag`). Keep them consistent — sort on the value, not the badge. * Turn on `sortable` per column for click-to-sort (asc → desc → off), and `filterable` for a per-column dropdown built from the column’s distinct values. * Provide a `rowKey` when rows can reorder or the list is large — falling back to the index is fine only for static data. * Use numeric `align="end"` for number columns so they line up. * `emptyText` speaks when a search or filter matches no rows and the table stays on screen. When the collection itself has zero records, render an `EmptyState` instead of the table. * This is **client-side** (filters/sort run over the `data` you pass). For huge or server-paged data, drive `Table.*` yourself and fetch on change. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ ` ` * ✅ a status column: `{ id: 'status', header: 'CI', filterable: true, cell: r => {r.status} }` * ❌ Building search/filter/sort state around a raw `Table` when DataTable already does it. * ❌ Sorting/filtering on the rendered `cell` output instead of the underlying value. **See also:** `Table` for hand-composed markup, `EmptyState` for an empty collection, `Pagination` for paged data; system guideline `data-display`. ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | ------------------- | ----------------------------------------------- | ------------------- | ----------------------------------------------------------------------- | | `columns` \* | `DataTableColumn[]` | — | Column definitions, left to right. | | `data` \* | `Row[]` | — | The rows to render. | | `rowKey` | `(row: Row, index: number) => string \| number` | `index` | Stable React key for a row. Defaults to the row index. | | `searchable` | `boolean` | `true` | Show the global search box. | | `searchPlaceholder` | `string` | `Search…` | Placeholder for the global search box. | | `title` | `ReactNode` | — | Optional surface title (rendered in a `Table.Container`). | | `subtitle` | `ReactNode` | — | Optional surface subtitle. | | `actions` | `ReactNode` | — | Actions rendered at the top-right of the toolbar (e.g. a "New" button). | | `density` | `TableDensity` | `normal` | Row height / cell padding scale. | | `emptyText` | `ReactNode` | `No matching rows.` | Message shown when no rows match the active filters. | | `className` | `string` | — | | \* required ## Related types ### `DataTableColumn` | Member | Type | Default | Description | | ------------ | ------------------------- | ------- | ---------------------------------------------------------------------------------- | | `id` \* | `string` | — | Stable key — also used to read the cell value when `accessor` is omitted. | | `header` \* | `ReactNode` | — | Column heading text. | | `accessor` | `(row: Row) => unknown` | — | Read the raw value for a row. Defaults to `row[id]`. Used for sorting + filtering. | | `cell` | `(row: Row) => ReactNode` | — | Custom cell renderer. Defaults to rendering the accessor value as text. | | `align` | `CellAlign` | — | Cell + header alignment. | | `sortable` | `boolean` | — | Allow clicking the header to sort by this column. | | `filterable` | `boolean` | — | Show a per-column dropdown filter built from the distinct values in the data. | ## Related components * [Table](/components/table/) * [EmptyState](/components/empty-state/) * [Pagination](/components/pagination/) * [Tag](/components/tag/) * [Select](/components/select/)
# DatePicker
> When to use: picking a single date from a compact field that opens a calendar on demand (forms, filters). For an always-visible month grid, use Calendar directl
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { DatePicker } from 'tenet-ui';
```
Source: [src/components/DatePicker/DatePicker.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/DatePicker/DatePicker.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** picking a single date from a compact field that opens a calendar on demand (forms, filters). For an always-visible month grid, use `Calendar` directly (DatePicker wraps it). ## Rules [Section titled “Rules”](#rules) * Give it an accessible name via `FormControl.Label` or `aria-label`. * Control it with `value` + `onChange`, or leave uncontrolled with `defaultValue`. `onChange` hands you a real `Date`. * Constrain with `min` / `max`; the popover calendar disables out-of-range days and stops its nav at the boundary. * For form submission pass `name` — it emits a hidden `yyyy-mm-dd` input. Use `size` / `fullWidth` from the scale (`block` is the deprecated 0.3.0 name and warns) and `invalid` for validation state. * Set `weekStartsOn={1}` and/or `locale` for non-US conventions; the displayed date and the calendar both follow them. * **Never** assemble a text input + hand-built dropdown calendar yourself — the popover focus management, the ARIA grid, and the keyboard model are exactly what this composes for you. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ ` ` * ✅ in a form: `Start date ` * ❌ ` ` next to a `` calendar wired by hand — no focus trap-out, no roving grid, no Escape handling. * ❌ Storing the value as a formatted string — keep the `Date`; format only for display. **See also:** `Calendar` (the inline grid it wraps), `FormControl`; system guideline `forms`. ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | ------------------ | ---------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `value` | `Date \| null` | — | Controlled selected date (null = empty). | | `defaultValue` | `Date \| null` | — | Uncontrolled initial selected date. | | `onChange` | `(date: Date) => void` | — | Called with the chosen date. | | `min` | `Date` | — | Earliest selectable day (inclusive). | | `max` | `Date` | — | Latest selectable day (inclusive). | | `weekStartsOn` | `WeekStart` | `0` | First day of the week: 0 = Sunday (default), 1 = Monday. | | `locale` | `string` | `runtime locale` | BCP-47 locale for the displayed date + calendar. Defaults to the runtime locale. | | `placeholder` | `string` | `Select a date…` | Text shown in the trigger when no date is selected. | | `size` | `'small' \| 'medium' \| 'large'`DatePickerSize | `medium` | Control height / font scale. | | `fullWidth` | `boolean` | — | Stretch the trigger to fill the available width. | | `block`deprecated | `boolean` | — | Stretch the trigger to fill the available width.**Deprecated since 0.4.0.** Use `fullWidth` instead (renamed in 0.4.0). `block` keeps working until 0.5.0. | | `invalid` | `boolean` | `false` | Invalid state (danger border + aria-invalid). | | `disabled` | `boolean` | `false` | Disable the control. | | `required` | `boolean` | `false` | Required (sets aria-required; pairs with FormControl). | | `name` | `string` | — | Form field name — emits a hidden input (yyyy-mm-dd) so the value submits with a form. | | `id` | `string` | — | Id applied to the trigger (FormControl sets this automatically). | | `className` | `string` | — | | | `aria-label` | `string` | — | | | `aria-describedby` | `string` | — | | Deprecated props keep working for one minor release; the replacement is named in the row. ## Related components * [Calendar](/components/calendar/) * [FormControl](/components/form-control/) * [TextInput](/components/text-input/) * [Popover](/components/popover/)
# Dialog
> When to use: a focused task or confirmation that must interrupt the flow — destructive confirmations, short forms, critical choices. For non-blocking feedback u
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { Dialog } from 'tenet-ui';
```
Source: [src/components/Dialog/Dialog.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/Dialog/Dialog.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** a focused task or confirmation that must interrupt the flow — destructive confirmations, short forms, critical choices. For non-blocking feedback use a toast or `Banner`; for lightweight hints use a `Tooltip`. ## Rules [Section titled “Rules”](#rules) * It’s a controlled component: drive `open` and handle `onClose` (Escape, backdrop, and the close button all call it). Render your actions in `footer`. * Always pass a `title` — it’s the dialog’s accessible name. Add a `description` for context; it’s wired as `aria-describedby`. * Focus management is automatic: focus moves in on open, is trapped while open, and is restored to the trigger on close, and body scroll is locked. **Don’t** re-implement this with a `position:fixed` div. * For a destructive confirm, keep the confirming button `variant="danger"` and make the consequence explicit in the body. * Keep dialogs short. If it scrolls a lot or has many steps, it probably wants its own page. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ `
Cancel Delete >}>…` * ❌ A hand-built overlay div — no focus trap, no scroll lock, no Escape, no restore. * ❌ Using a dialog for a transient “Saved!” message — that’s a toast. **See also:** `Popover` for non-modal surfaces, `Banner` and `Toast` for non-blocking messages; system guideline `feedback` has the decision table. ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | ----------------- | ------------------------------------------ | -------- | --------------------------------------------------------------------- | | `open` \* | `boolean` | — | Whether the dialog is open. | | `onClose` \* | `() => void` | — | Called when the user requests close (Escape, backdrop, close button). | | `title` \* | `ReactNode` | — | Accessible title — also the visible heading. | | `description` | `ReactNode` | — | Optional supporting description under the title. | | `size` | `'small' \| 'medium' \| 'large'`DialogSize | `medium` | Width from the scale. | | `footer` | `ReactNode` | — | Footer actions (e.g. Cancel / Confirm buttons). | | `closeOnBackdrop` | `boolean` | `true` | Allow closing by clicking the backdrop. | | `children` | `ReactNode` | — | Body content. | | `className` | `string` | — | | \* required ## Related components * [Popover](/components/popover/) * [Button](/components/button/) * [Banner](/components/banner/) * [Toast](/components/toast/)
# Divider
> When to use: a thin rule that separates content groups — sections of a menu, columns in a toolbar, an "or" between two choices.
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { Divider } from 'tenet-ui';
```
Also accepts every prop of `HTMLAttributes
`. Source: [src/components/Divider/Divider.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/Divider/Divider.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** a thin rule that separates content groups — sections of a menu, columns in a toolbar, an “or” between two choices. ## Rules [Section titled “Rules”](#rules) * It renders `role="separator"` with the correct `aria-orientation`, so it’s a real separator, not a decorative border. Use it instead of a styled ` ` or a bordered ``. * Use a labelled horizontal divider (“or”) to split alternative actions; keep the label one or two words. * Vertical dividers need a flex parent with a height to stretch against. * Don’t stack dividers to fake spacing — reach for `Stack` gaps for layout and a divider only when a visible rule communicates a boundary. ## Do / Don’t [Section titled “Do / Don’t”](#do--dont) * ✅ `
` · `
or ` * ✅ `
A B ` * ❌ `
` — off-system thickness/color. ## Examples [Section titled “Examples”](#examples) ## Props [Section titled “Props”](#props) | Prop | Type | Default | Description | | ------------- | ---------------------------------------------- | ------------ | ------------------------------------------ | | `orientation` | `'horizontal' \| 'vertical'`DividerOrientation | `horizontal` | Layout direction. | | `children` | `ReactNode` | — | Optional centered label (horizontal only). | ## Related components * [Stack](/components/stack/) * [Menu](/components/menu/) * [Card](/components/card/)
# EmptyState
> When to use: a table, list, inbox, search or filter that has nothing to show — first run ("No stories yet"), an exhausted filter ("No results for 'budget'"), a
Status: stableAccessibility reviewedtenet-ui\@0.4.0 Import
```tsx
import { EmptyState } from 'tenet-ui';
```
Also accepts every prop of `Omit
, 'title'>`. Source: [src/components/EmptyState/EmptyState.tsx](https://github.com/apurvkhare/tenet-ui/blob/main/src/components/EmptyState/EmptyState.tsx) ## Usage guidelines [Section titled “Usage guidelines”](#usage-guidelines) **When to use:** a table, list, inbox, search or filter that has nothing to show — first run (“No stories yet”), an exhausted filter (“No results for ‘budget’”), a cleared queue (“You’re all caught up”). For zero records, render it *in place of* the rows, inside the same `Card`/`Table.Container`, so the layout doesn’t jump. For an exhausted search or filter, keep the table and its toolbar on screen and pass a compact `EmptyState` (`headingLevel={4}`, no `size="large"`) as `DataTable` `emptyText`. It is not for errors (use `Banner variant="danger"`) or for loading (use `Skeleton`/`Spinner`). ## Rules [Section titled “Rules”](#rules) * `title` is required and specific: name what is empty and, for filters, echo the query. Avoid “Nothing here” / “Oops”. * `description` explains why and what to do next in one or two sentences; keep it inline content (it renders inside a paragraph). * `action` is the single step that fixes the emptiness — a `Button variant="primary"` (“Create story”, “Clear filters”). `secondaryAction` is optional and quieter (`Button variant="invisible"`, a `Link` to docs). Never two primaries; no action at all is fine when there is nothing the user can do. * `icon` is decorative and is wrapped `aria-hidden` — the title already carries the meaning. Pass an `Icon` or an inline `` that uses `currentColor`; it is tinted `fgColor-muted` and sized from the size scale. Don’t put text or a status meaning in it. * `headingLevel` (2–4, default 3) sets the title’s **rank** in the outline so it nests under the surrounding page/section heading; its visual size follows `size`. Use `size="large"` only for whole-page or first-run states. * Don’t hand-build one: a centered `` with inline `textAlign` and a `