@themvora/cli
`themvora init`
Prerequisites
- Node.js 22+
- Project with @themvora/cli installed or npx access
Create a project, or integrate Themvora into one you already have.
init has a different job from generate. generate turns a seed, preset or
brand into theme data and writes a file. init works on a project:
project directory
↓
detect or choose framework
↓
choose a theme source
↓
scaffold (a new project) or integrate (an existing one)
↓
install the Themvora packages
↓
wire the provider, passing the theme registry as props
↓
verify by running the project's own build and typecheckThe goal is not to write a configuration file. The goal is:
<!-- cli-command:generated:start -->Run
themvora init …and end with an actually working, minimally themed application.
Synopsis — Integrate Themvora into a project, or create one
Usage:
themvora init [dir] [options]| Argument | Description |
|---|---|
<dir> | Project directory. Created when it does not exist. Defaults to the current directory. (optional) |
Options
| Option | Description | Default |
|---|---|---|
--framework <id> | Target framework. Required for a new project; overrides detection in an existing one. Run themvora list frameworks for the available values. | — |
--preset <id> | Generate the theme from a canonical preset. Run themvora list presets for the available values. | — |
--brand <id> | Generate the theme from a brand-inspired recipe. Run themvora list brands for the available values. | — |
--seed <color> | Generate the theme from a seed colour. | — |
--styling <engine> | Styling engine for the generated app: css (plain semantic stylesheet) or tailwind (Tailwind v4 via @themvora/tailwind). | css |
--package-manager <name> | Package manager to use. Defaults to detection from the lockfile. One of: npm, pnpm, yarn, bun. | — |
--example | Accepted and ignored. The showcase is always generated; kept so older scripts do not break. | — |
--plan | Print what would happen and exit. Never writes or installs. | — |
--dry-run | Print the file operations and exit. Never writes, installs or scaffolds. | — |
--no-install | Skip dependency installation. Build/typecheck verification is then skipped and reported as skipped, not failed, because it needs those dependencies. | — |
--force | Replace files that exist and were not generated by themvora init. | — |
--json | Emit the plan or the report as JSON on stdout. | — |
--quiet | Suppress progress output. | — |
Exit codes
| Code | Meaning |
|---|---|
0 | the project was created or integrated and verified |
3 | the project was written but a verification step failed |
1 | command/runtime error, or existing files block the run |
2 | usage/argument error |
Examples
themvora init my-app --framework react
themvora init my-app --framework next --preset mint
themvora init my-app --framework react --plan
themvora init my-app --framework react --no-install<!-- cli-command:generated:end -->Generated from
packages/cli/src/command-schema.json— the same contractthemvora --helprenders.
Supported frameworks
themvora init my-app --framework react
themvora init my-app --framework next
themvora init my-app --framework vue
themvora init my-app --framework svelte
themvora init my-app --framework solidinit currently scaffolds and integrates React + Vite, Next.js App Router,
Vue 3 + Vite, Svelte 5 + Vite, and Solid + Vite. Themvora may support
additional frameworks at the package level — Angular, Nuxt, Astro and Remix are
fully supported by their own packages and documented — but that does not mean
the CLI scaffolder supports every one of them yet.
| Framework | Target | Build system | Rendering |
|---|---|---|---|
| React | React + Vite | Vite | Client |
| Next.js | App Router | Next | Server |
| Vue | Vue 3 + Vite | Vite | Client |
| Svelte | Svelte 5 + Vite | Vite | Client |
| Solid | Solid + Vite | Vite | Client |
themvora list frameworks prints the same set, from the same registry. Asking for
a published-but-unsupported framework says so explicitly rather than reporting an
unknown name:
themvora init my-app --framework nuxt
# "nuxt" is a supported Themvora integration but is not an `init` target yet
# — init supports: react, next, vue, svelte, solidA new project
themvora init my-app --framework reactThe project is created with the framework's own generator — create-vite or
create-next-app — and Themvora then integrates into the result. Themvora does
not ship a copy of anyone's starter template, because such a copy is stale the
moment that framework changes its template.
After scaffolding, the integration is computed from the generator's real output
rather than from an assumption about it, and the project is verified by running
its own build and typecheck scripts.
An existing project
themvora init . --framework reactThe framework is detected from package.json; --framework overrides detection.
Two properties matter more than the features here:
- Your files are not replaced. A file that is wholly Themvora's — the theme
module and the showcase components — is written only when it does not exist.
If you already have one of your own,
initstops and asks for--force. The application root is a special case: in a project Themvora scaffolded it is replaced, and in one you already had it is left alone entirely. - Running it twice does nothing the second time. Re-running
initon an integrated project reports0 created · 0 updatedand leaves the tree byte-identical. That is asserted by the test suite, not assumed.
When a file is not in a shape init recognises — a root element it cannot match,
a plugins array written unusually — it does not guess. It reports the exact
snippet to add, under manual steps.
Theme sources
| Flag | Result |
|---|---|
| (none) | Core's built-in neutral light / dark pair |
--preset <id> | A canonical preset from the catalog |
--brand <id> | A brand-inspired recipe from the catalog |
--seed <color> | A theme generated from the seed |
themvora init my-app --framework react --preset mint
themvora init my-app --framework react --brand apple
themvora init my-app --framework react --seed "#3366ff"Presets and brands are read from the same catalog the docs pages render, so the
theme written into your project is byte-for-byte the theme the docs show. A seed
goes through generateTheme. The resolved theme is validated before anything
is written.
The generated project
The generated application is the Themvora showcase — the same interactive reference the docs site renders, not a placeholder:
Themvora
Theming that survives contact with a real app
[Read the docs] [Next.js guide] [Custom themes] [Playground]
Mode Light Dark System
Preset family [mint] [themvora] [citrus] [cocoa] [berry] …
[Toggle light / dark]
┌─ Theme controls ────────────────┐
│ live preview · swatches · tokens │
└──────────────────────────────────┘
┌─ Scoped to cocoa ───────────────┐
│ a subtree pinned to one family │
└───────────────────────────────────┘Every colour comes from a semantic token. The generated stylesheet contains no literal colour, and the generated components contain none either — which is what makes a theme switch visibly real, and what makes the showcase a correct reference for the semantic system rather than a decorative demo.
The showcase UI is its own component rather than living in the application root, and there is exactly one provider boundary:
src/
├── main.tsx mounts the app; the provider boundary lives here
├── App.tsx composes the showcase
├── index.css the generated stylesheet
├── components/
│ ├── ThemvoraDefault.tsx the showcase UI
│ └── ThemeSwitcher.tsx the light/dark control
└── themes/
└── themes.ts the theme moduleEach framework uses its own conventions for the same split:
| Framework | Provider | Application root | Showcase | Theme module | Stylesheet |
|---|---|---|---|---|---|
| React | src/main.tsx | src/App.tsx | src/components/ | src/themes/themes.ts | src/index.css |
| Next.js | app/layout.tsx | app/page.tsx | components/ | themes/themes.ts | app/globals.css |
| Vue | src/App.vue | src/App.vue | src/components/ | src/themes/themes.ts | src/style.css |
| Svelte | src/App.svelte | src/App.svelte | src/components/ | src/themes/themes.ts | src/app.css |
| Solid | src/App.tsx | src/App.tsx | src/components/ | src/themes/themes.ts | src/index.css |
The app opens in a light theme. The provider receives initialMode="light"
and the source's light theme by name, so a first run looks the same for every
visitor instead of following the operating system. A selection the user has
already made still wins, as the runtime's persistence contract requires.
The theme module
themes/themes.ts is the one file that names colours, and it is generated from
canonical core APIs rather than copied from a template. It exports:
| Export | What it is |
|---|---|
themvora | the custom themvora light/dark pair |
selection | the families the app offers — the source's own, plus the four demo families |
presetThemes | getPresetThemes() filtered by selection — the canonical presets, referenced rather than copied |
sourceThemes | themes the catalog cannot serve (a brand, a seed, the neutral pair), written out |
themes | everything the application registers |
defaultTheme | the theme the app opens with, typed against the registry's own names |
scopedFamily / scopedThemes | the family the scoped demo pins to, resolved from the registry rather than hardcoded |
The shadows group is deliberately absent from the custom pair: shadow tokens
are CSS box-shadow values, so a bare colour would emit an unusable
--theme-shadow-sm. Empty optional groups are omitted for the same reason.
--example
The showcase is the starter. There is no minimal variant to opt out of, so
--example changes nothing about what is written. The flag is still accepted so
that scripts written against an earlier release keep working.
Styling
themvora init my-app --framework react --styling css
themvora init my-app --framework react --styling tailwind--styling chooses the styling engine for the generated application. Plain CSS
is the default, and the default in a non-interactive run.
css writes one global stylesheet that reads Themvora's semantic custom
properties (var(--theme-color-background), var(--theme-color-card),
var(--theme-color-primary), …). It needs no extra dependency.
tailwind writes a Tailwind CSS v4 entry point that imports tailwindcss
and then @themvora/tailwind, whose @theme mapping turns the same tokens into
utilities — bg-background, text-foreground, bg-card, border-border,
bg-primary. It installs @themvora/tailwind and, as dev dependencies,
tailwindcss and the framework's Tailwind plugin (@tailwindcss/vite, or
@tailwindcss/postcss for Next.js), and registers the plugin in the build
configuration.
Either way the showcase contains one styling system: no :root palette, no
prefers-color-scheme block, no literal colour in a component. Switching themes
repaints the whole showcase in both, including the page itself — the document
background and text come from the theme, not from the browser default.
The two variants do not produce the same markup with a different stylesheet
bolted on. The css variant renders component classes (class="card") that the
generated stylesheet defines from semantic variables. The tailwind variant
renders Tailwind utilities (class="grid gap-4 rounded-xl border border-border bg-card p-6") and defines no component classes at all — its stylesheet is
the Tailwind entry point plus a body rule.
--styling is deliberately not called --style: --style already means a theme
generation style (themvora generate --style minimal), which is a different
concern.
A fresh project versus an existing one
The two workflows are deliberately different.
For a project init scaffolded, the CLI owns the generated structure. It writes
the showcase, and it removes the scaffold's own unused files — the demo
stylesheet that declares a second palette, the demo component, the logo assets
the generated UI never renders. A file is removed only when it is a known
artifact of that framework's template and nothing left in the project refers to
it, so a dangling import is never produced.
For an existing project, none of that happens. The CLI adds its own files and
reports anything it cannot do safely as a manual step with the exact snippet.
Your App, your stylesheets and your build configuration are left alone —
including any Tailwind setup you already have. Nothing registers a Themvora
plugin in your build: initialization is provider props.
The plan and dry run
themvora init my-app --framework react --plan
themvora init my-app --framework react --dry-runBoth are built from the same plan object a real run applies, so the preview
cannot drift from the outcome. Neither writes a file, installs a package or
invokes a scaffold — --dry-run never runs an external generator.
For a new project the preview is computed before the generator has run, so the files Themvora owns are exact while the edits to the generator's own files are resolved afterwards. The plan says so.
Options
| Flag | Effect |
|---|---|
--no-install | Skip installing the Themvora packages |
--package-manager <name> | Force npm, pnpm, yarn or bun |
--force | Replace files that exist and were not generated by init |
--json | Emit the plan or the report as JSON |
--quiet | Suppress progress output |
The package manager is detected from the lockfile, and the corepack
packageManager field wins over a lockfile because it is an explicit
declaration. Several lockfiles is reported as a warning rather than silently
resolved.
What is wired
Initialization is provider props. There is no configuration file and no build plugin:
themes/themes.ts
↓
<ThemeProvider themes={themes} defaultTheme={...} initialMode="light" transition={...}>
↓
pre-paint bootstrapOne module, one consumer. The provider paints <html> before the first paint —
its own contract, so a client-rendered app needs neither themvoraVitePlugin()
nor a hand-rolled pre-paint script. React uses the same path; createThemeRoot
remains the documented entry for server rendering.
Next.js follows the same shape. @themvora/next's ThemeProvider renders
<html> and <body> itself and resolves the initial theme on the server, so
there is no Vite plugin either, and the registry is passed to the provider as a
prop. Client hooks come from @themvora/next/client, never from the package
root.
Next.js 16 and Cache Components
create-next-app now enables Cache Components by default, and the generated
layout needs nothing extra — in particular no export const instant = false.
That mode rejects a route that reads request data (cookies(), headers())
outside a <Suspense> boundary, and Next's remedy for such a route is the
instant = false opt-out. @themvora/next's provider keeps that read inside a
boundary instead: the <html>/<head>/<body> shell resolves from the
configured default alone, and the persisted selection is resolved in a nested
server component beneath <Suspense>. The route therefore prerenders as a static
shell with a dynamic, request-aware hole, so there is nothing to opt out of.
The visitor's theme still reaches the first paint through two mechanisms that
agree with each other: the blocking bootstrap script in <head> reads the theme
cookies in the browser and writes the resolved theme onto <html> before the
first frame, and the Suspense-gated server component resolves the same cookies
for the client runtime and the streamed, server-rendered subtree.
init leaves your next.config.ts alone either way.
Exit codes
| Code | Meaning |
|---|---|
0 | The project was created or integrated, and verified |
3 | The project was written, but a verification step failed |
1 | Command or runtime error, or existing files block the run |
2 | Usage or argument error |
A verification failure exits 3 rather than 0 deliberately: init claims to
produce a working application, so a project that does not build is not a
success.
Safety
init modifies your project, so the guarantees are explicit:
- Nothing is derived from an untrusted value into a command. The one piece of user input that reaches a child process — the scaffolded directory name — is validated against a conservative character set first.
- Every generated path is resolved and checked against the project root, so no registry value or theme name can write outside the project.
- Commands are passed as argument arrays. A shell is used only on Windows, where
the package managers are
.cmdshims that cannot be launched otherwise. - The exact command being run is printed before it runs.
- No remote template repository is ever downloaded by Themvora's own logic; scaffolding is delegated to the ecosystem's official generator.