Skip to content

Foundation

Motion

Motion reveals process — it never decorates. Every animation uses a named duration token and a standard easing curve. The ceiling is 300ms.

01 — Philosophy

Nodus motion follows one rule: if removing the animation breaks understanding, it stays; if removing it changes nothing, it goes.

A pulsing dot on a streaming indicator communicates “the agent is working.” Remove it and the user sees a static dot — the meaning is lost. That animation earns its place. A slide-in on a static card? Decoration. Cut it.

This aligns with Principle 04 — Temporal Honesty: motion should communicate real state changes over time, not create false impressions of activity.

Ceiling
300ms
No UI transition exceeds this
Default
200ms
Panel reveals, content entry
Micro
80ms
State feedback, toggles
Rule
No springs
CSS easing only — no physics

02 — Duration Tokens

Five named durations from imperceptible to deliberate. Click any bar to see the timing in real time.

--ds-motion-instant
alias: --ds-motion-interaction
50msInstant

Micro-feedback, focus rings, checkbox toggles

--ds-motion-interaction
80msSnap

Button press feedback, toggle switches, state dots

--ds-motion-fast
alias: --ds-motion-transition
150msFast

Hover states, tooltips, dropdown reveals

--ds-motion-smooth
alias: --ds-motion-reveal
200msSmooth

Panel reveals, content appearance, sidebar expand

--ds-motion-deliberate
300msDeliberate

Modal opens, major layout shifts, page transitions

--ds-motion-shake
400msShake

Error shake feedback (MFAInput, form validation)

--ds-motion-blink
1sBlink

Cursor-style blink for streaming text indicators

--ds-motion-pulse
2sPulse

Live/active state pulse (StatusDot, StreamingDot, DataFreshness)

--ds-motion-skeleton
1.5sSkeleton

Loading placeholder shimmer animation

03 — Easing Curves

Three easing functions. The default smooth curve handles 95% of use cases — reach for sharp or linear only when justified.

Smooth
--ds-motion-curve
cubic-bezier(0.4, 0, 0.2, 1)

Default for all transitions. Natural deceleration.

Sharp
--ds-easing-sharp
cubic-bezier(0.4, 0, 0.6, 1)

Snappier variant for small interactive elements. Slight overshoot feeling.

Linear
--ds-easing-linear
linear

Progress bars, loading indicators, spinners. Constant velocity.

04 — Keyframe Catalog

Eight named keyframes defined in globals.css. Each has a live demo — click to play.

ds-pulse

Rhythmic opacity pulse for active/streaming indicators.

@keyframes ds-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}
USED BY:StreamingDotStatusDotDataFreshnessTaskQueue
ds-blink

Hard on/off blink for cursor-style indicators.

@keyframes ds-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}
USED BY:StreamingResponse
ds-skeleton

Shimmer effect for loading placeholder content.

@keyframes ds-skeleton {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}
USED BY:Skeleton
ds-fade-in

Simple opacity entrance. The most common entry animation.

@keyframes ds-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
USED BY:General entry
ds-indicator-swap

Subtle flash when a status indicator changes value.

@keyframes ds-indicator-swap {
  from { opacity: 0.6; }
  to { opacity: 1; }
}
USED BY:StatusDotBadge state changes
ds-audit-enter

Slide-in from left for audit trail / log entries.

@keyframes ds-audit-enter {
  from { opacity: 0; transform: translateX(-8px); }
  to { opacity: 1; transform: translateX(0); }
}
USED BY:AuditTrailRetryLedger
ds-demo-slide-in

Wider slide-in for demo showcase elements.

@keyframes ds-demo-slide-in {
  from { opacity: 0; transform: translateX(-12px); }
  to { opacity: 1; transform: translateX(0); }
}
USED BY:Demo registry previews
ds-demo-scale-x

Horizontal scale from zero for progress bars and connectors.

@keyframes ds-demo-scale-x {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}
USED BY:Demo bar charts
ds-node-pulse

Subtle scale+opacity pulse for active graph nodes.

@keyframes ds-node-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.7; transform: scale(1.05); }
}
USED BY:NodePrimitive

05 — Agentic Transition Grammar

Agentic UI introduces three new easing curves tied to the three semantic color roles. Motion communicates what kind of change is happening — not just that something changed. The curve is the message.

Agentic Easing Curves
Productive
--ds-motion-easing-productive
FM: motionEase.productive
cubic-bezier(0.2, 0, 0.38, 0.9)

Data updates, confidence meter fills. Fast start, smooth settle.

Expressive
--ds-motion-easing-expressive
FM: motionEase.expressive
cubic-bezier(0.4, 0.14, 0.3, 1)

Agent state arrivals (idle→active), error escalation slide-in. Slight ease-in, strong deceleration.

Exit
--ds-motion-easing-exit
FM: motionEase.exit
cubic-bezier(0.4, 0.14, 1, 1)

Error dismissal, panel close, agent completion. Accelerates through exit — never lingers.

