Skip to content
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
Getting startedWebReactSwiftUIJetpack ComposeFlutterReact NativePresetsYour own design systemUsing Tanrei with AI agentsBrandDesign tokensDesign systems and customizationTheme StudioColorTypographyIconsShapeElevationMotionInteraction statesBehaviourLayoutAccessibilityWordsColor tokensTypescale tokensShape tokensElevation tokensMotion tokensState tokensSpace and measure tokensComponentsButtonMain action buttonSegmented buttonButton groupButton with menuToolbarCopy buttonCheckboxRadio buttonSwitchChipMenuSliderSlider fieldCalendarDateTimePickerField groupRadio groupCheckbox groupSelectSelect fieldMenubarFile uploadFile picker AdvancedDrop zoneText input AdvancedText fieldChip fieldFieldCombobox AdvancedCombobox fieldNumber input AdvancedNumber fieldColor input AdvancedColor fieldOne-time-code input AdvancedOne-time-code fieldEmbedded text input AdvancedChat composerCardDividerModal layer AdvancedDialogListList rowBottom sheetSide sheetCarouselCode blockTablePopoverStepsAccordionDescription listFigureApp shellPage headerTree viewTimelineImage previewHandleResizable frameScroll areaBadgeProgress indicatorMeterNumber tickerLoading indicatorSnackbarTooltipKeyboard shortcutTagInline codeCalloutAvatarEmpty stateTag groupSkeletonFile itemSwatchMarquee textTabsTab viewApp barNavigation barNavigation drawerSearchLinkPaginationPrevious and next linksStepperBreadcrumbsTable of contentsLink cardFAB menuStatusHow Tanrei is builtContributingLicense and attribution
HomeFoundationsDesign systems and customization

Design systems and customization

One vocabulary, many design systems. Change a dial, declare a palette, re-point a token — and every platform follows in one commit.

content_copyView as Markdown

Tanrei is built so that it can reproduce any design system — exactly, not approximately — while its own vocabulary stays small. Two rules make that possible: design systems differ in values first, and what a value cannot express, a layer adds.

What is a core token, and what is not

Tanrei’s core vocabulary is every state that one design system can take inside one product. That gives a test for every proposed token — the one-product test:

  • It is a token if a single product may need the distinction, by meaning or by place. focus-ring is separate from primary because a product may want a saturated brand color and a calm focus ring. That the default system gives them related values is a coincidence, not a reason to merge them. Button has sizes because one product uses several.
  • It is not a token if only one design system, or one unusual product, has it. Material Design 3’s tertiary is a declared palette in the preset that reproduces it, and its primary-fixed roles are extensions of that preset — not slots everyone carries. Twenty button sizes for one exotic product are not core vocabulary either; that product adds them to its own layer.

If some trait of a design system cannot be reproduced even so — a backdrop material, a text-size table of its own — that is a defect in Tanrei, filed in the mimicry-gaps registry (docs/governance/mimicry-gaps.json in the repository).

Dials: one change, everywhere

A design system should feel right by default and follow you when you change one thing. Seven numeric dials sit under everything:

Dial Default Moves
dial.type.base 16px every text size
dial.type.contrast 1 how far headings stand apart from body text
dial.space.unit 8px every distance
dial.size.unit 4px control heights, icon sizes, hit targets
dial.density 1 how tall controls and rows are, and the room around their content: below 1 closer, above 1 roomier
dial.shape.unit 2px every corner
dial.stroke.unit 1px borders, rings, the focus indicator
dial.motion.unit 50ms every duration

Each token is one of three kinds:

  • derived — a small rule, usually a coefficient on a dial: display is type.base × 3, space175 is space.unit × 1.75. Real type ramps are not geometric, so every token has its own coefficient;
  • fixed — a literal with a stated reason, exempt from the dials (corner.full, an easing curve, a legally mandated footnote size);
  • alias — a reference that can be re-pointed (button.container.gap → space100).

