zerogravity
v0.1.3
Published
Motion-first React components: pointer effects, scroll interactions, animated borders, display typography and illustrated scenes.
Maintainers
Readme
ZeroGravity UI
Motion-first React components for expressive interfaces.
ZeroGravity UI is a set of animated, interactive React components: pointer
effects, scroll-driven layout, display typography, animated borders, illustrated
scenes and route transitions. Every component ships its own styles, stops
animating when it is idle, and honours prefers-reduced-motion.
Published on npm as zerogravity.
Installation
pnpm add zerogravitynpm install zerogravityUsage
import { Reel } from "zerogravity"
export function Carousel() {
return (
<Reel radius={20}>
<article>One</article>
<article>Two</article>
<article>Three</article>
</Reel>
)
}Direct component imports
Components can be imported from the root package or through an explicit per-component subpath:
import { Reel } from "zerogravity"
import { Reel } from "zerogravity/reel"Both resolve to the same module. The package preserves one file per module and marks its stylesheets as side effects, so bundlers can drop the components you do not use — the repository verifies this on every run by building a consumer app that imports a single component and asserting the others are absent.
The subpath is the directory name of the component, lowercased and hyphenated:
zerogravity/grid-trail, zerogravity/split-flap, zerogravity/trailing-cursor.
Nothing else is importable — engines, geometry helpers and internal modules have
no entry point.
Styles
There is no global stylesheet to import. Each component imports its own CSS, and
sideEffects in package.json marks those files so bundlers keep them while
still tree-shaking unused components.
Colours meant to be themed are exposed as CSS custom properties on the component root, so you override them with ordinary CSS:
.my-carousel {
--reel-accent: oklch(0.8 0.15 200);
}Next.js App Router
Interactive components keep their "use client" boundaries in the published
build, so you can render them directly from a Server Component without marking
your own page as a client component. This is verified against a real Next.js 15
App Router production build, including static prerendering.
import { Reel } from "zerogravity"
export default function Page() {
return (
<Reel radius={20}>
<article>One</article>
</Reel>
)
}Components
| Component | Description | | --------------------------------------------------- | ------------------------------------------------------------------------------ | | Antigravity | A particle field that flows around the cursor and settles into formations | | Aperture | A full-bleed panel that closes into a framed card as you scroll, or opens | | Diorama | Depth layers that part as the pointer moves, letting you see past the front | | Elemental | An animated edge that wraps any content in electricity or fire | | GridTrail | A pointer trail that lights cells on an invisible grid, then stops the loop | | Kern | Glyphs that open up, lift and gain weight as the pointer passes them | | Lodestone | Magnetic buttons that lean toward the pointer but never overlap each other | | Meadow | A living pastel hero scene that drifts and flutters around your content | | Overprint | Colour separations that misregister on scroll and converge back into register | | Reel | A roulette-style carousel you can drag, flick, scroll sideways or step | | Ricochet | Destructible pixel text with breakout or shooter play | | ScrollStack | Sections that slide over each other on scroll, and unstack back | | SplitFlap | An airport board that flips one character at a time: text, clock or countdown | | Stencil | Display type filled with stripes, checks, gradients, an image or video | | Tessera | A tiled route transition: tiles cover the viewport, the route swaps, they lift | | TrailingCursor | A dot pinned to the pointer and a ring that lags, grows and recolours | | Vellum | A sheet that leans toward the pointer, with an optional dent and sheen | | pointer-fx | Shared colour resolution and the reduced-motion / pointer-type gate |
Each component README carries the full prop table, accessibility notes and performance characteristics.
A few prototypes — Facet, Louvre, Raster, Wash and the pixel loaders — live in
src/lib/experimental and have documentation pages, but they are not part of
the published package and cannot be imported from zerogravity.
Requirements
React and React DOM are peer dependencies:
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"The package has no runtime dependencies of its own.
It is ESM only and needs a bundler that understands CSS imports — Vite, Next.js, Rspack, webpack 5 and Parcel all qualify. Importing it from plain Node without a bundler will fail on the stylesheet imports.
The engines field in package.json (Node 22, pnpm 10) applies to developing
this repository. It is not a requirement for applications that install the
package.
Components degrade instead of throwing when a browser API is missing:
ResizeObserver and IntersectionObserver are feature-detected, and Antigravity
skips its engine when a 2D canvas context is unavailable. Server rendering and
jsdom-based test suites therefore need no polyfills.
Naming
ZeroGravity UI is the project. The npm package is published as
zerogravity, and the repository
is zerogravity-ui.
Links
Versioning
This project follows Semantic Versioning, with the pre-1.0 caveat that the public API is still settling:
- Patch — bug fixes, internal changes, documentation.
- Minor — new components, new props, new presets. Anything additive.
- Major — removing or renaming a prop or export, changing a default that alters rendered output, dropping a React version, changing the CSS strategy.
While the version is 0.x, breaking changes ship in minor releases and are
listed under a Changed or Removed heading in CHANGELOG.md.
The public API is exactly what src/lib/index.ts exports. The exports map
blocks deep imports, so engines, geometry, math helpers and src/lib/internal
are not reachable from the package and can change in a patch release.
Development
corepack enable
pnpm install
pnpm dev # docs site at http://localhost:3000The project uses pnpm and pins the version in packageManager.
Docs site
/is a short landing page,/docslists every component, and each component has its own page at/docs/<slug>.- The sidebar groups components by category and filters as you type. Ctrl/⌘ + K jumps to the search field.
- Each page is preview, customize, usage, props and dependencies, all generated
from one registry entry in
src/docs/registry.ts. - Controls are generated from a schema, and every row is labelled with the real
prop path (
pulse.waveform,formation.radius). - The usage snippet contains only the props that differ from the defaults, and updates as you move the controls. It is read-only, with a copy button.
Anything changed by hand becomes a sticky override. Switching presets applies the preset underneath those edits, so tweaks survive until Reset:
config = defaults → preset → your edits (win)Layout
src/
├── lib/ the published library
│ ├── internal/ shared helpers, never exported publicly
│ ├── pointer-fx/ colour resolution + the pointer/motion gate
│ ├── antigravity/
│ ├── aperture/
│ ├── grid-trail/
│ ├── reel/
│ ├── scroll-stack/
│ ├── split-flap/
│ ├── stencil/
│ ├── trailing-cursor/
│ └── index.ts the public entry point
│
├── docs/ the documentation site: registry, shell, search
│ ├── registry.ts one entry per component, the source of truth
│ └── components/ header, sidebar, preview, code, props table
│
├── playground/ demo sources, development only
│ ├── panel/ schema-driven controls
│ ├── previews/ one live preview per component
│ └── <component>/schema.ts defaults, controls and presets
│
├── app/ Next.js App Router, hosts the docs site
└── test/ test harnesses (rAF, canvas, media queries)Each component owns its folder: the component, its types, its CSS, a README.md
and an index.ts that defines its public surface.
src/docsandsrc/playgroundmay import fromsrc/lib. The reverse never happens.src/libimports React and browser APIs, never Next.js. An ESLint rule fails the build if either boundary is crossed.- Cross-component imports inside the library go to the specific module, not to another component's barrel, so barrels stay a description of the public API.
Conventions
- Engine separate from the React wrapper for canvas components. The component mounts a canvas and pushes config objects into a plain class, so changing a prop never restarts the animation.
- Props only. No global theme, no context, no CSS framework.
- Colours are never parsed by hand. Canvas takes the raw string and
transparency comes from
globalAlpha, which supportsoklch()and everything else for free. Onlyvar(--token)is resolved, viagetComputedStyle. - Every animation loop stops when idle and restarts on demand.
- Documentation lives in READMEs. Source stays comment-free.
Scripts
| | |
| ----------------------------------- | ------------------------------------------------ |
| pnpm dev | Docs site dev server |
| pnpm build | Docs site production build |
| pnpm build:lib | Library build into dist/ via tsup |
| pnpm lint | ESLint, including the library-boundary rule |
| pnpm format / pnpm format:check | Prettier |
| pnpm typecheck | tsc --noEmit |
| pnpm test | Vitest, jsdom |
| pnpm test:browser | Storybook stories in a real browser, plus a11y |
| pnpm test:e2e | Playwright against the built docs site |
| pnpm build-storybook | Static Storybook, also the Chromatic input |
| pnpm check:package | Packs, then runs publint and Are The Types Wrong |
| pnpm test:consumer | Installs the tarball into Vite and Next.js apps |
| pnpm check | Fast local gate: static checks and both suites |
| pnpm release:check | pnpm check plus build, package and consumer |
Do not run pnpm build while pnpm dev is running — both write to .next.
Library build
pnpm build:lib is three steps:
- tsup in preserved-module mode — every source file
becomes one ESM file in
dist/, with source maps. Nothing is bundled, which is what keeps"use client"attached to the individual client modules instead of collapsing the whole library into one client boundary. CSS files are copied next to the modules that import them. tsc -p tsconfig.build.jsonfor the declarations, one per module.scripts/declaration-extensions.mjs, which gives the relative specifiers in those declarations explicit.jspaths and drops the stylesheet imports TypeScript copies into them. Node16 resolution has no directory lookup, so without this step every entry point is a dead end for a consumer.
tsup can emit the declarations itself, and used to. Its dts pass flattens the
whole type graph in a worker thread: it peaked at 2.1 GB and failed outright on
smaller machines, with an error that named no file. tsc produces the same
public surface from the same config in a fifth of the memory and a quarter of
the time, and points at a line when something is wrong. Are The Types Wrong runs
against the packed tarball on every pnpm check:package, so the rewrite step
cannot silently regress.
The release process is documented in CONTRIBUTING.md.
Release status
0.1.0 is published as zerogravity.
The name zerogravity-ui was already taken on npm by an unrelated package, so
the shorter name was claimed instead; the repository keeps its original slug.
The release process is in CONTRIBUTING.md. A custom
docs domain is still unconfigured, and CI reports the Chromatic visual job as
skipped until CHROMATIC_PROJECT_TOKEN is set — neither blocks a release.
Adding a component
src/lib/<component>/with the component, anindex.tsdescribing its public surface, and aREADME.mdwith the prop table.- Re-export the intended API from
src/lib/index.ts. - A control schema (
src/playground/<component>/schema.ts, or an entry insrc/playground/experimental/schemas.ts) and a preview insrc/playground/previews/. - An entry in
src/docs/registry.tsand its preview insrc/docs/previews.tsx. The route, the sidebar, search, the props table and the usage snippet all come from that one entry. - A test next to the component covering rendering, keyboard or pointer behaviour, cleanup, and reduced-motion.
License
MIT © Vladyslav Pilkevych
