Skip to content
Kinetixui

Foundations

KinetixUI has one set of design foundations — tokens for color, type, space, shape, elevation and motion — defined once in tokens/ and compiled by Style Dictionary to CSS, TypeScript, SwiftUI, Compose and Flutter. The components on every platform read those tokens; they don't restate values.

This page covers the foundations that shape layout. Color and type live on Tokens and Theming; Dark mode, RTL and Accessibility have their own pages.

The 8-unit grid#

KinetixUI uses an 8-unit base spatial grid with an optional 4-unit half-step for compact and fine-grained interface needs.

Space is measured in units, not pixels, because KinetixUI is multi-platform: a unit is a CSS pixel on the web, a point on iOS, a dp on Android and a logical pixel in Flutter. Every value on the scale is a multiple of 4. A multiple of 8 is a base step; any other multiple of 4 is a half-step.

Spacing scale
TokenValueGridSizeNative constant
--spacing-00—space0
--spacing-14Half-step (× 4)space1
--spacing-28Base (× 8)space2
--spacing-312Half-step (× 4)space3
--spacing-416Base (× 8)space4
--spacing-520Half-step (× 4)space5
--spacing-624Base (× 8)space6
--spacing-728Half-step (× 4)space7
--spacing-832Base (× 8)space8
--spacing-1040Base (× 8)space10
--spacing-1248Base (× 8)space12
--spacing-1664Base (× 8)space16
--spacing-2080Base (× 8)space20
--spacing-2496Base (× 8)space24
--spacing-32128Base (× 8)space32

The token number is the value divided by 4 (--spacing-4 is 16), the same numbering Tailwind uses, so a utility like p-4 and the token --spacing-4 agree. Steps 1–8 are for component internals — padding, gaps, control sizing. Steps 10–32 (40–128) are the layout scale: section spacing, containers, page gutters.

What a unit means on each platform
PlatformUnitHow
Web (React)px / rem--spacing-n are px; Tailwind steps are 0.25rem (4px at the default root size).
SwiftUIpointsKinetixSpacing.spaceN (Double, points). Text follows Dynamic Type, not the grid.
Jetpack ComposedpKinetixSpacing.spaceN (Float — use .dp) and spacing_n in dimens.xml. Text uses sp.
Flutterlogical pixelsKinetixSpacing.spaceN (double). Text follows the platform text scaler.

What the grid governs — and what it doesn't#

The grid governs spacing, padding, margins, gaps, layout, control sizing and touch layout. It does not govern:

  • Typography. Type is sized for readability, hierarchy and each platform's text scaling (Dynamic Type, Android font scale, Flutter's text scaler), not for a multiple of 8. See Tokens.
  • Hairlines and optical nudges. A 1px border or a 2px offset isn't a spacing decision.
  • The .5 steps. Tailwind's 0.5, 1.5, 2.5 and 3.5 (2, 6, 10 and 14px) are the optical gap between an icon and its label and similar fine alignment. They are used deliberately and are not policed.

Keeping it on the grid#

pnpm check:grid runs in CI. It checks that every spacing token is a multiple of 4, and that the component library does not gain a new arbitrary pixel value (p-[13px], gap-[22px]) that is off the grid. Three exist today, all 18px — the checkbox and radio boxes and the button icon — because the design source specifies them; they are listed by name in the script, with the reason. It is a guardrail against new drift, not a rewrite: the components already sit on the grid the large majority of the time.

Radius#

Radius follows the same philosophy: multiples of 4, with full for pills and circles. Components take their radius from these tokens rather than inventing one.

Radius scale
TokenValueNative constantUse
--radius-none0KinetixRadius.none
--radius-sm4KinetixRadius.sm
--radius-md8KinetixRadius.md
--radius-lg12KinetixRadius.lgSYNTHESIZED
--radius-xl16KinetixRadius.xl
--radius-xxl24KinetixRadius.xxlLarge surfaces — sheets, hero cards. The 8-unit-grid step above xl.
--radius-full9999 (pill)KinetixRadius.full

Role aliases#

Sizes tell you how round; roles tell you what is round. A role alias is a token that points at a size step, so a theme can reshape one kind of thing — every field, say, or every card — without disturbing the rest:

Radius role aliases
Role tokenResolves toValueNative constantUsed by
--radius-field--radius-sm4KinetixRadius.fieldtext-entry and small selection surfaces: Input, Textarea, Select, Checkbox, Tag, Kbd. A theme can round or square every field by overriding this one token.
--radius-control--radius-md8KinetixRadius.controlpressable controls and menus: Button, Toggle, ButtonGroup, dropdown / context / menubar menus, Select popup, Popover.
--radius-container--radius-lg12KinetixRadius.containerinline containers: Alert, Tabs list, SegmentedControl, Sidebar, Tour.
--radius-surface--radius-xl16KinetixRadius.surfacelarge elevated surfaces: Card, Dialog, AlertDialog, Modal.

The mapping is measured from how the components use radius today (every component named above uses a class of that size). On the web each alias is a live reference — --radius-control: var(--radius-md) — so overriding a step carries through to its roles, and the Tailwind preset has matching rounded-field, rounded-control, rounded-container and rounded-surface utilities. On SwiftUI, Compose and Flutter the same names are on KinetixRadius.