On the web the dials also work live: derived tokens are emitted as calc() expressions, so setting --tanrei-dial-type-base on any element rescales everything inside it. A test evaluates every derived token in a real browser under several dial settings and requires it to equal the build’s result.

A design system is a layer

A design system is a small JSON file that extends another. Mostly it changes values; where it must, it adds to the vocabulary.

{
  "id": "acme",
  "extends": "tanrei",
  "palettes": {
    "brand":   { "description": "Acme red is fixed by brand guidelines.", "source": { "seed": "#e60012" }, "roles": "intent" },
    "correct": { "description": "A quiz marks right answers — neither success nor primary.", "source": { "seed": "#0f766e" }, "roles": "intent" }
  },
  "tokens": {
    "tanrei.dial.type.base": { "$value": { "value": 17, "unit": "px" }, "reason": "Our audience reads at arm's length." },
    "tanrei.comp.button.container.padding-inline-start.medium": { "scale": 2 },
    "tanrei.comp.button.container.gap.medium": { "$value": "{tanrei.sys.space.space50}" },
    "tanrei.sys.typescale.label-small": { "$value": { "fontSize": { "value": 11, "unit": "px" } }, "reason": "Legal footnote size is contractual." }
  }
}
You want You write
Everything a bit larger set a dial
Button padding twice the default — and still following the spacing dial "scale": 2
This token to use a different step re-point the alias
This one value to stay put whatever the dials do pin $value with a reason
A brand or domain color with its own on- and -container roles, contrast-checked declare a palette with "roles": "intent"

Because every platform’s code is generated from the same resolved system, one pull request changes the web, SwiftUI, Compose, Flutter and React Native together.

Adding to the vocabulary

Values are not always enough. A reference system has button sizes the core does not; your product has a rating component of its own. So a layer can also extend the vocabulary — under vocabulary, always with a rationale, and only by adding:

"vocabulary": {
  "tanrei.sys.space.space1200":        { "$type": "dimension", "rationale": "Hero sections need a 96px rhythm step.",
                                          "derive": { "fn": "mul", "of": "{tanrei.dial.space.unit}", "by": 12 } },
  "tanrei.comp.button.size.3x":        { "from": "large", "scale": 1.5, "rationale": "A hero call-to-action on the landing screen." },
  "tanrei.comp.button.variant.danger": { "from": "filled", "rationale": "Destructive confirmation is a distinct emphasis here." },
  "acme.comp.rating.star.size":        { "$type": "dimension", "rationale": "The rating component's star.",
                                          "derive": { "fn": "mul", "of": "{tanrei.dial.size.unit}", "by": 5 } }
}
You add What happens
A step in a series — a spacing step, a corner, a text role, a color role with the role drawn on it (onRole) It sits next to its neighbours, follows the same dial, and is contrast-checked like a core pair
A value on a component axis — a Button size or variant One line is enough. A size clones every member of the axis (height, paddings, label style, icon) from another size and scales it, still following the dials. A variant clones its resting colors; the system’s pattern rules value its hovered, focused, pressed and disabled tokens for you
Your own tokens, in your layer’s namespace (acme.…) For your own components. They may reference any tanrei.* token, follow dials and schemes, and become --acme-… on the web and a typed object on every other platform

vocabulary says that something exists; tokens and rules give it a value, exactly as for core tokens — so a child layer changes an extension like anything else. Extensions are inherited, never remove or retype the core, and cannot add interaction states (those belong to the platform). Every generated bundle lists them with who declared them and why.

Components accept what you add. <tanrei-button size="3x" variant="danger"> works on the web, and the native packages take the same keys; a value the active design system does not have renders as the default. A system that adds nothing pays nothing: its generated code does not mention extensions at all.

This is how two ideals hold together: Tanrei’s own vocabulary stays small and plainly named, and any design system can be matched exactly — because nothing has to be added to the core for someone else’s sake.

How a state looks is a value too

