Tokens
Source#
tokens/
├─ primitives/
│ ├─ color.json 6 brand ramps + synthesized neutral, 0–1000
│ ├─ dimension.json spacing, radius, border width
│ ├─ typography.json Material-3 scale, Montserrat
│ ├─ motion.json duration + easing (cubicBezier)
│ ├─ opacity.json hidden/disabled/muted/visible scale
│ ├─ interaction.json target size, focus ring, press, drag threshold
│ └─ z-index.json base/raised/docked/focus/sticky/overlay scale
└─ semantic/
├─ color.light.json aliases → semantic contract
├─ color.dark.json synthesized
└─ typography.json composite text styles
All DTCG format ($value / $type), extracted from Figma variables.
Build#
pnpm build:tokens # node style-dictionary/build.mjsStyle Dictionary v4 runs once per theme because two files defining the same
token path would collide in a single run. The light pass emits every platform;
the dark pass emits the web globals.dark.css and the native semantic
theme files that a KinetixTheme needs real dark values for — Theme.dark.kt
(Compose), KinetixColorsSwiftUI.dark.swift (SwiftUI) and
kinetix_color_scheme.dark.dart (Flutter).
Output#
| Platform | Files |
|---|---|
| Web | web/globals.css, web/globals.dark.css, web/tokens.ts |
| iOS | ios/KinetixColors.swift, ios/Theme.swift (UIColor), ios/KinetixType.swift; ios/KinetixColorsSwiftUI.swift + .dark.swift (SwiftUI Color, for packages/ui-swiftui); ios/KinetixMotion.swift |
| Android | android/Color.kt, android/Theme.kt + Theme.dark.kt, android/KinetixType.kt, res/values/{colors,dimens}.xml; android/KinetixMotion.kt |
| Flutter | flutter/app_colors.dart, flutter/app_theme.dart, flutter/app_text.dart; flutter/kinetix_color_scheme.dart + .dark.dart (for packages/ui-flutter); flutter/kinetix_motion.dart |
The KinetixColorsSwiftUI / KinetixColorScheme pairs are additive — the
older Theme.swift / app_theme.dart outputs (light-only, colliding class
names) are untouched. See SwiftUI and Flutter
for why.
Motion + scale#
duration, easing, opacity, and z-index are theme-independent, like
typography — defined once, the same in light and dark. Each platform's output
groups them into KinetixDuration / KinetixEasing / KinetixOpacity /
KinetixZIndex (native — which also get the spatial scale as KinetixSpacing
and KinetixRadius, see Foundations) or --duration-* / --easing-* / --opacity-* /
--z-index-* custom properties (web), and packages/ui's tailwind.config.ts
wires the web ones into real Tailwind utilities — duration-fast,
ease-standard, opacity-disabled, z-overlay — used directly in component
source instead of the ad hoc duration-200 / z-50 literals components used
to carry. A handful of pre-existing values that don't cleanly match a token
step (e.g. a couple of opacity-40/opacity-60 icon dimmings) are left as
literals with a comment, rather than silently changed to the nearest token.
Newly defined, not yet consumed by components: duration.instant (100ms) and the easing.enter,
easing.exit and easing.emphasized curves (standard decelerate / accelerate / expressive), plus the
larger spacing steps (spacing.10–spacing.32, 40–128) and radius.xxl (24). They are part of the contract
and generated for every platform, but no component reads them yet and there are no Tailwind utilities for
the new curves — adopting them is incremental, component by component.
Interaction tokens#
Behavioral values, not visual ones — how big a target has to be, how far a focus ring sits, how a press or a drag reads. Theme-independent, like motion:
| Token | Value | Wired up? |
|---|---|---|
interaction.target.minimum | 44px | Not yet — WCAG 2.5.5 (AAA) / iOS HIG / Material's converged minimum hit-target size. No component in this package currently has an explicit hit-target smaller than 44px that would benefit without a bigger layout change (Checkbox/Radio/Switch's ~18–20px visual controls rely on callers wrapping them in a Label for the larger click area, the same pattern their Radix primitives assume) — defined for app authors and future components to reach for, not retrofitted here. |
interaction.target.default | 40px | Not yet — the default a standalone control should target when nothing else constrains its size. |
interaction.focus.width | 1px | Yes — backs the spread of every shadow.focus* entry's inner layer (tokens/semantic/shadow.json), replacing a hardcoded literal. Compiled CSS output is unchanged (still 0 0 0 1px …). |
interaction.focus.offset | 2px | Not yet as a token reference — matches the dominant ring-offset-2 already used across packages/ui/src/components (4 of 6 current call sites; the other 2 intentionally use ring-offset-1 for tightly-bordered controls). Tailwind's ring-offset-* utilities are static classes, not CSS-var-driven, so wiring this one needs either a custom Tailwind token or a run of literal replacements — left for a follow-up slice. |
interaction.press.opacity | 0.8 | Not yet — Button's own active state is a color shift (active:bg-primary/90), not opacity-based, so there's no current opacity-driven press consumer to point this at. Defined for components that do reach for opacity feedback. |
interaction.drag.threshold | 4 | Yes — KanbanBoard's PointerSensor activationConstraint.distance, via tokens.interaction.drag.threshold imported from @kinetixui/tokens. Matches the value KanbanBoard already shipped tuned at, not dnd-kit's textbook default of 8 — this token records the deliberate existing value rather than silently changing drag feel. |
Web-only in this slice — none of the native platform ports (ui-compose,
ui-swiftui, ui-flutter) consume interaction.* yet.
Semantic contract#
The values every component actually resolves to, in both themes:
| Token | Light | Dark |
|---|---|---|
| --background | #ffffff | #050c11 |
| --foreground | #050c11 | #f0f7ff |
| --cardsynthesized | #ffffff | #0b1821 |
| --popoversynthesized | #ffffff | #0b1821 |
| --primary | #1d4ed8 | #60a5fa |
| --primary-foreground | #f0f7ff | #050c11 |
| --secondary | #c7cfc7 | #2e362e |
| --secondary-foreground | #465245 | #e3e7e3 |
| --muted | #f6f6f6 | #102432 |
| --muted-foreground | #6d6d6d | #92b2c8 |
| --accent | #f0f7ff | #102432 |
| --accent-foreground | #1d4ed8 | #f0f7ff |
| --destructivedarkened for AA | #c60a0a | #dd6a6a |
| --success | #5d6d5c | #90a08f |
| --warningdarkened for AA | #7f5b21 | #ffc975 |
| --border | #92b2c8 | #395a70 |
| --ringsynthesized | #1d4ed8 | #60a5fa |
| --brandidentity — the Figma navy | #1b3c53 | #7495ab |
| --brand-foreground | #f0f7ff | #050c11 |
| --actiondefaults to primary | #1d4ed8 | #60a5fa |
| --action-foregrounddefaults to primary-foreground | #f0f7ff | #050c11 |
| --action-hoveraction @ 90% — native | #3460dc | #5796e3 |
| --action-pressedaction @ 85% — native | #3f69de | #528ed7 |
| --linkdefaults to primary | #1d4ed8 | #60a5fa |
| --focusdefaults to ring | #1d4ed8 | #60a5fa |
Deviations from raw Figma#
The build fixes the Primay typo, rounds letterSpacing float artifacts, and
synthesizes what Figma lacks (--card, --popover, --ring, the neutral ramp,
the dark theme, radius.lg, spacing.7). Each is documented in TOKENS.md
with its rationale.