npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@c96/ui

v0.1.2

Published

Reusable design system extracted from Studio C96 — tokens, motion primitives, hooks and a generic WebGL liquid-text effect.

Readme

@c96/ui

An opinionated design system extracted from Studio C96 — design tokens, a full accessible component library built on Base UI, motion primitives, hooks, and a generic WebGL liquid-text effect. Built for React 19 / Next.js (app router), monochrome and type-led by default, and fully re-skinnable.

  • Tokens, split for re-skinning — structural tokens (type scale, spacing, motion, z-index) are separate from brand tokens (colours), so a new project overrides one layer.
  • Default typeface: Public Sans (SIL OFL — redistributable). Swap it by overriding --font-sans.
  • Component library on Base UI — form controls, overlays (dialog, menu, popover, tooltip, toast…) and structure primitives, headless behavior + accessibility from @base-ui/react (a regular dependency — nothing extra to install), styled with the c96 tokens.
  • Motion primitivesSplitText, MaskText, SplitLines, SplitParagraphs, MagneticButton, SmoothScroll, AppLink.
  • WebGL — a generic LiquidText (any lines, any font) on an isolated subpath.
  • Accessible by default — keyboard nav, focus management and aria wiring come from Base UI; everything resolves cleanly under prefers-reduced-motion.

Install

npm install @c96/ui
# peers (install what you use):
npm install react react-dom motion lenis
# only if you use @c96/ui/webgl:
npm install three @react-three/fiber @react-three/drei

Pre-built ESM with "use client" directives. In Next.js add it to transpilePackages so the directives are picked up:

// next.config.ts
export default { transpilePackages: ["@c96/ui"] };

Wiring

1. Import the stylesheet once (root layout)

import "@c96/ui/styles";

This pulls in, in order: fonts → structural tokens → brand theme → reset → utilities → component styles → lenis hooks → reduced-motion. The Public Sans @font-face files ship inside the package and resolve automatically.

2. Root layout — smooth scroll + toasts

"use client";
import { SmoothScroll, Toast } from "@c96/ui";

function Toasts() {
  const { toasts } = Toast.useToastManager();
  return toasts.map((toast) => (
    <Toast.Root key={toast.id} toast={toast}>
      <Toast.Content>
        <Toast.Title />
        <Toast.Description />
      </Toast.Content>
      <Toast.Close aria-label="Close">×</Toast.Close>
    </Toast.Root>
  ));
}

export default function Providers({ children }) {
  return (
    <Toast.Provider>
      <SmoothScroll>{children}</SmoothScroll>
      <Toast.Portal>
        <Toast.Viewport>
          <Toasts />
        </Toast.Viewport>
      </Toast.Portal>
    </Toast.Provider>
  );
}

