@still-void/ui
v1.1.1
Published
Still Void design system — framework-agnostic TypeScript core (tokens, CSS, recipes, behaviors) with an optional React adapter compatible with Next.js Server Components.
Maintainers
Readme
@still-void/ui
Still Void design system as a framework-agnostic TypeScript library — extracted from the
blog.kalleopinheiro.dev prototype. Works with React, Angular, Vue, or plain HTML, with
first-class support for Next.js Server Components.
Architecture
The library is layered so the core never depends on any framework:
| Layer | Entry | Runs where | What it is |
|---|---|---|---|
| Tokens | @still-void/ui | anywhere | Typed constants: colors (hex + oklch), typography, spacing, radii, motion |
| Theme CSS | @still-void/ui/theme.css | browser | CSS vars (--sv-*), dark/light via data-theme, accents via data-accent, signature utilities |
| Component CSS | @still-void/ui/style.css | browser | All component classes (sv-*) |
| Recipes | @still-void/ui | anywhere (RSC-safe) | Pure functions returning class strings: postCard({ dense: true }) |
| Behaviors | @still-void/ui | client | Vanilla DOM: createThemeManager, createScrollSpy, createReadingProgress, copyToClipboard |
| React (server-safe) | @still-void/ui/react | server or client | Components without hooks — render inside Server Components |
| React (client) | @still-void/ui/react/client | client | 'use client' bundle: ThemeProvider, ThemeToggle, CopyButton, TableOfContents, ReadingProgress, hooks |
Install
npm install @still-void/uiReact is an optional peer dependency — non-React consumers install nothing extra.
Usage — Next.js (App Router, Server Components)
// app/layout.tsx (Server Component)
import '@still-void/ui/theme.css';
import '@still-void/ui/style.css';
import { Header, Logo, Footer, ThemeScript } from '@still-void/ui/react';
import { ThemeProvider, ThemeToggle } from '@still-void/ui/react/client';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
// Server-rendered attributes = correct theme for a first-time visitor.
// <ThemeScript /> runs before paint and re-applies a *returning* visitor's
// stored preference, so there's no flash of the wrong theme either way.
<html lang="en" data-theme="dark" data-accent="cyan" suppressHydrationWarning>
<head>
<ThemeScript />
</head>
<body className="sv-body">
<ThemeProvider>
<Header
logo={<Logo label="still.void" />}
items={[{ label: 'Home', href: '/', active: true }]}
actions={<ThemeToggle />}
/>
{children}
<Footer author="Kalleo Pinheiro" links={[{ label: 'RSS', href: '/rss' }]} />
</ThemeProvider>
</body>
</html>
);
}// app/page.tsx (Server Component — zero client JS for this content)
import { Hero, PostGrid, PostCard, Layout } from '@still-void/ui/react';
export default async function Home() {
const posts = await getPosts(); // fetch on the server
return (
<Layout>
<Hero eyebrow="blog" title="Calm engineering, sharp accents" />
<PostGrid>
{posts.map((post) => (
<PostCard key={post.href} post={post} />
))}
</PostGrid>
</Layout>
);
}Everything in @still-void/ui/react is hook-free and browser-API-free, so it renders on the
server. Interactive pieces come from @still-void/ui/react/client and are composed in via
slots (actions, visual, eyebrow).
// CodeBlock stays a Server Component; only the copy button hydrates.
import { CodeBlock } from '@still-void/ui/react';
import { CopyButton } from '@still-void/ui/react/client';
<CodeBlock code={source} language="ts" actions={<CopyButton code={source} />} />;Syntax highlighting is bring-your-own (by design — the prototype tokenizer was demo-only):
render with Shiki on the server and pass the markup via rendered.
Usage — any other framework (recipes + behaviors)
import { postCard, postCardClasses, createThemeManager } from '@still-void/ui';<!-- Angular -->
<article [class]="postCard({ dense: true })">
<h3 [class]="postCardClasses.title">{{ post.title }}</h3>
</article>Behaviors are plain DOM and return a destroy():
const theme = createThemeManager(); // drives data-theme / data-accent, persists to localStorage
theme.toggleMode();
theme.setAccent('violet');See playground/index.html for the full catalog rendered with zero
framework code — open it with npm run playground after npm run build.
Fonts
theme.css sets --sv-font-display/--sv-font-body/--sv-font-mono to Sora, Manrope and
JetBrains Mono, but does not load them — that's a render-blocking @import on every
consumer's critical path, and a runtime dependency on a third-party CDN. Load them yourself:
// app/layout.tsx — next/font self-hosts and inlines the @font-face rules, no network request
import { Sora, Manrope, JetBrains_Mono } from 'next/font/google';
const sora = Sora({ subsets: ['latin'], weight: ['400', '600', '700'], variable: '--sv-font-display' });
const manrope = Manrope({ subsets: ['latin'], weight: ['400', '500', '600', '700'], variable: '--sv-font-body' });
const mono = JetBrains_Mono({ subsets: ['latin'], weight: ['400', '500'], variable: '--sv-font-mono' });<!-- any other framework: preconnect + stylesheet -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700&family=Manrope:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>System-font fallbacks (ui-sans-serif, system-ui, ui-monospace) already cover the gap
before your chosen font loads — see Fidelity rules.
Theming
- Mode:
data-theme="dark" | "light"on<html>(dark is default). - Accent:
data-accent="cyan" | "violet" | "mint" | "amber"— pure CSS, so it works in Server Components with no JS. - Everything is overridable via CSS vars (
--sv-accent,--sv-bg, …). No!importantanywhere.
Fidelity rules (do not regress)
- Color values (hex/oklch) are literal from the spec — never rounded.
.sv-gradient-borderis the visual signature; never replace withbox-shadow.- Cards have no shadow.
text-wrap: balanceon every display heading.- Fonts: Sora (display) / Manrope (body) / JetBrains Mono (code). System fonts are loading fallbacks only.
- Hover easing
cubic-bezier(.3,.7,.4,1); reading progress bar is linear. - Categories are colored dots + label — never emoji.
Note: the reference sources (
colors_and_type.css,ui_kits/blog/*.jsx) were not available at extraction time. Colors, accents, fonts, radii, easing and component inventory come literally from the spec document; the numeric type/spacing scales were derived and are centralized insrc/tokens/for easy correction against the originals.
Scripts
npm run build # tsup (ESM + CJS + .d.ts) + CSS to dist/
npm test # vitest (31 tests)
npm run typecheck # tsc --noEmit (strict)
npm run lint:package # publint + are-the-types-wrong on the packed tarball
npm run playground # serve the framework-free catalog
npm run storybook # component catalog at localhost:6006
npm run build-storybook # static Storybook build → storybook-static/
npm run changeset # record a change for the next release
npm run version-packages # apply pending changesets, bump version, update CHANGELOG (CI does this)
npm run release # build + publish to npm (CI does this)Versioning
Releases are managed with Changesets and are fully automated — nobody publishes from a laptop:
- A pull request that touches
src/orscripts/must carry a changeset (npm run changeset); CI fails without one. - Merging into
mainopens achore: version packagespull request with the version bump and the generatedCHANGELOG.md. - Merging that publishes to npm with
provenance, tags
v<version>, creates the GitHub release and redeploys Storybook.
The changelog and the version are generated from changeset entries, never written by hand.
Unreleased changes can be tried out from a canary snapshot before they ship.
Bump levels for a design system (what counts as breaking, and why a corrected token is a patch while a redesigned one is a major) are documented in CONTRIBUTING.md.