Agentic Duration Scale
Token
Value
Use
--ds-motion-duration-instant
80ms
Status dot flickers, badge micro-feedback
--ds-motion-duration-fast
150ms
Badge state changes, hover feedback
--ds-motion-duration-default
250ms
Panel expand/collapse, agent card reveals
--ds-motion-duration-slow
400ms
Full page transitions (rare)
Transition Grammar
Agent state arrival (idle → active)
fade + 4px translateY, expressive easing, 250ms
Token: motionTransition.agentArrive
<motion.div
  variants={motionVariants.agentArrive}
  initial="hidden"
  animate="visible"
/>
ANTI-PATTERN:Do not use the generic fadeIn — it uses smooth easing which doesn't communicate agent authority.
Error / escalation appearance
slide-in from top (−8px translateY → 0), expressive easing, 150ms
Token: motionTransition.errorExit
<motion.div
  variants={motionVariants.errorSlideDown}
  initial="hidden"
  animate="visible"
/>
ANTI-PATTERN:Do not slide from bottom — escalation enters from above (authority/system origin).
Confidence meter fill
width transition, productive easing, 250ms — CSS only
Token: --ds-motion-easing-productive
transition: width var(--ds-motion-duration-default) var(--ds-motion-easing-productive);
ANTI-PATTERN:Do not use linear — data fills need deceleration to read as 'settling into a value'.
Thinking state pulse
opacity 0.4→1 loop, 1.2s ease-in-out — CSS @keyframes only
Token: ds-pulse (CSS only — no motion token)
animation: ds-pulse 1.2s ease-in-out infinite;
ANTI-PATTERN:Do not use motion tokens for infinite loops — CSS handles reduced-motion collapse automatically.
Data stream tokens
NO ANIMATION — never animate individual text characters
Token: N/A
// Anti-pattern: do not do this
// text.split("").map((c, i) => <motion.span key={i} ...>)
ANTI-PATTERN:Character-level animation in data tables causes cognitive overload and fails at 60fps with large datasets.

06 — Framer Motion Add-on

The Nodus motion tokens are CSS custom properties. When using Framer Motion, import the adapter from @/lib/motion-tokens — it converts ms → seconds and CSS cubic-bezier() → control-point arrays. DS protocol still applies: no springs for UI transitions, 300ms ceiling.

Installation
import { motion, AnimatePresence } from "framer-motion";
import {
  motionTransition,
  motionVariants,
  motionDuration,
  motionEase,
} from "@/lib/motion-tokens";
Token Bridge
CSS Token
FM Export
FM Value
CSS Value
--ds-motion-instant
motionDuration.instant
0.05s
50ms
--ds-motion-interaction
motionDuration.interaction
0.08s
80ms
--ds-motion-fast
motionDuration.fast
0.15s
150ms
--ds-motion-smooth
motionDuration.smooth
0.2s
200ms
--ds-motion-deliberate
motionDuration.deliberate
0.3s
300ms
--ds-motion-curve
motionEase.smooth
[0.4,0,0.2,1]
cubic-bezier
--ds-easing-sharp
motionEase.sharp
[0.4,0,0.6,1]
cubic-bezier
--ds-easing-linear
motionEase.linear
[0,0,1,1]
linear
Transition Presets
// Transition presets — map 1:1 to DS semantic intent
motionTransition.snap        // 80ms  — button press, focus rings
motionTransition.fast        // 150ms — hover states, tooltips
motionTransition.smooth      // 200ms — panel reveals (default)
motionTransition.deliberate  // 300ms — modal opens, page transitions
motionTransition.snappy      // 150ms — sharp easing, small elements
motionTransition.progress    // 300ms — progress bars, linear

// Usage:
<motion.div transition={motionTransition.smooth} animate={{ opacity: 1 }} />
Variant Demos — click to play
Fade In
Opacity entrance — most common entry pattern.
<motion.div
  variants={motionVariants.fadeIn}
  initial="hidden"
  animate="visible"
/>
Slide Up
Slide 8px up + fade — for panels and cards.
<motion.div
  variants={motionVariants.slideUp}
  initial="hidden"
  animate="visible"
/>
Scale In
Scale from 96% + fade — for dialogs and modals.
<motion.div
  variants={motionVariants.scaleIn}
  initial="hidden"
  animate="visible"
/>
Stagger List
Parent stagger with child items entering in sequence.
<motion.ul variants={motionVariants.stagger} initial="hidden" animate="visible">
  {items.map(i => (
    <motion.li key={i} variants={motionVariants.staggerItem}>{i}</motion.li>
  ))}
</motion.ul>
PROTOCOL

No springs for UI transitions. Springs are only permitted for drag interactions and physical gesture responses. type: "tween" is always the default. motionSpring.dragRelease is the only exported spring preset.

07 — Reduced Motion

Nodus respects prefers-reduced-motion: reduce at the system level. A single rule in globals.css collapses all animation durations to 0.01ms and iteration counts to 1.

This means your components need no extra code. The override is automatic. But you should verify that your component still communicates its state without animation — if the only signal is a pulse, add a static indicator too.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}