The aliases are additive: components still use the size steps, and move to the roles one at a time. A component can use more than one size (a menu's container is control, its items are field-sized), so a role names the component's outer shape.

Why not rename the sizes? The public names sm (4) and md (8) predate the grid and are used across the library and consuming apps. Renaming to xs 4 / sm 8 / md 12 / lg 16 / xl 24 would shift every name by one step — a breaking visual change on every platform. Roles give the same benefit without breaking anything. The one addition to the steps is xxl (24), the missing step on the grid. (Tailwind's rounded-2xl is a separate, older value — 20px — and is not xxl.)

Elevation#

Elevation is the shadow that separates a surface from what's behind it. The table shows the levels as the components use them today, measured from the source. The level names are documentation for now — the values themselves are the --shadow-* tokens — and each platform draws shadow with its own mechanism, so values need not be identical everywhere.

Elevation as used by the components
LevelTokenUsed by
Surface—Page and inline surfaces — no shadow.
Raised--shadow-smCard, SegmentedControl, Toggle, InputOTP, KanbanBoard.
Popover--shadow-mdPopover, DropdownMenu, ContextMenu, Menubar, Select, HoverCard, NavigationMenu.
Modal--shadow-lgDialog, AlertDialog, Sheet, Sonner (toast), Fab, Tour.
(unused tier)--shadow-xlOnly the Chart tooltip. Kept for larger surfaces.

Motion#

Two duration and easing scales, shared by every platform. Native ports expose them as KinetixDuration and KinetixEasing.

Motion durations
TokenDurationUse
--duration-instant100msNear-immediate feedback — press states, toggles. Below this a change reads as a jump.
--duration-fast200msQuick UI transitions — icon rotation, chevrons, sidebar collapse.
--duration-base300msDefault transition speed — fades, stroke/progress animations.
--duration-slow500msEntrance transitions for larger surfaces — sheet/drawer open.
--duration-slower1000msLooping animations — caret blink.
Motion easings
TokenCurveUse
--easing-linearcubic-bezier(0, 0, 1, 1)Constant-speed motion — panel slides (Sidebar).
--easing-standardcubic-bezier(0.4, 0, 0.2, 1)Default ease-in-out — dialogs, sheets, overlays.
--easing-entercubic-bezier(0, 0, 0.2, 1)Decelerate — an element entering the screen or a surface opening.
--easing-exitcubic-bezier(0.4, 0, 1, 1)Accelerate — an element leaving the screen or a surface closing.
--easing-emphasizedcubic-bezier(0.2, 0, 0, 1)Expressive — large or attention-drawing transitions. Use sparingly.

What each tier is for. instant is press and toggle feedback — the tier a control reaches for when it has to answer the pointer. fast is a small state change: a chevron, a tab, a menu opening. base is a fade or a progress stroke. slow is a large surface arriving, and slower exists for looping animation, which is almost always the wrong answer. The easings carry direction rather than taste: enter decelerates because a surface that is arriving should settle, exit accelerates because one that is leaving should get out of the way, and emphasized is reserved for the rare transition that should be noticed.

When not to use motion. Nothing decorative that runs on its own, nothing that delays an interaction, nothing that animates layout unless moving is the point (Tour's spotlight travelling between targets is the exception that proves it). Prefer opacity and transform; naming the properties that actually change beats transition-all, which quietly animates layout too.

Core is quieter than IoT, deliberately. A button or a menu should feel immediate and then get out of the way. @kinetixui/iot uses motion to carry state that genuinely has stages — requested, pending, confirmed — and that is a different job, documented in IoT.

Reduced motion. @kinetixui/ui ships no CSS of its own, so the guarantee lives in the Tailwind preset consumers already extend: under prefers-reduced-motion: reduce it shortens every duration to 0.01ms rather than removing motion. That distinction is the safety property. A shortened transition still arrives at the same end state, so a checked box is still checked, a switch thumb is still moved and an IoT control still reads confirmed rather than requested — the interpolation goes, the information does not. It is also why the rule is not animation: none: Radix unmounts an overlay when its exit animation fires animationend, and an animation that never runs never fires it, which would strand the overlay in the tree for exactly the people who asked for less motion. Components that need more than this floor still say so at their own call site with motion-reduce:, as the IoT package does where a pulse carries meaning.

Native platforms have their own Reduce Motion settings; honoring them in the native components has not been audited yet, and this pass did not change any native implementation.

On every platform#

The spatial and shape scales reach each platform from the same source:

PlatformWhere the tokens live
WebCSS custom properties (--spacing-n, --radius-name) in globals.css; typed in @kinetixui/tokens
SwiftUIKinetixSpacing, KinetixRadius, KinetixDuration, KinetixEasing (generated KinetixMotion.swift)
Jetpack Composethe same enums in KinetixMotion.kt, plus spacing_n / radius_name in dimens.xml
Flutterthe same classes in kinetix_motion.dart

Existing native components still contain literal numbers (with a // spacing/4 comment); moving them onto the generated constants is incremental work, done component by component so nothing shifts visually.

See Supported platforms for what each platform covers today.