Skip to content

shutteringv1.0.0

Conventions

The rules the packages hold to. They are what keep a system this size coherent — and what a contribution is reviewed against.

No hardcoded color

Color drift is the failure this system exists to prevent.

Use the token utility classes — bg-card, text-muted-foreground, bg-primary/15, border-border — never a literal hex. The two sanctioned exceptions are the pre-paint theme script (it runs before CSS loads) and the chart theme bridge (a canvas can't read CSS variables, so it reads them via getComputedStyle, live).

Do: <div className="bg-card text-foreground border-border" />

Don’t: <div style={{ background: "#f9faf9" }} />

One source for motion

Import EASE / SPRING / DURATION from @shuttering/motion; never retype a curve.

import { EASE, DURATION } from '@shuttering/motion'

transition={{ duration: DURATION.base, ease: EASE.out }}

Every animated surface handles reduced motion — three layers back each other up: MotionProvider sets Motion's reducedMotion="user", components branch on useReducedMotion, and theme.css carries a prefers-reduced-motion backstop. Animations never start from scale(0); they begin at 0.96.

Every control is reachable by touch

Target size is a separate claim from visual size.

Interactive controls declare --spacing-tap at pointer: coarse — by growing the control (pointer-coarse:min-h-tap) or, where the paint stays small, by growing the hit area alone, which takes a positioned box and all three of after:absolute, after:-inset-N and after:content-['']. A pseudo-element without content generates no box, so an inset on its own expands a target that was never drawn. A hover-only affordance satisfies nothing either: every hover: utility compiles inside @media (hover: hover), so anything reachable only that way cannot be reached by touch at all.

Animate only what earns it

Causality, continuity, feedback — or nothing.

Before adding motion, name what it carries: causality (the thing came from somewhere), continuity (this is the same object), or feedback (the press registered). Anything else is decoration. Frequency argues against motion rather than for it — a surface crossed dozens of times a session pays the duration every time. If the purpose does not fit in one word, don't build it.

Every control has a focus state

Nothing is focusable without being visible.

Interactive controls render a visible keyboard-focus indicator (focus-visible:ring-2 focus-visible:ring-ring/50, with a ring offset on filled controls). See Agent surfaces and the accessibility baseline for the full commitment.

Strict TypeScript

The tsconfig is strict, and the strictness has consequences worth knowing up front.

  • import type for type-only imports; relative imports carry the .js extension even from .tsx.
  • Optional props are conditionally spread, never set to undefined (exactOptionalPropertyTypes).
  • Indexed access is T | undefined — guard it (noUncheckedIndexedAccess).
  • no-explicit-any is an error; external option shapes are typed.

Component shape

React 19 conventions across the library.

  • No forwardRef, no React.FC; props inline, extending React.ComponentProps<'x'>.
  • Each component carries a data-slot; state is expressed as data-state / data-active attributes and selected in classes, not toggled class names.
  • Styling goes through cn from @shuttering/utils and the token utilities.

Designed & built by stxkxs — React 19, Tailwind v4, and its own @shuttering/* packages.

Agent surfaces are always-on. Proprietary.

© stxkxs