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
HomeFoundationsBehaviour

Behaviour

Two choices of kind — how a press is answered and how keyboard focus looks — that a design system sets like any other token.

content_copyView as Markdown

Almost everything that separates one design system from another is an amount: a radius, a tone, a duration. Amounts are values, and a layer changes them.

A few differences are not amounts. One system answers a press with a ripple and another dims the control; one draws a hard ring around the focused control and another a soft glow. No number on an existing token turns one into the other. Tanrei calls these choices of kind and keeps them as two tokens under sys.behavior. Each one names a short, closed list of ways to behave, and a design system picks one.

Design system pressed-feedback focus-style
tanrei overlay ring
cupertino dim ring
m3-expressive-like ripple ring
m3-like ripple ring

Generated from tokens/systems/*.system.json, design/tokens/<system>/tanrei.light.tokens.json

Components read these tokens. They never look at which design system is active.

Pressed feedback

tanrei.sys.behavior.pressed-feedback

How a press is answered. overlay: the pressed tokens apply to the whole container at once. ripple: the pressed state layer grows from the point of contact. dim: the pressed opacity applies to the whole control and no state layer is drawn. none: no visual answer to a press (focus and hover still apply).

— default, and the fallback of a platform that cannot draw a choice

Generated from tokens/sys/behavior.tokens.json

Value What a component draws while pressed
overlay The pressed tokens apply to the whole container at once.
ripple The pressed state layer grows from the point of contact — from the center when the press comes from the keyboard — and is clipped to the container’s shape.
dim The component’s pressed opacity applies to the whole control. No state layer is drawn.
none Nothing. Hover and focus still show.

The choice decides only how the pressed layer appears. Its color and opacity still come from the component’s per-state tokens, which a layer values as before.

Whatever the choice, a pressed state appears without delay and stays visible for at least sys.motion.duration.press-hold (100ms by default, following the motion dial). A tap or a key press is over within a frame; without that floor the answer could not be seen. Button states this as requirements BTN-8 and BTN-9.

What is not a choice: how a corner curves

How a corner eases into the straight edge looks like a choice of kind — circular or continuous — but it is an amount: Figma’s corner smoothing, 0 to 1, where 0.6 is the iOS corner. It lives with the radii it shapes, as sys.shape.corner-smoothing, and is described on the Shape page.

Focus style

tanrei.sys.behavior.focus-style

How keyboard focus is shown. ring: the two-tone focus ring at sys.focus offset and width. glow: the same colors as a soft halo (blurred by the ring width) instead of a hard line. Both must meet the focus contrast invariants.

— default, and the fallback of a platform that cannot draw a choice

Generated from tokens/sys/behavior.tokens.json

glow is drawn with the same two focus colors, offset and width as the ring, so the focus contrast invariants apply to it unchanged. It changes the edge of the indicator, not whether there is one.

The shipped systems

The same Button under each system, drawn by the installed web build. Press each one, then reach it with Tab.

Press, then tab

tanrei overlay · ring

FilledOutlined

cupertino dim · ring

FilledOutlined

m3-expressive-like ripple · ring

FilledOutlined

m3-like ripple · ring

FilledOutlined

Setting a choice in a layer

A choice is changed like any value: under tokens, with a reason.

{
  "id": "acme",
  "extends": "tanrei",
  "tokens": {
    "tanrei.sys.behavior.pressed-feedback": { "$value": 1, "reason": "Acme answers a press with a ripple." },
    "tanrei.sys.behavior.focus-style":      { "$value": 1, "reason": "Acme shows keyboard focus as a glow." }
  }
}

The $value is a number: the position of the name in the token’s list, counted from 0. For pressed-feedback the list is overlay, ripple, dim, none, so 1 is ripple. The format Tanrei’s token files use (DTCG 2025.10) has no string type, which is why the file holds an index. The build refuses an index that is outside the list.

The number stays in the layer file. The generated CSS carries the name (--tanrei-sys-behavior-pressed-feedback: ripple), and SwiftUI, Compose, Flutter and React Native receive a typed enumeration on the system instance.

The lists themselves are part of the core vocabulary. A layer chooses among the names; it cannot add one. A new way of behaving has to be drawn by every platform, so it is proposed against the core like any other vocabulary change.

When a platform cannot draw a choice

The first name in each list is the default, and it is also the fallback. A platform that cannot draw a choice draws the first value instead and says so in its package README.

Note

The index is awkward to write by hand, so Theme Studio (the Behaviour group), tanrei explain and the MCP server show and accept the names.