Components never compute hover or pressed appearance. Each state has its own tokens (button.filled.hovered.container.color, …), and a system’s pattern rules value them:

{ "match": "tanrei.comp.*.*.hovered.container.color",
  "derive": { "fn": "mix", "base": "{tanrei.comp.$1.$2.container.color}", "with": "{tanrei.comp.$1.$2.label.color}", "amount": "{tanrei.sys.state.hover-state-layer-opacity}" } }

That is Tanrei’s state layer. A system with explicit per-state colors writes aliases instead; one with no hover aliases the state to its resting value; one that dims on press sets pressed.opacity. Same vocabulary, different values.

Choices of kind

A few differences are not amounts at all: a press answered by a ripple or by dimming the control, keyboard focus shown as a ring or as a glow. These are two tokens under sys.behavior (pressed-feedback, focus-style), each with a short closed list of names, and a layer picks one like any other value. How a corner curves is an amount, not a choice: sys.shape.corner-smoothing on Shape. Behaviour describes them. The icon set and the typefaces are a layer’s to change as well: see Icons and Typography.

The systems that ship

The same <tanrei-button>, the same markup. m3-like and cupertino are there to prove the mechanism against two systems with opposite characters, and as starting points for your own layer.

Exactness is measured, not claimed. m3-like carries a fidelity map to the published tokens of material-web (Apache-2.0): every color role, every text style and every Button token of the reference must be equal, covered by another token, or listed with the mimicry gap that explains it. The number is shown with the preset below — equal out of the tokens that are not covered by another one, read from tokens/generated/<id>/fidelity.json. It is called “-like” for trademark reasons — not because it approximates.

tanrei Tanrei

LightFilledTonalElevatedOutlinedTextDisabled
DarkFilledTonalElevatedOutlinedTextDisabled
Dark · High contrastFilledTonalElevatedOutlinedTextDisabled
Declared palettes
accent
Values changed
152
Pattern rules
132
WCAG
Meets every invariant in every scheme. A violation fails the build.

The default design system. Near-monochrome surfaces, a monochrome primary and one accent. It takes its values from core and leaves out the components and variants it does not draw.

cupertino Cupertino (Apple HIG-inspired)

LightFilledTonalElevatedOutlinedTextDisabled
DarkFilledTonalElevatedOutlinedTextDisabled
Dark · High contrastFilledTonalElevatedOutlinedTextDisabled
Extends
core
Declared palettes
None
Values changed
581
Pattern rules
15
Vocabulary extensions
3
Fidelity (measured)
not measured — the reference publishes no machine-readable tokens
WCAG
113 contrast measurements below Tanrei’s thresholds (all at standard contrast). Information only: Tanrei’s accessibility criteria do not apply to an imitation preset, in any contrast scheme.

An independent approximation of the look of Apple's platforms, expressed purely as values: a saturated system tint, pure white and black canvases, gray fills with tinted labels, rounded rectangles, no hover state and a dimmed pressed state.

Tanrei is not affiliated with, endorsed by, or sponsored by Apple Inc. Apple is a trademark of Apple Inc. The values here are Tanrei's own approximations, not a transcription of Apple's documentation, and no Apple font or symbol is bundled: the system font is referenced by name only.

m3-expressive-like M3 Expressive-like (Material 3 Expressive-inspired)

LightFilledTonalElevatedOutlinedTextDisabled
DarkFilledTonalElevatedOutlinedTextDisabled
Dark · High contrastFilledTonalElevatedOutlinedTextDisabled
Extends
m3-like
Declared palettes
None
Values changed
53
Pattern rules
0
Vocabulary extensions
2
Fidelity (measured)
35 / 55 reference tokens equal · 0 different · 20 unmapped, with a registered gap — androidx.compose.material3 button size tokensv0_11_0 (Apache-2.0)
WCAG
Meets every invariant in every scheme. Information only: Tanrei’s accessibility criteria do not apply to an imitation preset, in any contrast scheme.

