An earthy, tree-themed design system for rogueoak β built on Radix Β· shadcn Β· Tailwind v4 Β· TypeScript.
Note
π§ Status: early development. Canopy is being built in the open, foundation-first.
The first packages are published to npm under the @rogueoak scope; APIs below marked
(planned) don't exist yet β they describe where we're headed. Follow the
roadmap for what's live.
Canopy is the design system that defines the look, feel, and building blocks of rogueoak β its products and its website. It ships as consumable npm packages so any rogueoak app can build interfaces from the same earthy, considered foundation.
The whole system is organised like a tree, foundation β composite, and every layer is named for a part of one. rogueoak is the forest, canopy is the system, and the layers below grow from shared roots.
Atomic design, renamed by tree anatomy:
| Atomic layer | Canopy name | What lives here |
|---|---|---|
| Design tokens | Roots π± | primitive + semantic tokens β colour, type, spacing, radii, elevation, motion. Everything draws nourishment from here. |
| Atoms | Seeds | the smallest components β Button, Input, Label, Badge (icons ship separately, see below) |
| Molecules | Twigs | small compositions β FormField, SearchBar, Card |
| Organisms | Branches | larger assemblies β NavBar, DataTable, Dialog |
| Templates (later) | Boughs | page scaffolds and layout patterns |
| The whole system | Canopy | the published library + the Storybook showcase |
Components only ever consume Roots semantic tokens (color-surface, text-primary,
radius-control) β never raw palette values. Light and dark are a property of the token
layer: semantic tokens remap per theme, so a component is themed without knowing it.
Icons are atom-tier too, but ship as their own package β @rogueoak/icons, a curated,
tree-shakeable set re-exported from react-icons
(Lucide glyphs + the popular social marks). They sit apart from @rogueoak/canopy because their
dependency footprint (react-icons, no tokens) and release cadence differ; they render in
currentColor, so they theme through whatever text colour they inherit.
Roots is a token source of truth, not hand-written CSS. Tokens are authored once as DTCG JSON and compiled β via Style Dictionary β into the outputs each consumer needs:
- CSS custom properties (
tokens.css) for runtime theming - A typed TypeScript export (
tokens) for programmatic access - A Tailwind v4
@themepreset (tailwind-preset.css) so utilities map straight onto tokens
The system is two-tier, so theming is a remap of one layer and never touches components:
- Primitive ramps β the raw palette,
50β¦950:moss(brand),bark,stone(neutrals),amber(accent), and desaturated functional rampssuccess/warning/danger/info. Muted & natural; moss/olive brand. Primitives are never used by components directly. - Semantic tokens β theme roles that reference primitives: surfaces
(
color-bg,color-surface,color-muted), text (color-text,color-text-muted,color-text-subtle), lines (color-border,color-ring), roles (color-primary+-foreground,secondary,accent), interaction states (-hover/-active, plus acolor-disabled+-foregroundconvention) and status (success/warning/danger/info). Components consume only these. Each role has a light and a dark value β see Theming.
Alongside colour: typography (Figtree sans + Geist Mono, type scale text-xsβ¦6xl, weights,
leading, tracking), spacing (4px base), radii, elevation (shadow-*), and motion
(durations + easings). Token names flatten onto Tailwind v4 @theme namespaces so utilities
generate directly: color-*βbg-*/text-*, radius-mdβrounded-md, text-lgβtext-lg,
font-sansβfont-sans, shadow-mdβshadow-md, and spacing utilities (p-4, gap-2) derive
from a single --spacing base.
Fonts are self-hosted (no CDN). Roots ships the family names; consumers install the
open-licensed @fontsource packages and import them once:
pnpm add @fontsource-variable/figtree @fontsource-variable/geist-mono/* in your global stylesheet, alongside the Roots imports */
@import '@fontsource-variable/figtree';
@import '@fontsource-variable/geist-mono';
@import '@rogueoak/roots/tokens.css';
@import '@rogueoak/roots/tailwind-preset.css';This pipeline is deliberately built to grow: a native (Swift) target can be added later as just another output platform, without rewriting a single token. (native target: planned)
The theme is a property of the token layer, not your components. tokens.css declares the
light theme on :root and a .dark block that re-points only the semantic vars at
different primitive ramp steps (primitives are the shared, theme-agnostic palette and are not
repeated). Because every utility (bg-primary, text-default, β¦) and var(--color-*) read
resolves through those runtime vars, toggling a single class re-themes the whole UI with zero
per-component code. Light is the default; add dark to a root element to flip:
// the one-line mechanism β toggle the class on <html> (or any container)
document.documentElement.classList.toggle('dark');Optional β respect the OS preference on first paint (before your app hydrates):
<script>
// bootstrap: honour a saved choice, else the OS setting
const saved = localStorage.getItem('theme');
const dark = saved ? saved === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.toggle('dark', dark);
</script>The common path needs no dark: utilities β semantic tokens auto-flip. For the rare
explicit case, add Tailwind's dark variant once in your global CSS so dark: utilities work:
@custom-variant dark (&:where(.dark, .dark *));Every semantic role meets WCAG AA in both themes β a build-time test computes the real
contrast ratios for light and dark and fails the build on any regression. Interaction-state
roles (color-primary-hover/-active, secondary, accent, danger-hover) and a
color-disabled surface + color-disabled-foreground convention are defined with light and
dark values too, ready for the first components.
Canopy publishes under the @rogueoak npm scope as a small set of versioned packages:
| Package | Holds | Status |
|---|---|---|
@rogueoak/roots |
design tokens + Tailwind preset | published |
@rogueoak/canopy |
components (/seeds, /twigs, /branches) |
published |
@rogueoak/icons |
curated icon set (re-exported from react-icons) | published |
Releases are tag-driven: pushing a bare-SemVer tag (X.Y.Z, no v prefix) publishes all
three packages at that version via GitHub Actions (.github/workflows/release.yml,
npm trusted publishing / OIDC).
pnpm add @rogueoak/canopy @rogueoak/rootsimport { Button } from '@rogueoak/canopy/seeds';
export function Example() {
return <Button>Plant a seed</Button>;
}Canopy ships className strings (Tailwind v4 utilities), not a prebuilt stylesheet β
so your build generates and tree-shakes only the utilities you actually use, and your
.dark flips canopy too. You wire this once in your global CSS: import Tailwind and the
Roots preset, then add @source pointing at @rogueoak/canopy so Tailwind scans canopy's
component source and emits its utilities into your build:
@import 'tailwindcss';
@import '@rogueoak/roots/tailwind-preset.css';
/* Generate canopy's component utilities by scanning its shipped code. Without this,
canopy components render UNSTYLED β the utilities never get emitted. `@source` takes a
PATH (Tailwind v4 has no bare-package resolution), RELATIVE TO THIS CSS FILE β adjust the
`../` depth so it resolves to canopy in your node_modules. */
@source '../node_modules/@rogueoak/canopy';(Add the @rogueoak/roots/tokens.css and @fontsource imports too β see
Tokens & theming.) This is exactly how the Storybook app β Canopy's
first consumer β is wired (it points @source at canopy's source by relative path). A
prebuilt-CSS bundle for non-Tailwind consumers may come later.
The component showcase β swatches, type specimens, and every component in light and dark β
lives on GitHub Pages, built from Storybook and deployed by CI on every push to main:
https://rogueoak.github.io/canopy/. The Foundations section is the living spec β
colour ramps + semantic swatches, the Figtree type specimen and scale, spacing, radii,
elevation, motion, a WCAG AA contrast table, and a Theme demo. Use the toolbar
Light / Dark toggle β every story reads correctly in both themes (it flips .dark).
Canopy is a pnpm + Turborepo monorepo. Requires Node 20+ and pnpm 11+
(npm install -g pnpm). The workflow:
pnpm install # install the workspace
pnpm build # build tokens (Style Dictionary), components (tsup), and Storybook
pnpm storybook # run the showcase locally at http://localhost:6006
pnpm test # run the test suite (Vitest)
pnpm lint # lint the workspace (ESLint + Prettier)Layout:
| Path | Package | What it is |
|---|---|---|
packages/roots |
@rogueoak/roots |
design tokens β CSS vars, typed TS export, Tailwind v4 preset (Style Dictionary) |
packages/canopy |
@rogueoak/canopy |
components, built to ESM + types (tsup) |
apps/storybook |
private | the Storybook showcase, deployed to GitHub Pages |
Roots ships the real foundation plus light & dark theming: primitive ramps + semantic tokens (light + dark, with interaction states), type, spacing, radii, elevation, and motion. The full Seeds atom catalogue is live, built on the shared component recipe (
cn(), cva variants over semantic tokens, RadixSlotforasChild). The first Twigs (molecules) are live too β FormField, SearchBar, and Card β composing those atoms on the@rogueoak/canopy/twigssubpath. The Branches (organisms) layer is filling in on the new@rogueoak/canopy/branchessubpath β Dialog (a focus-trapping, portalled modal), TopNav (a responsive, hand-rolled-disclosure navigation bar), and SideNav (a collapsible side rail whose mobile drawer reuses the Radix Dialog primitive) β each composing Seeds and Twigs.
Built foundation-first, so there's always working software and working docs at each step:
- Roots β tokens: palette, typography, spacing, radii, elevation, motion; light & dark theming
- Seeds β the atoms; the full first catalogue is live
- Twigs β molecules; the first compositions are live (FormField Β· SearchBar Β· Card)
- Branches β organisms; the layer is open (Dialog Β· TopNav Β· SideNav are live; DataTable to come)
- Icons β
@rogueoak/icons, a curated tree-shakeable set (Lucide + social marks) re-exported from react-icons - Boughs β page scaffolds and layout patterns
Development follows the Spectra protocol: every change is built and tested before merge, and this README is updated as the system grows so the docs never outrun the software.
MIT Β© rogueoak