Using cascivo with Ghost

Status: tokens and themes only. cascivo's design tokens and the twelve themes are plain CSS and work in a Ghost theme. The components do not — and cannot, without changing what a Ghost theme is. Read the next section before planning around them.


What a Ghost theme can and cannot take

Ghost themes are Handlebars templates (.hbs) compiled by Ghost's own server, which sends publication content to the browser as static HTML. There is no React renderer, no server-side JavaScript execution inside a theme, and no bundler in the default workflow — you edit .hbs and assets/, zip, upload.

@cascivo/react ships React components. There is nothing in that pipeline that can mount one, so there is no cascivo "Ghost theme" and no template that would produce one. This is a runtime mismatch, not a gap in effort.

Works in a Ghost theme
@cascivo/tokens — the three-level custom-property system✅ plain CSS
@cascivo/themes — twelve themes, data-theme switching, dark mode✅ plain CSS
@cascivo/react — every component❌ React only
Signals, @cascivo/core primitives, the FSM layer❌ React only

What you get is the design system minus the component library: one coherent color system in oklch, a type and space scale, elevation, radii, motion tokens, and twelve themes you switch with an attribute. You write the markup and the component CSS yourself, against var(--cascivo-*) values instead of hard-coded ones.

If you want the components, the honest path is headless Ghost below.


Installing the CSS

Ghost has no build step, and the shipped theme CSS uses bare @import specifiers (@import '@cascivo/tokens';) that a browser cannot resolve. So flatten once on your machine and commit the result into the theme.

1. Flatten

In a scratch directory (not the theme itself):

npm install @cascivo/themes esbuild
echo "@import '@cascivo/themes/light-dark.css';" > cascivo.css
npx esbuild cascivo.css --bundle --outfile=cascivo.flat.css

light-dark.css produces ~27 KB unminified — tokens, the base layer, and the light and dark themes, with every @import resolved and the cascade layer order intact. Add --minify if you like. For all twelve themes use @cascivo/themes/all.css instead; for one theme, import that theme's file (each self-imports the tokens it needs).

One cosmetic detail: concatenation leaves the leading @layer a, b, c; statement re-listing some names it already contains. That is harmless — a layer keeps the position of its first appearance — but if you want the canonical statement back, collapse the duplicates, as scripts/build-css.mjs does.

2. Drop it in

Copy the flattened file into the theme's asset folder:

your-theme/
├── assets/
│   └── css/
│       ├── cascivo.css      ← the flattened file
│       └── screen.css       ← your own theme CSS
├── default.hbs
├── index.hbs
└── post.hbs

In default.hbs, before your own stylesheet, and set the theme on <html>:

<!DOCTYPE html>
<html lang="{{@site.locale}}" data-theme="light">
<head>
    {{!-- cascivo first: your own CSS is unlayered and will win over it --}}
    <link rel="stylesheet" href="{{asset "css/cascivo.css"}}">
    <link rel="stylesheet" href="{{asset "css/screen.css"}}">
    {{ghost_head}}
</head>
<body class="{{body_class}}">
    {{{body}}}
    {{ghost_foot}}
</body>
</html>

Then style against the tokens:

.post-card {
  background: var(--cascivo-color-surface);
  color: var(--cascivo-color-text);
  border-radius: var(--cascivo-radius-lg);
  padding: var(--cascivo-space-6);
}

Switching themes

Every theme is scoped to a data-theme attribute — there is no OS-preference media query in the CSS, so nothing switches on its own. Set the attribute:

<html data-theme="dark">

To follow the reader's OS setting, set it before first paint so there is no flash:

<script>
  document.documentElement.dataset.theme =
    matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
</script>

data-theme works on any element, not just <html> — scope a single card or section to a different theme by setting it there.

Layering with your own CSS

cascivo ships everything inside @layer blocks, and unlayered CSS beats every layer regardless of specificity. Your theme's own CSS is unlayered, so it already wins — which is what you want. Two consequences worth knowing:

Updating

Re-run the flatten step and replace assets/css/cascivo.css. Nothing else in the theme changes — you are consuming custom properties, and the token names are stable across the 1.x line.


Headless Ghost

If the components are what you actually want, run Ghost headless: keep it as the editorial backend and read the Content API from a React front end. That is not a special integration — it is the ordinary react-vite or react-next setup with Ghost as the data source, so everything in GETTING-STARTED.md applies unchanged, and you get the whole catalog.

The trade is Ghost's built-in theming, Portal, and the admin preview — you are building the front end yourself.

npx cascivo create my-blog                     # Vite + React
npx cascivo create my-blog --framework astro   # or Astro, for content-heavy sites

Astro is often the better fit here: Ghost content is mostly static, and cascivo components used with no client directive render to HTML with zero JavaScript. See USING-WITH-ASTRO.md.


A working theme

Everything on this page is implemented as a real theme in apps/examples/ghost-theme — templates, the flatten step, and a stylesheet whose every value is a --cascivo-* token. Copy it as a starting point:

apps/examples/ghost-theme/
├── scripts/build-css.mjs   # the flatten step, as a script
├── scripts/check-theme.mjs # the assertions below
└── theme/                  # the Ghost theme itself — this is what you zip and upload
    ├── package.json        # Ghost's theme manifest
    ├── default.hbs  index.hbs  post.hbs
    └── assets/css/{cascivo.css (generated), screen.css}

Verification status

pnpm --filter @cascivo/example-ghost-theme run check runs in CI and asserts:

Each of those fails when deliberately broken; that was checked rather than assumed.

Not covered: nothing here runs Ghost and renders a page. There is no headless Ghost in CI, so the templates are a validated recipe, not a rendered one. Theme structure, {{asset}}, and the absence of a build step come from Ghost's own documentation.

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

← All guides