m3-like with the five button sizes of Material 3 Expressive — extra small to extra large, each with its own paddings, icon size, icon-to-label gap and outline width. Colors, type, shape and states are m3-like's.

Tanrei is not affiliated with, endorsed by, or sponsored by Google. "Material Design" is Google's; this preset is an independent reproduction of published, Apache-2.0 licensed token values.

m3-like M3-like (Material 3-inspired)

LightFilledTonalElevatedOutlinedTextDisabled
DarkFilledTonalElevatedOutlinedTextDisabled
Dark · High contrastFilledTonalElevatedOutlinedTextDisabled
Extends
core
Declared palettes
tertiary
Values changed
711
Pattern rules
19
Vocabulary extensions
18
Fidelity (measured)
273 / 274 reference tokens equal · 0 different · 1 unmapped, with a registered gap — material-webv0_192 (Apache-2.0)
WCAG
Meets every invariant in every scheme. Information only: Tanrei’s accessibility criteria do not apply to an imitation preset, in any contrast scheme.

An independent approximation of Material Design 3's baseline look, expressed purely as values over Tanrei's vocabulary: a chromatic primary, tonal surfaces, pill buttons, shadow elevation without a ring.

Tanrei is not affiliated with, endorsed by, or sponsored by Google. "Material Design" is Google's; this preset is an independent approximation derived from publicly available, Apache-2.0 licensed design tokens.

On the web a preset is one extra stylesheet, scoped to an attribute, so systems can even sit side by side. Its entry registers the components it ships, those the default system leaves out among them (@tanrei/web registers the default system’s alone):

import '@tanrei/web/tokens.css';
import '@tanrei/web/systems/cupertino.css';
import '@tanrei/web/cupertino'; // the components cupertino ships
<body data-tanrei-system="cupertino">

What neither values nor choices of kind can express yet — backdrop materials, uppercase labels as a matter of style — is listed as a defect of Tanrei in docs/governance/mimicry-gaps.json, not hidden.

A component your system does not have

A system can leave out a component, or one value of a component’s variant: the default system has no segmented button, and no filled or outlined text field. A layer says so under components, with the reason and what to use instead:

"components": {
  "omit": { "carousel": { "reason": "Acme shows one thing at a time.", "insteadUse": ["list"] } },
  "variants": { "tabs": { "omit": ["tonal"] } }
}

What is left out still renders and works. In a development build, each element that uses it warns once in the console, naming your system and what it uses instead. The warning names the component you wrote: <tanrei-text-field variant="outlined"> is reported as the text field’s outlined, not as the text input it draws inside. A component that lives in another system’s layer, such as the FAB menu in m3-like, is reported the same way, with the layer it lives in.

To fail your tests on it, have it reported as an error, and ask what was used:

import { setAvailabilityMode, unavailableUses } from '@tanrei/web';

setAvailabilityMode('error'); // in your test setup, before anything renders

test('the product uses only what its system has', async () => {
  // …render the screens of the product…
  expect(unavailableUses()).toEqual([]);
});

unavailableUses() lists each use once, in alphabetical order (segmented-button, text-field:outlined), in either mode. A production build checks nothing, so there the list stays empty. @tanrei/web/availability holds the same two functions without registering any element.

Sharing a system

Building a system produces a self-contained DTCG 2025.10 bundle — plain, fully valued token files with a standard resolver — so any tool that reads design tokens can consume it without Tanrei. The layer files that produced it travel inside the bundle, so Tanrei (or an AI agent) can rebuild or modify it.

Accessibility

Contrast thresholds are constants nobody can change. In the default system a violation fails the build. In a product layer a violation is a warning — printed at build time and written into the generated artifacts, never silent. A preset that imitates another system is held to fidelity to that system instead: Tanrei’s accessibility criteria do not apply to it, in standard, medium or high contrast, and the contrast ratios the build measures there are recorded as information, not as warnings. A functional failure — a control that cannot be operated, a missing name or state — is a defect in every system, an imitation preset included.