Getting started with cascivo
Two minutes: a component on screen
The fastest path is the prebuilt package. In any React 18+ app built with a bundler (Vite, Next.js, webpack):
pnpm add --save-exact @cascivo/react @cascivo/themes @preact/signals-react
import '@cascivo/themes/light-dark.css'
import { Button, Card, CardContent } from '@cascivo/react'
export function App() {
return (
<main data-theme="light">
<Card>
<CardContent>
<Button>It works</Button>
</CardContent>
</Card>
</main>
)
}
That is the whole setup. The theme import and the data-theme attribute are the only
required wiring — without them components render uncolored. Component CSS comes in with
each import.
When that renders, decide the rest:
- Want to own and edit the source? Switch to Path A (copy-paste via the CLI).
- Components need state? Read State in your own components — hold it with
useSignalStateand write through the setter. - Strict ESLint or the React Compiler? Read Your linter and the
signal.value = nextidiom before your first lint run.
Which versions go together
- The packages that share
@cascivo/core—core,react,charts,editor,flow,i18n,storage,ai,text,render— release together. Install them all at the same version. @cascivo/themes,@cascivo/tokens,@cascivo/iconsand thecascivoCLI have their own version numbers. Any current1.xworks with the rest.- Packages still on
0.x(@cascivo/mcp,@cascivo/email, the ESLint packages, …) are tooling; they are not covered by the 1.x stability contract.
The current pairs are in the compatibility table,
and cascivo doctor reports a mismatched install.
Step 0 — don't read this linearly if you don't have to
This page plus the guides it links is ~2,000 lines, and three hands-on reports have now said the linear read is a real cost even when it pays off (2026-08-31 report §30). If you are working with an agent, or you just want an answer rather than an education, index the docs first and ask them questions instead:
pnpm add -D --save-exact @cascivo/docspack docspack
npx docspack sync # once per machine
npx docspack ask "how do I wire a theme" # ~3,000 tokens, offline
sync builds a local SQLite index; ask, search and list then make no network
requests, and every answer is scoped to the versions in your lockfile rather than to
whatever cascivo.com documents today. Details, timings, and the agent-instruction snippet
are in Searching the docs.
Two things are still worth reading in full, whichever way you get here: the two install paths and the theme wiring, because skipping the second is what makes components render uncolored.
There are two ways to adopt cascivo. Both consume the same tokens and themes, and they can coexist in one project.
| Path | You get | Choose it when |
|---|---|---|
| A. Copy-paste (CLI) | Component source (TSX + CSS) copied into your repo | You want to own and edit the code — the shadcn model |
B. Prebuilt (@cascivo/react) | A normal npm dependency, all 214 components | You just want to use the system; upgrades via pnpm up |
Either way, one piece of wiring is not optional: importing the themes CSS
and setting data-theme. Skip it and components render as correctly-structured
but uncolored markup — see What it looks like when you forget.
Reading this offline, or can't reach the docs site? The whole reference is also an npm package:
npx -y @cascivo/docs(index),npx @cascivo/docs <component>(one reference),npx @cascivo/docs guide getting-started. No website needed — it comes through the same registry that installs the packages.Working with an AI agent? Install the docs as a searchable index instead:
pnpm add -D @cascivo/docspack docspack && npx docspack sync, thennpx docspack ask "<question>"answers offline from the versions in your lockfile. See Searching the docs.
Where the documentation lives
Four surfaces, and each is authoritative for something different. Two hands-on reports independently reached a working model only after reading all four, so it is worth 30 seconds to know which to open.
| Surface | Authoritative for | Reach it |
|---|---|---|
The shipped .d.ts | Props — always check here first. Every prop carries its type, @defaultValue, the rationale, and ⚠ warnings for the mistakes previous adopters made (Flex defaults to vertical; a chart's title renders nothing visible; useToast() returns { toast }, not a callable). Both reports rated it the single best artefact in the project. | Ctrl-click any import in your editor, or node_modules/@cascivo/react/dist/index.d.ts |
llms.txt + /llms/<name>.md | Agent-facing reference: the component index with distribution channel, per-component pages (props → object types → examples → tokens → a11y), and the machine-readable catalogs (registry.json, tokens.catalog.json, icons.catalog.json). | https://cascivo.com/llms.txt, or offline via npx @cascivo/docs |
The guides (docs/*.md) | Cross-cutting concerns no single component owns: install paths, SSR, routers, theming, layers, lint config, the dashboard recipe. | This directory, https://cascivo.com/docs, or npx @cascivo/docs guide <name> |
| The docs site | The same content, rendered, plus live examples you can interact with. | https://cascivo.com |
If two disagree, the .d.ts wins — it is generated from the same source the
components compile from, and the parity guards in scripts/checks/ fail the build
when a manifest and its interface diverge.
The @cascivo/docs package ships two directories that look alike:
llms/<name>.md is the full per-component reference, and context/<name>.md is
the condensed intent summary (when to use, when not to, related components) meant
for pasting into an agent's context window.
Searching the docs instead of reading them
@cascivo/docs prints whole files. @cascivo/docspack ships the same content
pre-chunked in the docspack format, so an agent can query it:
pnpm add -D @cascivo/docspack docspack
npx docspack sync # index it, once per machine
npx docspack ask "which CSS layer do my overrides go in"
sync builds a local SQLite index; ask, search and list then make no network
requests at all. Two properties matter for an agent: the answer is version-scoped
to what your lockfile installed rather than what cascivo.com documents today, and it
is bounded — three chunks, ~3,000 tokens — so asking a question costs a fraction
of fetching the page that contains the answer.
Run the installed binary, not npx. Measured on this ~600-chunk index, a query takes
83–92 ms through node_modules/.bin/docspack and about 400 ms through npx,
which re-resolves the package every time. Add a script rather than paying that on every
question:
{ "scripts": { "ask": "docspack ask" } }
One line in AGENTS.md or CLAUDE.md is the whole setup:
Run `docspack ask "<question>"` for documentation on this project's
dependencies. It answers from the installed versions.
For clients that prefer a declared tool, npx docspack mcp serves the same index over
MCP. That is complementary to @cascivo/mcp,
which serves the component registry — use @cascivo/mcp to pick and install a
component, and docspack to answer a "how do I…" question about one.
This is not layout-only. Alongside the CSS-native layout system, cascivo ships:
- Interactive components with the behavior wired in —
Dropdown,Menu,ContextMenu,Combobox,CommandMenu,MultiSelect, andTabsall come with keyboard navigation, focus management, and outside-click dismissal already implemented (native<dialog>/Popover APIs plus@cascivo/coreprimitives). You do not need to pair cascivo with a separate headless library or write your own ARIA event handling. - A full charts package —
@cascivo/charts(25 chart types: line, area, bar, sparkline, KPI, heatmap, and more), token-scaled to your theme, with live-streaming support. - An icon set —
@cascivo/icons(~440 tree-shakeable SVG icon components, sized by the token system) forSideNavitems,IconButton, andButtonicons. Don't hand-roll SVGs — install it and import the named icon. - Pre-built dashboard blocks —
block/dashboard-charts,block/stats-cards,layout/console-app; see them assembled into full apps underapps/examples/(deploy,pulse,trade,pay,track— Vercel/Datadog/Trade-Republic/Stripe/Linear-style consoles).
If you're building a dashboard/console page specifically, the
cascivo-design-page Claude Code skill and its component recipe are the
fastest path — see Where to go next.
Path A — copy-paste via the CLI
New project
npx cascivo create my-app
Scaffolds a ready-to-run Vite + React + TypeScript app pre-wired with the
cascivo app shell, side navigation, header, and a theme. Options:
--template <spec> (start from a marketplace template), --theme <name>,
--sections "<a, b>", --yes.
New project from a blueprint
A blueprint describes the app and names the registry block each page renders. The CLI turns it
into the same scaffold, with every block's source in src/blocks/ and every page wired into the
routes and the side nav:
{
"name": "acme-console",
"framework": "cloudflare",
"theme": "dark",
"pages": [
{ "title": "Overview", "block": "dashboard-overview" },
{ "title": "Users", "block": "users-table-page" },
{ "title": "Settings", "block": "settings-profile" },
{ "title": "Reports" }
]
}
npx cascivo create --from cascivo.app.json
A page without a block is a placeholder to build out. runtime, examples and auth take
the same values as the flags, and a blueprint is held to the same rules. An unknown field or
block fails with the list of valid ones. The blueprint is kept in the app as cascivo.app.json.
Agents use the MCP tools list_blocks and compose_app for the same thing.
Every cascivo create writes cascivo.app.json, whether it ran from flags, prompts or a
blueprint, so any scaffolded app can grow from its directory:
npx cascivo app add page "Users" --block users-table-page
npx cascivo app add example crud # a cloudflare example, as --example would have added it
npx cascivo app upgrade # this CLI's templates, keeping your edits
app add page adds the page to cascivo.app.json and writes the page, its block, and the
route and nav entries. The app's original files are rebuilt with the CLI version recorded in
cascivo.app.json, so a newer CLI's template changes are merged in, not mistaken for your
edits. A generated file you have edited since keeps your edits, merged three ways. When your
edit and the change touch the same lines, the file gets git-style conflict markers and the
command exits non-zero.
New project as a workspace
npx cascivo create my-app --workspace
Writes the same app into a pnpm workspace: the app in apps/web, the team's own components in
packages/ui (run npx cascivo add there, so every app shares one copy), Vite+ (vp run)
running each package's scripts, and a GitHub Actions workflow that runs typecheck, lint, test and
build on every push. A React + Vite app also gets Vitest with a smoke test. It needs
--framework react-vite (the default) or cloudflare, and pnpm.
Every scaffold, workspace or not, comes with agent files: a short AGENTS.md, a CLAUDE.md
that includes it, and a .mcp.json that registers the cascivo MCP server.
Existing project
npx cascivo init
npx cascivo add button card dialog
init pins exact versions for you — no carets to clean up afterwards.
init writes cascivo.config.ts and installs everything copied source needs in
one step: the runtime packages @cascivo/core, @cascivo/tokens,
@cascivo/themes, and the @preact/signals-react peer, plus cascivo as a dev
dependency (the generated config imports its CascadeConfig type). Flags:
--theme <name> (one of the twelve first-party themes); --package-manager <pm>
(alias --pm) to force pnpm/yarn/npm/bun — otherwise it auto-detects
from your lock file, packageManager field, or the PM that launched it, so it
works inside a pnpm/yarn workspace where the lock file lives at the repo root;
--no-install to write files and print the install commands instead of running
them; and --yes / -y to accept defaults without prompting (implied when stdin
is not a TTY, so it is safe in CI).
add copies the component source — TSX, its CSS module, and its manifest
(<name>.meta.ts: props, examples, tokens and accessibility, as data your own
tooling and agents can read) — from the registry into your project, resolving component dependencies (adding dialog also brings
anything it composes) and installing any extra runtime package a component
declares (e.g. @cascivo/i18n). Charts are the exception: cascivo add chart/area-chart installs the @cascivo/charts npm package (a runtime
dependency, not copied source) and prints the import lines. --dry-run shows
what would happen; --package-manager/--pm and --no-install work here too;
--yes skips confirmation.
The files the CLI manages
cascivo.config.ts — where the CLI reads its settings:
import type { CascadeConfig } from 'cascivo'
const config: CascadeConfig = {
registry: 'https://cascivo.com/registry.json', // component index
outputDir: 'src/components/ui', // where copied source lands
theme: 'light',
}
export default config
cascivo.lock — written by cascivo add. Records, per installed component,
the registry it came from, the version, and a sha256 hash of every copied file.
Commit it: it is what lets the CLI later tell your edits apart from upstream
changes.
Tracking upstream changes
Copied code is yours, but the registry keeps evolving. Check for drift:
npx cascivo update --check # lists outdated components, exits 1 if any
npx cascivo update button # three-way merge of upstream changes into your copy
update merges upstream changes around your local edits using the lockfile's
recorded base version; genuine collisions get standard conflict markers to
resolve by hand. See UPGRADING.md for the full story.
Path B — prebuilt dependency
pnpm add --save-exact @cascivo/react @cascivo/themes @preact/signals-react
(@cascivo/tokens comes with @cascivo/themes automatically — it is a direct
dependency, not a peer, so you never install it by hand.)
--save-exact is not optional decoration: this page tells you to pin exact
versions below, and a bare pnpm add writes ^1.0.0 for all three. Following
the command left one adopter with carets, contrary to the instruction two
paragraphs later (2026-08-31 report §26). npm and yarn take --save-exact too;
bun uses --exact. To make it the default for the project, put
save-exact=true in .npmrc.
Your linter and the signal.value = next idiom
Write state through a setter and this section does not apply to you. Both linters
below report signal.value = next on a signal a hook returned; the setter from
useSignalState (setCount(next)) is not a mutation, so it passes with every rule on — and
it is also the form the React Compiler can compile. See
State in your own components.
If your code already assigns .value in components, the rule will flag it until you tell it
not to. Pick the row that matches your project:
| Your linter | What to add |
|---|---|
oxlint (what pnpm create vite --template react-ts scaffolds in 2026) | "extends": ["./node_modules/@cascivo/eslint-config/src/oxlintrc.json"] in your .oxlintrc.json, or just "react/immutability": "off" in its rules |
ESLint with eslint-plugin-react-hooks@7 | pnpm add -D --save-exact @cascivo/eslint-config, then spread it last |
| Neither / an older linter | nothing — the rule does not exist to fire |
// eslint.config.js
import cascivo from '@cascivo/eslint-config'
export default [...yourConfig, ...cascivo] // spread LAST
// .oxlintrc.json
{
"extends": ["./node_modules/@cascivo/eslint-config/src/oxlintrc.json"],
"categories": { "correctness": "error" }
}
Both are the same rule — ESLint's react-hooks/immutability and oxlint's port of it,
react/immutability — and it reports every signal.value = next as
This value cannot be modified. Without the fix you get a lint warning on every
piece of state you write — 8 across 3 files in one reported build, and nothing
else. The oxlint half was missing entirely until the 2026-08-31 report (§13)
pointed out that the library which lints itself with oxlint shipped no oxlint
story for its adopters. See
USING-WITH-STRICT-ESLINT.md §1 for what turning
the rule off costs.
Peer dependencies: react >=18, react-dom >=18, and @preact/signals-react
(cascivo components are signal-driven, so the signals runtime is required).
On React 19 the signals runtime must be 3.x — the peer range enforces >=3
(3.x still supports React 16.14+/17/18), because signals-react 2.x imports a React
internal that React 19 removed. If a lockfile pinned 2.x from an earlier install,
cascivo doctor flags it with the upgrade command.
Component CSS ships per component and is pulled in automatically when you
import a component — your bundler includes styles only for the components you
use. There is no component-CSS import to add. (No bundler at all? Import the
aggregate @cascivo/react/styles.css instead.)
Typing a status map? The shared vocabulary types ship from a subpath:
import type { Tone } from '@cascivo/react/types'(alsoProgress,SpaceStep,RovingOrientationand the*Input/*Aliasspellings).Status.statusandBadge.variantare typed with these, so aRecord<DeployState, Tone>is the supported way to map your domain states onto them. Do not add@cascivo/coreto reach them — on this path it is a transitive dependency, and pinning it puts you in a two-package lockstep.
Server-rendering with Vite (TanStack Start, Remix, vite-ssr, workerd)? On
@cascivo/react0.10+ SSR works with zero Vite config — the package ships a CSS-freenode-condition build that a bare server loader imports cleanly. The only SSR checklist left:@preact/signals-react3.x, import the CSS set (@cascivo/react/styles.css+@cascivo/themes/light-dark.css), and — for runtime theme switching —themePreloadScript()+suppressHydrationWarning. On@cascivo/react< 0.10 you additionally needssr: { noExternal: [/^@cascivo\//] }(orcascivoSsr()), or an unconfigured build throwsUnknown file extension ".css". Full recipe: USING-WITH-VITE-SSR.md. Next.js App Router needs none of this — see USING-WITH-NEXTJS.md.
Trade-off vs Path A: you cannot edit component internals, but you upgrade with a
version bump instead of a merge. Full details in the
@cascivo/react README.
All packages are 0.x, and they version independently. A low number on one package means it has had fewer releases, not that it is less finished —
@cascivo/[email protected]is new, not immature. Compatibility is per-entrypeerVersionsinregistry.json, not version equality, so nothing has to move in lockstep.Pin exact versions (no
^). Before any upgrade, runcascivo doctor --drift— it readsbreaking-changes.json(published per package, machine-readable) and tells you what changed under you.
The critical wiring: themes + data-theme
Whichever path you chose, two things are not optional: import a theme
stylesheet once, and set data-theme on a root element. Pick the recipe that
matches what you ship.
Recipe A — light + dark with a system default (the common case):
import '@cascivo/themes/light-dark.css' // tokens (once) + base typography + light & dark
<main data-theme="light">…</main>
<main>or<html>? Both work — the attribute is scopable to any element. Set it in JSX when you ship one fixed theme; letThemeProviderset it on<html>when you switch themes at runtime (it must be the document element so portalled overlays switch too). The decision table is in THEMING.md.
@cascivo/themes/light-dark loads @cascivo/tokens once, applies base typography (so
plain markup uses the sans stack, not browser serif), and ships both light and
dark. Cost: ≈41 KB / ≈9 KB gzip of CSS (source, pre-minification).
Recipe B — shipping a single theme (e.g. a dark-only dashboard): import the
base scaffold plus your one theme, and set that theme's name:
import '@cascivo/themes/base.css' // tokens + base typography (required scaffold)
import '@cascivo/themes/dark.css' // your one theme
<main data-theme="dark">…</main>
Cost: ≈28 KB / ≈7 KB gzip — it drops the second theme (~2 KB gzip). The lever
is small: the bulk is the shared token layer, not the theme files. (For scale: the
component stylesheet @cascivo/react/styles.css is ~273 KB / ~37 KB gzip and
dwarfs any theme choice — if you measured a ~287 KB CSS chunk, that is it, not the
themes bundle.)
JS bundle size is measured too, and against the alternatives: see BENCHMARKS.md for min+gzip totals vs shadcn/ui and Carbon plus the per-component incremental cost. Per-component CSS is auto-included and tree-shaken by your bundler, so a real app ships a fraction of the aggregate sheet (a ~45-component dashboard measured 137 KB / 19 KB gzip of the 273 KB).
SSR gets the same saving. TanStack Start, Remix and Next collect component CSS through the same module graph, so budget the fraction, not the full sheet — a server-rendered card measured 29 KB of CSS end to end. Import a theme and let the component CSS ride along; the aggregate
@cascivo/react/styles.cssis for setups with no bundler. USING-WITH-VITE-SSR.md has the measurements.Before 0.18.0 this said the opposite, because it was true: with no
react-serverexport condition, RSC resolved the CSS-free server build and every server-rendered component lost its stylesheet, so the aggregate really was the only way to get a styled page. That was a bug, and it is fixed.
TypeScript setup for the CSS imports. A bare CSS side-effect import (
import '@cascivo/react/styles.css') has no types on its own, so atsc --noEmitstep fails withTS2307: Cannot find module …(orTS2882undernoUncheckedSideEffectImports). Add Vite's ambient types once — createsrc/vite-env.d.tscontaining/// <reference types="vite/client" />— or, if you don't usevite/client, declare the module explicitly withdeclare module '*.css'.npx cascivo createwrites this for you; the manual setup doesn't. See TROUBLESHOOTING.md.
Theme export → data-theme value
The export name is the attribute value: import @cascivo/themes/<name> and
set data-theme="<name>". The twelve first-party themes:
| Import | data-theme value | Base scheme |
|---|---|---|
@cascivo/themes/light | light | light |
@cascivo/themes/dark | dark | dark |
@cascivo/themes/warm | warm | light |
@cascivo/themes/flat | flat | light |
@cascivo/themes/minimal | minimal | light |
@cascivo/themes/midnight | midnight | dark |
@cascivo/themes/pastel | pastel | light |
@cascivo/themes/brutalist | brutalist | light |
@cascivo/themes/corporate | corporate | light |
@cascivo/themes/terminal | terminal | dark |
@cascivo/themes/cyberpunk | cyberpunk | dark |
@cascivo/themes/arcade | arcade | light |
@cascivo/themes/base is required scaffolding (tokens + typography), not a
theme — always load it (directly, or transitively via a bundle). @cascivo/themes/tailwind
is a Tailwind bridge stylesheet, also not a theme. Each import has a .css twin
(@cascivo/themes/dark.css) for bundlers that need the explicit extension.
Which bundle?
Setting data-theme="cyberpunk" while only light and dark are loaded leaves every
--cascivo-color-* unresolved, so components render greyscale. Pick the bundle that
contains the themes you actually set:
| Import | Contains | Use when |
|---|---|---|
@cascivo/themes/light-dark.css | light + dark | the common case — a light/dark toggle |
@cascivo/themes/all.css | all twelve | you offer a theme picker |
@cascivo/themes/<name>.css | that one theme | you ship a single fixed theme (pair with base.css) |
@cascivo/react/styles.css | light + dark, plus every component's CSS | no bundler / one-file setup |
Changed in 0.14.0.
all.cssused to contain light and dark only, despite the name — a trap that cost adopters real time. It now contains all twelve. If you imported it for a light/dark app, switch tolight-dark.cssto keep the smaller sheet. See UPGRADING.md.
ThemeProvider warns in dev when you set a data-theme whose CSS is not loaded, naming
the import to add.
Which stylesheets do I import?
Themes are always yours to import. Component CSS is not — every cascivo package that ships components imports its own stylesheet from its entry, so a bundler pulls it in when you import the component and tree-shakes what you don't use.
That holds for SSR too — Vite SSR, TanStack Start, Remix and Next all collect component CSS from the same module graph, so you import a theme and nothing else.
The exception is the node export condition. Vite-SSR frameworks externalise dependencies
on the server, and a bare .css side-effect import is unloadable by a plain Node ESM loader
(ERR_UNKNOWN_FILE_EXTENSION) — so cascivo ships a CSS-free node/ twin for those packages.
That twin is only ever the server half of the build: the client (and, via the
react-server condition, the RSC) graph still carries the CSS edges, which is what styles
the server-rendered HTML. You import the aggregate yourself only when no bundler walks
the graph at all — a CDN <link>, or an island runtime that strips the edges (Astro).
| Package | Bundler (Vite/Next/webpack), SPA or SSR | No bundler / CDN |
|---|---|---|
@cascivo/themes | always import a bundle | same |
@cascivo/react | automatic per component | @cascivo/react/styles.css |
@cascivo/charts | automatic (dist/index.js imports ./charts.css) | @cascivo/charts/styles.css |
@cascivo/charts/styles.cssis redundant on a bundler build, not required. It used to be required — until 0.14 the charts entry never imported its own sheet and charts rendered unstyled — and several docs still said so after the fix. Importing it anyway is harmless (the bundler dedupes it), so if you are unsure, import it. What you must not do is skip it on an SSR/externalised setup: the chart's screen-reader data-table fallback is hidden by that stylesheet, so without it the fallback renders visibly as a table of numbers under every chart.Enforced by
pnpm css-contract:check: a package that ships a stylesheet and declaressideEffects: ["**/*.css"]must import it from its entry, and ship the CSS-freenodetwin, and — if it also ships modules that render on the RSC server — offer areact-servercondition pointing back at the CSS-bearing build. Each half without the others just trades one bug for another: a styling bug for an SSR blocker, then the SSR blocker back for a styling bug that only shows up in Server Components.
Runtime switching & SSR (no-flash)
For a user-selectable theme, use the runtime from @cascivo/react:
import { ThemeProvider, useTheme } from '@cascivo/react'
function ThemeToggle() {
// useTheme() returns a TUPLE [theme, setTheme] where `theme` is a plain string
// (the current theme name) — use it directly, not `theme.value`. It is NOT a
// `{ theme, setTheme }` object (that's next-themes' shape). The hook calls
// useSignals() for you, so this re-renders on theme changes. For signal-native
// code (computed()/effect()/Preact) use themeSignal() instead.
const [theme, setTheme] = useTheme()
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme}
</button>
)
}
ThemeProvider persists the choice and drives data-theme for you; useTheme()
reads/sets it via that tuple. Under
SSR you must avoid a hydration mismatch and a flash of the wrong theme. Three
correct options, by how the theme is decided:
- Static theme (fixed for the whole app): hard-code
data-theme="dark"on your<html>in the server-rendered document. This is the right answer for a single-theme app — it is not a workaround, and it never mismatches. - Controlled by server state (e.g. a per-account theme from your DB): pass it as
<ThemeProvider value={serverTheme}>. The provider is SSR-safe on its own — it emits an inline attribute setter during render, so the first paint is themed with no flash and no extra<head>script. (Passnoncefor a strict CSP.) - Persisted / user-switchable theme (client
localStorage): inlinethemePreloadScript()(from@cascivo/react) in your document<head>before the app renders, so the attribute is set from storage before first paint, and addsuppressHydrationWarningto the<html>it writes to (the script mutatesdata-themepre-hydration; without the flag React 19 logs a mismatch). PassdefaultThemeto keep a "dark by default" app dark for light-OS visitors — it wins overprefers-color-scheme. See THEMING.md for the full recipe.
Never write a useEffect that toggles a .dark class — that is the pattern
ThemeProvider + themePreloadScript() exists to replace.
If you forget the theme import entirely: components render unstyled — correct
structure, no colors, wrong fonts, missing padding rhythm. Component CSS only
references var(--cascivo-*) custom properties; those properties do not exist until
the tokens + a theme are loaded. Unstyled-looking components are almost always this,
not a broken install. See TROUBLESHOOTING.md.
First component
import '@cascivo/themes/light-dark.css'
// Path A: import from your copied source
import { Button, Card, CardContent } from '@/components/ui'
// Path B: import { Button, Card, CardContent } from '@cascivo/react'
export function App() {
return (
<main data-theme="light">
<Card>
<CardContent>
<Button>Get started</Button>
</CardContent>
</Card>
</main>
)
}
Wiring your router
If your app has a router — most do — wire it once, at startup, before the nav components render anything:
import { setLinkComponent } from '@cascivo/react'
import type { LinkComponentProps } from '@cascivo/react'
import { Link } from 'react-router' // or '@tanstack/react-router'
setLinkComponent(({ href, ...rest }: LinkComponentProps) => <Link to={href ?? '#'} {...rest} />)
That one call makes SideNav, ShellHeader, Breadcrumb, Switcher and Dock render
real router links — client-side navigation, and still real <a>s, so middle-click and
open-in-new-tab keep working.
It does not cover links you write yourself in page content. Those use asChild:
<Link asChild>
<RouterLink to="/projects/alpha">alpha</RouterLink>
</Link>
Two kinds of link, two mechanisms — the most-reported friction in adopter reports. Full guide, including active-item matching and URL-driven tabs: USING-WITH-A-ROUTER.md.
State in your own components: useSignalState and useSignals()
Read this before you write a component that holds state. Two rules, and breaking either one fails quietly.
1. Hold local state with useSignalState, and write through the setter.
import { useSignalState } from '@cascivo/core' // prebuilt path: from '@cascivo/react'
function Counter() {
const [count, setCount] = useSignalState(0)
return <Button onClick={() => setCount((n) => n + 1)}>Clicked {count.value} times</Button>
}
Read count.value in render; write with setCount(next) or setCount((n) => n + 1).
Do not assign count.value = … in a component: the React Compiler refuses to compile a
component that assigns to a value a hook returned, and react-hooks/immutability reports it.
The setter form passes both — see
Your linter and the signal.value = next idiom.
2. A signal you did not get from a cascivo hook needs useSignals(). cascivo's hooks —
useSignalState, useSignal, useComputed and the rest — subscribe the component for you.
A module-level signal(), or one passed in as a prop, does not: in a React app with no Babel
signals transform (the normal case: Vite + React, Next.js), a component that reads it during
render never re-renders when it changes.
The symptom is distinctive: handlers fire, the UI freezes. Toggles that don't toggle, modals that don't open, a counter stuck at 0. Everything logs correctly.
import { signal, useSignals } from '@cascivo/core'
const count = signal(0) // shared, module-level — assigning its .value is fine
// ✗ Broken — reads count.value during render without subscribing, never re-renders.
function Counter() {
return <Button onClick={() => { count.value++ }}>Clicked {count.value} times</Button>
}
// ✓ Correct — useSignals() first, before anything else in the body.
function Counter() {
useSignals()
return <Button onClick={() => { count.value++ }}>Clicked {count.value} times</Button>
}
useSignals() is a no-op where a transform is already active (Preact apps, or React with the
Babel plugin), so adding it is always safe. You do not need it to pass signals into a
cascivo component, in an event handler, or inside useSignalEffect — only for a read that
happens during render.
Full reactivity model, including which React hooks map to which cascivo primitive: HEADLESS.md. Symptom-first version: TROUBLESHOOTING.md.
Where to go next
- USING-WITH-A-ROUTER.md — any router (React Router,
TanStack Router, Next.js). cascivo links come in two kinds wired two different ways:
setLinkComponentfor the config-driven navs (SideNav, ShellHeader, Breadcrumb) and<Link asChild>for links you write in page content. Read this before writing any link. - RECIPE-DASHBOARD.md — building a console/dashboard page (project switcher, cards, KPIs, sparklines/charts): the exact component for each need, plus pre-built blocks and reference apps.
- HEADLESS.md — the reactivity model: state with signals (not
useState/useEffect/useContext), the behavior primitives, and the "React hook → cascivo primitive" mapping. Read this before writing any state. - ENTERPRISE-READINESS.md — signal↔state, layout, theming, signal lifecycles, typed tokens, and forms, each mapped to its shipped primitive with code.
- THEMING.md — brand it: token tiers, the
data-themespecificity footgun, a starter brand theme. - TOKENS.md — every
--cascivo-*custom property. - AI-RULES.md — house rules for your AI agent: the CSS layer contract, the reactivity contract (signals, not React hooks), and a utility-first (Tailwind) mapping table.
- USING-WITH-NEXTJS.md — App Router / RSC setup.
- USING-WITH-VITE-SSR.md — TanStack Start / Vite SSR /
Remix / workerd: the 4-line SSR checklist, and wiring your router's
<Link>into the shipped nav/shell withsetLinkComponent(theLinkComponentPropscontract). - COMPATIBILITY.md — frameworks, browsers, package version matrix.
- TESTING.md — testing signal-driven components with Vitest.
- MIGRATING-FROM-SHADCN.md — coming from shadcn/ui.
- Example apps:
apps/examples/react-vite(Vite),apps/examples/react-vite-ssr(Vite SSR),apps/examples/react-next(Next.js App Router), plus full demo dashboards underapps/examples/.
As Markdown: /docs/getting-started.md