- Blur Reveal
- Staggered Fade Up
- Masked Slide Reveal
- Tracking In
- Inline Highlight
- Marker Highlight
- Shimmer Sweep
- Typewriter
- Text Fade Replace
- Slot Machine Roll
- Infinite Marquee
- Perspective Marquee
- Matrix Decode
- RGB Glitch Text
- Strikethrough Replace
- Blur Out Up
- Bottom Up Letters
- Top Down Letters
- Per Character Rise
- Soft Blur In
- Micro Scale Fade
- Scale Down Fade
- Focus Blur Resolve
- Spring Scale In
- Line by Line Slide
- Mask Reveal Up
- Short Slide Right
State atoms
State atoms (Button, Input, Checkbox, Switch) are pure visual functions of (state | style, theme). They read no clock internally and snap instantly between named states when you pass state. Pass from plus duration to animate between states via CSS keyframes generated by the transition hook.
Purity boundary: framecn UI components never call browser APIs or hold internal React state for interaction. Timeline resolution happens in transition hooks (use-button-transition.ts, etc.) or via explicit state / from props on the component.
Motion atoms
Motion atoms (Spinner, Caret, TypingIndicator) loop on the timeline with no discrete states. They are safe in Editframe's headless renderer — no Date, no Math.random, no requestAnimationFrame.
Transition hooks
Each state atom ships a transition file (e.g. use-button-transition.ts) copied into your project alongside the component. That file:
- owns default duration and easing
- exports keyframe helpers and style presets per state
- resolves
from→stateanimation timing for theTimegroup
To get smooth motion, pass from and state with a duration:
import { Button } from "@/components/framecn/button";
export function Scene() {
return (
<Button from="idle" state="loading" duration="12frames" label="Continue" />
);
}To tune timing globally, edit DEFAULT_DURATION in your copied use-button-transition.ts. To tune easing, edit the easing call in the same file.
States snap by default
Without from, state atoms snap instantly between named states. Snap is the default path: pass state="hover" and the atom jumps straight to that visual.
Theming
The UI tier uses a JS theme object with stock shadcn token names. Values are concrete oklch strings — not CSS custom properties.
import type { FramecnTheme } from "@/lib/framecn-ui";
const myTheme: Partial<FramecnTheme> = {
primary: "oklch(0.55 0.2 250)",
radius: 6,
};FramecnUIProvider
Wrap your composition in FramecnUIProvider to set a theme and/or mode for all nested UI primitives:
import { FramecnUIProvider } from "@/lib/framecn-ui";
export function Scene() {
return (
<FramecnUIProvider mode="dark" theme={{ primary: "oklch(0.6 0.22 260)" }}>
<Button state="idle" label="Continue" />
</FramecnUIProvider>
);
}Token resolution order (highest wins): per-component theme prop → FramecnUIProvider theme → defaultLightTheme / defaultDarkTheme per mode.
Available exports from @/lib/framecn-ui:
| Export | Description |
|---|---|
FramecnUIProvider | Context provider for theme and mode |
useFramecnTheme | Resolves the active theme inside a component |
defaultLightTheme | Stock shadcn neutral light palette |
defaultDarkTheme | Stock shadcn neutral dark palette |
FramecnTheme | Full token shape with radius: number |
Animated colors require concrete JS values
CSS custom properties (var(--primary)) work for static styling only. Editframe's per-frame renderer cannot resolve var(...) for JS color interpolation. Pass concrete oklch, hex, or rgb values via the theme prop or FramecnUIProvider for animated tokens.
Core library exports
Installing any UI component also installs lib/framecn-ui/ with these modules:
| Module | Key exports |
|---|---|
timeline.ts | framesFor, revealCount, revealedText, useTypewriter |
theme.ts | FramecnUIProvider, useFramecnTheme, defaultLightTheme, defaultDarkTheme |
color.ts | mixOklch, parseColor, oklchToRgb, rgbToOklch, toCss |
motion.ts | easings, springs |
types.ts | Step |
You can import from @/lib/framecn-ui (the barrel) or from the individual module paths.
Blocks
Blocks are multi-step UI flows (ChatFlow, SignupFlow, CheckoutFlow) composed from state atoms. They orchestrate several primitives in sequence on a shared timeline — install them the same way as components via @framecn/<name>.