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:

Which versions go together

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.

PathYou getChoose it when
A. Copy-paste (CLI)Component source (TSX + CSS) copied into your repoYou want to own and edit the code — the shadcn model
B. Prebuilt (@cascivo/react)A normal npm dependency, all 214 componentsYou 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, then npx 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.

SurfaceAuthoritative forReach it
The shipped .d.tsProps — 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>.mdAgent-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 siteThe 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:

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 linterWhat 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@7pnpm add -D --save-exact @cascivo/eslint-config, then spread it last
Neither / an older linternothing — 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' (also Progress, SpaceStep, RovingOrientation and the *Input/*Alias spellings). Status.status and Badge.variant are typed with these, so a Record<DeployState, Tone> is the supported way to map your domain states onto them. Do not add @cascivo/core to 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/react 0.10+ SSR works with zero Vite config — the package ships a CSS-free node-condition build that a bare server loader imports cleanly. The only SSR checklist left: @preact/signals-react 3.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 need ssr: { noExternal: [/^@cascivo\//] } (or cascivoSsr()), or an unconfigured build throws Unknown 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-entry peerVersions in registry.json, not version equality, so nothing has to move in lockstep.

Pin exact versions (no ^). Before any upgrade, run cascivo doctor --drift — it reads breaking-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; let ThemeProvider set 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.css is 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-server export 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 a tsc --noEmit step fails with TS2307: Cannot find module … (or TS2882 under noUncheckedSideEffectImports). Add Vite's ambient types once — create src/vite-env.d.ts containing /// <reference types="vite/client" /> — or, if you don't use vite/client, declare the module explicitly with declare module '*.css'. npx cascivo create writes 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:

Importdata-theme valueBase scheme
@cascivo/themes/lightlightlight
@cascivo/themes/darkdarkdark
@cascivo/themes/warmwarmlight
@cascivo/themes/flatflatlight
@cascivo/themes/minimalminimallight
@cascivo/themes/midnightmidnightdark
@cascivo/themes/pastelpastellight
@cascivo/themes/brutalistbrutalistlight
@cascivo/themes/corporatecorporatelight
@cascivo/themes/terminalterminaldark
@cascivo/themes/cyberpunkcyberpunkdark
@cascivo/themes/arcadearcadelight

@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:

ImportContainsUse when
@cascivo/themes/light-dark.csslight + darkthe common case — a light/dark toggle
@cascivo/themes/all.cssall twelveyou offer a theme picker
@cascivo/themes/<name>.cssthat one themeyou ship a single fixed theme (pair with base.css)
@cascivo/react/styles.csslight + dark, plus every component's CSSno bundler / one-file setup

Changed in 0.14.0. all.css used 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 to light-dark.css to 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).

PackageBundler (Vite/Next/webpack), SPA or SSRNo bundler / CDN
@cascivo/themesalways import a bundlesame
@cascivo/reactautomatic per component@cascivo/react/styles.css
@cascivo/chartsautomatic (dist/index.js imports ./charts.css)@cascivo/charts/styles.css

@cascivo/charts/styles.css is 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 declares sideEffects: ["**/*.css"] must import it from its entry, and ship the CSS-free node twin, and — if it also ships modules that render on the RSC server — offer a react-server condition 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:

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

As Markdown: /docs/getting-started.md

← All guides