(Skip the Toast parts if the site doesn't push notifications — nothing else depends on them.)

3. Use the components

import { SplitText, MagneticButton, Field, Input } from "@c96/ui";

<h1><SplitText text="Studio grade." /></h1>
<MagneticButton onClick={...}>Get in touch</MagneticButton>

<Field.Root name="email">
  <Field.Label>Email</Field.Label>
  <Input type="email" required />
  <Field.Error match="valueMissing">Required.</Field.Error>
</Field.Root>

Entry points

| Import | Contents | Notes | | --------------------- | ----------------------------------------------------- | ----- | | @c96/ui | Components + hooks + cx + tokens/math | Client-tagged | | @c96/ui/tokens | EASE, DUR, Z, math helpers | Pure, server-safe | | @c96/ui/webgl | LiquidText, LiquidContainer | Needs three / r3f / drei | | @c96/ui/styles | The full stylesheet | Import once | | @c96/ui/styles/* | Individual CSS files (e.g. tokens.base.css) | For custom composition | | @c96/ui/fonts/* | Raw font files | Copy to public/ for WebGL |

Components

Most components are compound: they mirror Base UI's part model 1:1 as a namespace object (Dialog.Root, Dialog.Trigger, Dialog.Popup, …). Every part accepts Base UI's full prop surface — including the render prop to swap the underlying element (<Dialog.Trigger render={<Button>Open</Button>} />) — with className narrowed to a plain string; state styling is handled in CSS via Base UI's data attributes ([data-open], [data-checked], [data-invalid], …). The docs/ app is a living reference with one page per component.

import { Dialog, Button } from "@c96/ui";

<Dialog.Root>
  <Dialog.Trigger render={<Button>Open</Button>} />
  <Dialog.Portal>
    <Dialog.Backdrop />
    <Dialog.Popup>
      <Dialog.Title>Title</Dialog.Title>
      <Dialog.Close render={<Button>Close</Button>} />
    </Dialog.Popup>
  </Dialog.Portal>
</Dialog.Root>

ActionsButton (variant?: "default" | "accent" | "danger", size?: "default" | "small", render for link-buttons), Toggle + ToggleGroup (exclusive by default, multiple for multi-select), Toolbar (one tab stop, arrow-key nav).

FormsForm (aggregates native validation, maps server errors onto fields), Field (Root/Label/Control/Description/Error — automatic label/aria/id wiring), Fieldset, Input (compact? for inline table-cell edits), Textarea (styled native; compose with <Field.Control render={<Textarea />} />), Select (full listbox with typeahead) and NativeSelect (zero-JS <select>), Checkbox + CheckboxGroup, Radio + RadioGroup, Switch, NumberField (steppers, wheel/arrow stepping, scrub area), Slider (single or range).

OverlaysDialog (focus trap, scroll lock), AlertDialog (no light dismiss), Popover, Tooltip (wrap groups in Tooltip.Provider), Menu + ContextMenu (typeahead, submenus, checkbox/radio items), PreviewCard, Toast (mount Toast.Provider + Toast.Viewport once at the root — next to SmoothScroll — then push from anywhere with Toast.useToastManager()).

Structure & feedbackTabs (sliding indicator), Accordion, Collapsible, ScrollArea, Separator, Avatar, Progress (determinate/indeterminate), Meter.

TableTable, TableHead, TableBody, TableRow, TableCell, TableHeaderCell: a thin structural wrapper, not a data-grid. Nest plain <thead>/<tbody>/<tr>/<th>/<td> inside; reach for TableCell/TableHeaderCell only for the hint and sortable/sorted/onSort behaviors.

MotionSplitText (per-character reveal), MaskText (single-line reveal), SplitLines (measures wrapped lines, staggers on scroll-in), SplitParagraphs, MagneticButton (cursor-following, static on touch/reduced-motion), SmoothScroll (Lenis wrapper), AppLink (safe external defaults).

Overlay notes

  • The reset sets isolation: isolate on <body> (a Base UI requirement) so portaled popups stack correctly without z-index wars; overlays sit on --z-overlay. If you portal outside <body>, replicate the isolation.
  • Portals and theming: popups portal to <body>, so a popup triggered from inside a [data-theme="dark"] section renders on the light root theme. Theme at the app root (<html data-theme="dark">) or pass a container to the Portal part.

Breaking changes vs the pre-Base-UI 0.1.0 API

| Old | New | | --- | --- | | Label | removed — use Field.Label | | Field({ label, hint, children }) | compound Field.Root/Label/Description/Error | | Select (styled native) | Select is now the Base UI compound; native is NativeSelect | | Input, Textarea, Button, Table* | same shape, upgraded internals | | @c96/ui/styles/components/form.css | split into field/fieldset/form/input/… files |

Future candidates (exist in Base UI, not wrapped yet): Autocomplete, Combobox, Drawer, OTP Field, Menubar, NavigationMenu.

cx

The tiny class joiner used internally is exported: cx("a", cond && "b")"a b". Server-safe.

Hooks

useMediaQuery, useIsTouch, usePrefersReducedMotion, useIsClient — all SSR-safe.

Tokens in JS

import { EASE, DUR, Z } from "@c96/ui/tokens";
// EASE.outExpo -> [0.16, 1, 0.3, 1]  (matches --ease-out-expo)

WebGL liquid text

"use client";
import { LiquidText } from "@c96/ui/webgl";
import { useIsClient } from "@c96/ui";

function Hero() {
  const isClient = useIsClient(); // canvas is client-only
  return (
    <div style={{ height: "70svh" }}>
      {isClient && <LiquidText lines={["HELLO", "WORLD."]} animate />}
    </div>
  );
}

The font is fetched at runtime by troika, so it must be a URL your app serves. Copy the bundled font into your public/ once:

cp node_modules/@c96/ui/fonts/PublicSans-Variable.ttf public/fonts/

The default font prop is /fonts/PublicSans-Variable.ttf. Pass your own (heavier display) font URL via font="...". Trail look is tunable via radius, strength, dissipation, dispStrength.

Re-skinning

Structure lives in tokens.base.css; the brand lives in tokens.theme-c96.css. To recolour without touching components, import the stylesheet then override the brand custom properties:

/* your app */
@import "@c96/ui/styles";

:root {
  --paper: #ffffff;
  --ink: #111111;
  --accent: #0044cc;
  --accent-contrast: var(--paper); /* text on accent-filled surfaces */
  --bg: var(--paper);
  --fg: var(--ink);
  --fg-muted: #777;
  --rule: rgba(0, 0, 0, 0.12);
  --success: #2e8b57;
  --warning: #d99a2b;
  --danger: #d13c2f;
  --hover-tint: rgba(0, 0, 0, 0.04);
}

A [data-theme="dark"] block ships with the C96 theme and flips --bg/--fg/--fg-muted/ --rule/--hover-tint. --success/--warning/--danger are semantic status colours (used sparingly, same discipline as --accent) — override them alongside the rest of the brand layer if your palette needs different ones.

Documentation site

docs/ is a Next.js app serving as the full documentation site: a landing page, Getting started and Theming guides, one page per component (live demos, highlighted copyable snippets, props tables, keyboard notes), a token viewer with live values, and a global dark-theme switch.

cd docs && npm install && npm run dev

In dev, the docs app resolves @c96/ui from src/ (turbopack alias), so editing the library source hot-reloads instantly — no npm run build needed. Production builds keep resolving dist/ for published-artifact parity.

The site is a static export: cd docs && npm run build produces a self-contained docs/out/ ready to host anywhere (e.g. Cloudflare Pages).

Develop

npm run build      # tsup -> dist/ (per-file ESM, preserves "use client") + d.ts + CSS
npm run typecheck  # tsc --noEmit (strict)

License

MIT for the code. Public Sans is licensed under the SIL Open Font License 1.1 (see fonts/OFL.txt).