Using cascivo with Preact

Short version: it works on Vite CSR. @cascivo/react runs inside a Preact app via the standard react → preact/compat alias — components render, signals update, interactions fire, with zero runtime errors. Two production migrations (a Vite + Tailwind v4 studio and a Preact 10 PWA) verified this firsthand. cascivo's bundle is ~75 KB JS under compat with no JS warnings; the signals runtime does not fight Preact.

This page documents the setup so you don't have to discover it by trial.

Scope — read this first

Being precise about what "it works" covers, because the unqualified claim cost a 2026-07-28 adopter a day (report C3):

Status
Vite CSR (@preact/preset-vite)✅ Verified. This is the configuration described below, and the one the numbers come from. Components, signals, overlays (Modal, Drawer, Popover, Tooltip, Toast, CommandMenu), DataTable, Timeline and SegmentedControl all behave identically to React. Roughly half the JS — one adopter measured 60 KB gzip against 110 KB.
SSR / prerender❔ Not verified. No SSR Preact setup is exercised in this repo or in the migrations behind this guide. It may work; nobody has checked.
Astro (@astrojs/preact({ compat: true }))❌ Known broken. Three stacked causes plus an alias-ordering conflict that cannot be worked around from cascivo's side. Fully documented in USING-WITH-ASTRO.md. Use React islands under Astro.

Everything below describes the ✅ row.


1. Alias react/react-dom to preact/compat (build)

Use @preact/preset-vite, which wires the alias for you:

// vite.config.ts
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'

export default defineConfig({
  plugins: [preact()],
})

If you alias manually instead, map all four entry points:

resolve: {
  alias: {
    react: 'preact/compat',
    'react-dom': 'preact/compat',
    'react/jsx-runtime': 'preact/jsx-runtime',
    'react-dom/test-utils': 'preact/test-utils',
  },
}

2. Map the same aliases in tsconfig (typecheck)

cascivo's .d.ts files import from "react". For the type-checker to resolve those to Preact, add paths:

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "react": ["./node_modules/preact/compat"],
      "react-dom": ["./node_modules/preact/compat"],
    },
  },
}

Both migrations were strict-mode clean (including exactOptionalPropertyTypes: true) with no any needed once small wrappers were in place.

3. Satisfy the React peer dependency

@cascivo/react (and @cascivo/core) declare react/react-dom >= 18 as peer dependencies. Under compat the alias means React is never actually bundled, but the peer still has to resolve. Install React purely to satisfy it:

pnpm add -D react react-dom

These are dev-only here — the alias replaces them at build time. (We're tracking a peerDependenciesMeta change to make this explicit; for now, install them.)

4. Signals package

@cascivo/core peer-deps @preact/signals-react. Under preact/compat you might expect to need @preact/signals — but @preact/signals-react@3 works fine under compat, and it's the package cascivo expects. Install it:

pnpm add @preact/signals-react

You do not need to add the Babel signals transform: cascivo components that read signal.value during render call useSignals() internally, so reactivity works without app-level transform configuration.


Minimal working setup

pnpm add @cascivo/react @cascivo/themes @preact/signals-react
pnpm add -D react react-dom
// entry
import '@cascivo/react/styles.css'
import '@cascivo/themes/light-dark.css'
import { Button, Card } from '@cascivo/react'

export function App() {
  return (
    <Card>
      <Button>It works under Preact</Button>
    </Card>
  )
}

That's the whole story. If something renders but never updates on a signal write, the cause is almost always a missing alias (so two different React copies are loaded) — re-check steps 1 and 2.

As Markdown: /docs/using-with-preact.md

← All guides