Skip to content
nexubDesign System
v2.4.0

Foundations

Motion & interaction

Motion is springs, not durations (Apple’s fluid interfaces, Material 3’s motion scheme). The same nine springs drive the web (CSS linear() easings) and React Native (Reanimated), so a sheet or a toast moves the same on every platform. Transitions stay interruptible: a new target retargets from where the element is.

The reduced-motion preview applies the CSS fallbacks (no travel or scale; crossfades stay). Components that check prefers-reduced-motion in JavaScript follow your OS setting.

Springs

Three families, three speeds. Curves are drawn from the shipped tokens; press a track to run one.

spatial

317 ms

effects

231 ms

expressive

435 ms

Spring families
FamilyUseFast / default / slow
spatialPosition and size: panels, sheets, indicators. Damping 0.9, a hair of settle.224 / 317 / 484 ms
effectsOpacity and colour. Critically damped: never overshoots.150 / 231 / 327 ms
expressiveKey moments only: toggles, success, drag release. Damping 0.7–0.8.364 / 435 / 600 ms
transition: transform var(--motion-spring-spatial-default-duration)
                      var(--motion-spring-spatial-default-easing);
/* Tailwind: duration-spatial-default ease-spatial-default */

Every motion in @nexub/react, live. Replay, slow it down, or preview the reduced-motion fallback with the bar above.

Dialog

nx-motion nx-dialog · effects + spatial

Exit is faster, never overshoots

Popover

nx-motion nx-pop · grows from its trigger

Drawer

nx-drawer · spatial spring from its edge

Stagger

nx-stagger · 40 ms apart

Re-key the parent to replay

  • Payment #1

  • Payment #2

  • Payment #3

List add and remove

ListTransition · rows grow in, collapse out

Skeleton to content

SkeletonSwap · crossfade

$12,480.00

Number ticker

NumberTicker · digits roll on the spatial spring

Sliding indicator

SegmentedControl · spatial spring

Press and lift

nx-press 0.97 · nx-lift 2px

Hover and press

Collapse animates height on the spatial spring.

Fade · Slide · Scale · Collapse

Motion primitives · on springs

On iOS & Android

@nexub/react-native uses the same springs through Reanimated. Toasts, banners, list rows, chips, the segmented control, sheets and press feedback already move; use the presets for your own views.

import Animated, { withSpring } from 'react-native-reanimated';
import { nxEnter, nxExit, nxLayout, nxSpring } from '@nexub/react-native';

// Enter and exit on the effects + spatial springs; off under Reduce Motion.
<Animated.View entering={nxEnter('up')} exiting={nxExit('down')} layout={nxLayout()} />

// Lists: rows glide into place when others are added or removed.
<Animated.FlatList itemLayoutAnimation={nxLayout()} … />

// Your own value, on a token spring.
offset.value = withSpring(target, nxSpring('spatial', 'fast'));

// Money: the new figure rises in when the value changes.
<Amount value={balance} currency="SGD" size="display" animate />
Native motion presets
PresetKindsUse
nxEnter(kind, speed)'fade' | 'up' | 'down' | 'scale'Views appearing: toasts, banners, new rows.
nxExit(kind, speed, rtl)'fade' | 'up' | 'down' | 'scale' | 'start' | 'end'Views leaving; exits are faster and never overshoot.
nxLayout(speed)—Siblings gliding into place after a change.
nxSpring(family, speed)spatial | effects | expressivewithSpring on your own shared values.

Interactive preview of @nexub/react-native rendered for the web. Native chrome (switches, pickers, Liquid Glass) looks like the platform on a device.

State layers

Hover, focus and press are an overlay of the control’s own text colour at a fixed opacity, so they read correctly on any fill, theme, brand or glass: --state-hover 0.08, --state-focus 0.10, --state-pressed 0.12, --state-dragged 0.16. Add nx-state-layer to any custom control. Pressables also scale to 0.97 on a fast spring (nx-press). There is no ripple. On mobile the pressed layer and the press spring come with a semantic haptic instead of hover.

Enter and exit

Overlays, menus, tooltips and toasts share one set of keyframes. Add nx-motion and the element fades in on the effects spring while it moves and scales in on the spatial spring. It runs on Radix’s data-state (open / closed), or on data-motion="from-…" / "to-…" for anything else. Exits are faster and never overshoot. nx-pop scales floating layers from 0.96 with a 4px nudge away from the side they open on; nx-dialog scales from 0.96 and rises 8px; nx-drawer slides from its edge.

<div className="nx-motion nx-pop" data-state={open ? 'open' : 'closed'} data-side="bottom">…</div>

Lists, stagger and loading

ListTransition grows new items in and collapses removed ones in place (space items with padding, not gap). nx-stagger brings children in 40ms apart; re-key the parent to replay. SkeletonSwap crossfades a skeleton into the content, so loaded data never pops.

<ListTransition as="ul" itemAs="li">{items.map((i) => <Row key={i.id} />)}</ListTransition>

<div className="nx-stagger" key={run}>…</div>

<SkeletonSwap loading={isLoading} skeleton={<Skeleton shape="rect" height={120} />}>{content}</SkeletonSwap>

Cards

Card variant="interactive" lifts 2px on hover on the spatial spring and moves from elevation 1 to 2; leaving settles faster on the effects spring. Press sinks it to 0.99. The lift only applies where a real pointer hovers, so touch never gets stuck in the hover state. effect="sheen" is opt-in: a soft light follows the pointer. Keep it to one or two hero cards per page.

Page transitions

Opt in with one import. The old page fades out fast; the new one fades in and rises 8px on the spatial spring. Single-page apps wrap the route update in startViewTransition; where the browser has no View Transitions API it just runs. On mobile the native stack does the push and the back gesture.

import '@nexub/themes/view-transitions'; // after the base theme
import { startViewTransition } from '@nexub/react';

startViewTransition(() => navigate('/settings'));

Scroll

AppHeader condense shrinks the header from 64 to 52px and lifts it onto elevation 1 over the first 64px of page scroll, with a CSS scroll timeline and no JavaScript. On iOS the native large title does the same.

Focus and touch targets

One focus ring for everything: a 3px outline in --color-ring-default, offset 2px, following border-radius and using Highlight in forced-colours mode. On coarse pointers small controls get an invisible 44×44px hit area (nx-hit) without changing their size; native controls always reach 44pt.

Reduced motion

Under Reduce Motion
  • Transitions and animations become instant changes.

  • Overlays, revealed content and list items keep a short crossfade with no travel or scale, so nothing pops in.

  • Native presets, the press spring and sheets follow the OS setting automatically.

Never
  • Rely on motion alone to convey meaning.

  • Animate backdrop-filter, or use transition-all.

  • Loop motion on content people read (shimmer and morph are for loading only).