Astro examples
Each export in @themvora/astro, with the problem it solves and the code that solves it.
@themvora/astroAstrov2.0.0StableAstro is the only integration where a themed, switchable page can ship no client framework at all. The provider renders the themed <html> on the server, <ThemeToggle /> and <ThemeFamilySelect /> are real elements with no hydration, and getThemeController() is a plain object any <script> can use.
That gives three levels you can stop at: server-rendered theming only (zero JavaScript), native controls plus a small runtime script, or a React island when a component genuinely needs React. The page is organised in that order.
1Add the integration
astro.config.mjs and put the provider around the document. themvora() finds theme.config.ts at the project root and derives the pre-paint bootstrap and the runtime configuration from it — nothing is repeated in the config file.import { defineConfig } from "astro/config";
import { themvora } from "@themvora/astro";
export default defineConfig({
integrations: [themvora()],
});import { defineThemvoraConfig, getBuiltInThemes } from "@themvora/core";
export default defineThemvoraConfig({
themes: getBuiltInThemes(),
defaultTheme: "mint-light",
});---
import Provider from "@themvora/astro/provider.astro";
import ThemeToggle from "@themvora/astro/ThemeToggle.astro";
---
<Provider>
<slot />
<ThemeToggle showMode />
</Provider>The provider must wrap <html> itself: it server-renders the themed document element from the request cookies, which it cannot do if it is nested inside one.
2Pages that ship no framework JavaScript
A themed page with zero client JavaScript
A content site wants light/dark that respects the visitor's saved choice, without shipping a framework or a runtime bundle.
provider.astrotheme.config.ts discoveryZero-flash bootstrap---
import Provider from "@themvora/astro/provider.astro";
---
<Provider>
<slot />
</Provider>The provider resolves the theme on the server from the cookies, and the bootstrap handles the one case the server cannot decide — system with a dark-preferring visitor. The page paints the persisted theme on the first frame. No client bundle is emitted unless a control is added.
Native controls, one small runtime
The page needs a working toggle and family picker, but the team does not want a framework renderer.
ThemeToggle.astroThemeFamilySelect.astrogetThemeController()---
import Provider from "@themvora/astro/provider.astro";
import ThemeToggle from "@themvora/astro/ThemeToggle.astro";
import ThemeFamilySelect from "@themvora/astro/ThemeFamilySelect.astro";
---
<Provider>
<header>
<ThemeToggle showMode />
<ThemeFamilySelect />
</header>
<slot />
</Provider>The components are real elements with no hydration, and the controller is a plain object any <script> can use — so the only JavaScript is the shared runtime. A working toggle and family picker that persist across reloads, with no React in the bundle.
Watch out: The toggle needs the runtime, so this page does ship @themvora/core. A page with no controls does not.
A theme gallery that never flashes
A gallery must show several themes at once, and applying them from a script would paint the page theme first and repaint.
ThemeScope.astroAny number per page---
import ThemeScope from "@themvora/astro/ThemeScope.astro";
const families = ["mint", "plum", "cocoa"];
---
<div class="grid grid-cols-2 gap-4">
{families.map((family) => (
<ThemeScope theme={family} class="p-4 rounded-lg">
<strong>{family}</strong>
<p class="muted">The quick brown fox</p>
</ThemeScope>
))}
</div>The scope resolves its variables in the frontmatter, so the server markup already carries them. There is nothing to repaint and no id to collide with. Every panel is correct in the first paint, and switching the page theme leaves the panels alone.
Add a React island without breaking the rest
One component needs React, but the rest of the page is native. Adding a renderer must not duplicate the runtime or change the theme on load.
@themvora/astro/clientAdding the rendererOne runtime, shared---
import ThemeToggle from "@themvora/astro/ThemeToggle.astro";
import { ThemeProviderClient } from "@themvora/astro/client";
import { ThemePanel } from "../components/ThemePanel";
---
<ThemeToggle />
<ThemeProviderClient client:load>
<ThemePanel />
</ThemeProviderClient>The island adopts the runtime the native controls created and takes its config from the transported theme.config.ts, so there is still one source of truth. The island renders server-side in the correct theme, hydrates in place, and shares the selection with the native toggle.
Watch out: Use client:load, not client:only. And put every themed React component inside the same island — separate islands hydrate in an unspecified order.
3Integration, components, islands
Astro integration
The integration decides what goes into <head> and what the runtime is configured with. All of it is derived from theme.config.ts.
themvora() in astro.config.mjstheme.config.ts discoveryConfiguration transportZero-flash bootstrapnavigation: trueNo client framework requiredthemvora() in astro.config.mjs
Registers the integration and injects the pre-paint bootstrap into every page's <head> with injectScript("head-inline").
Once, in astro.config.mjs. Without it nothing is injected and the page relies on the runtime to apply the theme after it loads.
import { defineConfig } from "astro/config";
import { themvora } from "@themvora/astro";
export default defineConfig({
integrations: [themvora({ navigation: true })],
});Every page gets the blocking bootstrap. The provider adopts what that script painted instead of starting from the default.
theme.config.ts discovery
themvora() finds theme.config.ts at the project root and derives everything from it: the pre-paint bootstrap and the runtime's configuration.
Always — it is how the integration is configured. Nothing is repeated in astro.config.mjs.
import { defineThemvoraConfig, getBuiltInThemes } from "@themvora/core";
export default defineThemvoraConfig({
themes: getBuiltInThemes(),
defaultTheme: "mint-light",
initialMode: "system",
});One catalog, one default theme, one source of truth. A page cannot disagree with the bootstrap because both read the same file.
Configuration transport
Injects the bootstrap projection as window.__THEMVORA_CONFIG__ from <head>. The island reads it instead of taking theme props.
Relevant when you write your own client script or island: the registry, default theme and initial mode are already on the page, so restating them creates a second source of truth.
<!-- Injected by the integration; you do not write this. -->
<script>
window.__THEMVORA_CONFIG__ = {
themes: [/* the projection of theme.config.ts */],
defaultTheme: "mint-light",
initialMode: "system"
};
</script>A server-rendered island and its hydrated version come from the same registry, which is what prevents a React hydration mismatch.
Zero-flash bootstrap
Reads the persisted cookies, resolves the effective mode against prefers-color-scheme, and applies the CSS variables plus data-theme before first paint.
Always, once the integration is registered. It is the whole reason the Astro setup has no flash.
The first frame is the persisted theme, including for a system visitor whose OS preference differs from the server's assumption.
navigation: true
Documents navigation: true (default)
Keeps the resolved theme across <ClientRouter /> navigations. Astro otherwise copies each incoming page's <html> attributes over the live root, which would reset the theme on every navigation.
Leave it on. It is inert without <ClientRouter />, so there is no cost to a multi-page site.
import { defineConfig } from "astro/config";
import { themvora } from "@themvora/astro";
export default defineConfig({
// Default; set it explicitly only to document the dependency on ClientRouter.
integrations: [themvora({ navigation: true })],
});Client-side navigations keep the theme instead of flashing back to the server's markup attributes.
Relatedastro-integration
No client framework required
@themvora/astro imports no React: the integration, the server helpers, <ThemeToggle /> and the browser runtime are all framework-neutral. React is an optional peer, pulled in only by an island.
Every Astro project. This is the property that lets an Astro-only site ship no framework bundle.
{
"dependencies": {
"@themvora/astro": "^2.0.0",
"astro": "^5.0.0"
}
}An Astro-only project never installs React, and its theme controls are real elements with no hydration.
Astro components (no React)
These are .astro components and plain browser APIs. None of them needs a framework renderer, and most need no client script at all.
provider.astroThemeToggle.astroThemeFamilySelect.astrodata-tv-selectThemeScope.astroThemeScrollbar.astrogetThemeController()installThemeBindings()data-tv-readoutdata-tv-pressedgetInitialThemeState()createAstroThemePersistence() / computeFingerprint()provider.astro
Server-renders the themed <html data-theme> from the request cookies. A concrete mode inlines that theme's variables; system emits both schemes as media blocks instead, because an inline variable would outrank the dark block.
Once per page, wrapping <html>. It is the server half of the whole integration.
---
import Provider from "@themvora/astro/provider.astro";
---
<Provider scrollbar>
<slot />
</Provider>The HTML arrives already themed. Add scrollbar to also hide the native scrollbar before first paint.
ThemeToggle.astro
A native Astro toggle: a real <button>, so keyboard and focus behaviour come from the platform. No island, no client:load, and no per-page initialMode / initialFamily. showMode makes its text the live mode.
The default mode control. Reach for an island only when the control must render something React owns.
---
import ThemeToggle from "@themvora/astro/ThemeToggle.astro";
---
<header>
<!-- `showMode` renders the live mode as the button text. -->
<ThemeToggle showMode />
</header>Clicking cycles the mode and the button's own text follows. The pre-paint bootstrap has already set its state, so it never renders a stale label.
ThemeFamilySelect.astro
A native <select> over the families in the registry. It renders from theme.config.ts, forwards class to the <select> itself, and needs no script of its own. A mode select is the same contract by hand: <select data-tv-select="mode">.
A settings page or a header with a family picker. Native <select> is the right control on mobile and needs no JavaScript.
---
import ThemeFamilySelect from "@themvora/astro/ThemeFamilySelect.astro";
---
<label>
Theme family
<ThemeFamilySelect class="select" />
</label>
<label>
Color mode
<!-- The same contract, written by hand. -->
<select data-tv-select="mode">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
</label>The options are the registered families, and the selected option tracks the live selection — including one restored from persistence.
data-tv-select
The selection-control contract: a change event is delegated from document, and the live selection is written back into the element's value. Every control of a kind therefore agrees, a restored selection is shown rather than the first option, and a control swapped in by navigation works without re-running a script.
Any hand-written <select> that should drive the theme. Use it instead of a per-element listener.
<select data-tv-select="mode" aria-label="Color mode">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>The control is kept in sync in both directions, and the pre-paint bootstrap writes it too — so a <select> never shows its first option on a page painted in another theme.
ThemeScope.astro
A subtree painted with a theme of its own, resolved in the frontmatter — so the server markup already carries the scoped variables and nothing repaints. Independent of the app-wide selection.
A preview panel, a pricing block in a fixed palette, or a component gallery.
---
import ThemeScope from "@themvora/astro/ThemeScope.astro";
---
<ThemeScope theme="cocoa-dark">
<h3>Always cocoa</h3>
<p>This panel keeps its own theme while the page changes.</p>
</ThemeScope>The scoped panel is correct in the first paint. It ships no client JavaScript unless a runtime already exists.
ThemeScrollbar.astro
The overlay scrollbar as a native component — no island, no client framework. It installs the app-wide runtime itself when the page has none, so the overlay is live on the first load instead of waiting for a theme change.
A custom scrollbar is part of the design. Pair it with scrollbar on provider.astro (or themvora({ scrollbar })), which is what puts the hiding class and CSS in the server-rendered HTML.
---
import Provider from "@themvora/astro/provider.astro";
import ThemeScrollbar from "@themvora/astro/ThemeScrollbar.astro";
---
<Provider scrollbar>
<slot />
<ThemeScrollbar />
</Provider>The overlay mounts without a framework bundle and follows runtime theme changes.
Relatedastro-provider
getThemeController()
The framework-neutral browser API, from @themvora/astro/runtime. Not a hook: a plain object with getMode(), setMode(), setFamily(), toggleTheme() and subscribe(), valid in any <script>. Idempotent — one runtime per document, shared with every other consumer.
A page needs to drive the theme from its own script — a keyboard shortcut, a canvas that must repaint, a control the components do not cover.
<script>
import { getThemeController } from "@themvora/astro/runtime";
const controller = getThemeController();
controller.setMode("dark");
const unsubscribe = controller.subscribe(({ mode }) => {
document.querySelector("#mode-readout").textContent = mode;
});
</script>The script shares the runtime the components use, so there is no second source of truth and no double subscription.
installThemeBindings()
Registers the delegated click / change listeners for [data-tv-toggle] and [data-tv-select] once per document, and mirrors the live selection into [data-tv-select] and [data-tv-pressed] on every change.
Your markup is hand-written and uses the attribute contract without the Themvora components. The components call it for you.
<button data-tv-toggle aria-label="Toggle theme">Theme</button>
<select data-tv-select="family" aria-label="Theme family">
<option value="mint">Mint</option>
<option value="plum">Plum</option>
</select>
<script>
import { installThemeBindings } from "@themvora/astro/runtime";
// Idempotent: safe to call on every page and after a ClientRouter navigation.
installThemeBindings();
</script>The attributes start working: a [data-tv-toggle] button toggles, a [data-tv-select] select writes back, and pressed states stay accurate.
data-tv-readout
Marks an element whose text Themvora owns: theme (the machine name), label (the theme's meta.label), mode, family, or var:--theme-color-primary.
A server-rendered label that states the theme. Without the attribute you would have to write the value from a script after load.
<p>
Theme: <strong data-tv-readout="label">…</strong>
· mode: <span data-tv-readout="mode">…</span>
· primary: <span data-tv-readout="var:--theme-color-primary">…</span>
</p>The pre-paint bootstrap patches it from <head>, before the body is parsed, and the controller keeps it in sync afterwards — so a server-rendered label is never painted and then corrected.
data-tv-pressed
Marks an element as one option of a segmented control: data-tv-pressed="mode" or "family", with data-tv-value naming the option it represents. Themvora owns its aria-pressed.
A segmented control where the selected option must be both visually and accessibly marked, and the markup is server-rendered.
<div role="group" aria-label="Color mode">
<button data-tv-pressed="mode" data-tv-value="light">Light</button>
<button data-tv-pressed="mode" data-tv-value="dark">Dark</button>
<button data-tv-pressed="mode" data-tv-value="system">System</button>
</div>
<style>
/* Themvora writes aria-pressed, so one rule covers both states. */
[data-tv-pressed][aria-pressed="true"] {
background: var(--theme-color-primary);
color: var(--theme-color-primaryForeground);
}
</style>aria-pressed is written before the first paint and kept live afterwards, so a server-rendered control never asserts a selection the page is not showing. Only aria-pressed is written, which lets a token class keyed off it keep the visual and accessible states in step.
getInitialThemeState()
Resolves the initial state from Astro.request so a client island hydrates against exactly what the document rendered. Needs the registry (config or themes) and a request, so it belongs on a server-rendered page.
You are passing the resolved state to an island or a custom script rather than letting the transported config supply it.
---
import { getInitialThemeState } from "@themvora/astro";
import config from "../../theme.config";
const initial = await getInitialThemeState({ request: Astro.request, config });
---
<p>Rendered in {initial.theme.name}.</p>The island's first render equals the server's markup, which is what prevents a hydration mismatch and the text flicker that comes with it.
Relatedastro-transportastro-client
createAstroThemePersistence() / computeFingerprint()
A cookie + localStorage persistence adapter, plus config fingerprinting so a cookie written against an older registry is rejected rather than resolved to a theme that no longer exists.
You construct a runtime yourself — a shared runtime for several islands, or a custom script — and need the same persistence contract the components use.
import { createThemeRuntime } from "@themvora/core";
import { computeFingerprint, createAstroThemePersistence } from "@themvora/astro";
import config from "../../theme.config";
const themes = config.themes;
const fingerprint = computeFingerprint(themes, config.defaultTheme);
export const runtime = createThemeRuntime({
themes,
defaultTheme: config.defaultTheme,
persistence: createAstroThemePersistence({ themes, fingerprint }),
});Selection changes survive reloads, and a registry change invalidates stale cookies instead of applying a missing theme.
Scoped themes
The scope is resolved during render, not applied by a script — which is why a scoped panel is correct in the first paint instead of flashing the page theme first.
ThemeScope props
Documents Props
theme (required), themes (only outside a provider), as (wrapper element, default div), class, id and transition. class merges with the dark marker the component adds for a dark theme.
You need a scope with a specific element type or your own classes — a <section> rather than a <div>, or a utility class set.
<ThemeScope
theme="plum-dark"
as="section"
class="rounded-lg p-6"
transition={false}
>
<h3>Plum, dark, no transition</h3>
</ThemeScope>The wrapper is the element you asked for, and Tailwind's dark: variants work inside the scope on the first paint because the marker is already in the markup.
Independent of the selection
A scope does not read or change the app-wide selection. A page can show a cocoa panel next to a mint page, and switching the page theme leaves the panel alone.
Anywhere two themes must coexist — a comparison view, a preview gallery, a marketing block.
<main class="surface">
<h1>Page theme</h1>
</main>
<ThemeScope theme="cocoa-dark">
<div class="surface">Always cocoa, whatever the page does.</div>
</ThemeScope>The panel keeps its theme through every page-level change, including a schedule transition.
No client JavaScript by default
A scope is static markup. The component's script only refreshes an existing runtime's scopes when the registry is reconfigured, and honours transition — it never creates a runtime.
A page that only displays a scoped panel. It stays a page with no client framework on it.
---
import Provider from "@themvora/astro/provider.astro";
import ThemeScope from "@themvora/astro/ThemeScope.astro";
---
<Provider>
<ThemeScope theme="cocoa-dark">
<p>Server-rendered, no client script.</p>
</ThemeScope>
</Provider>No runtime is created, so the panel costs nothing at runtime. If the page already has a runtime, the scope joins it.
Any number per page
Scopes are matched by the data-tv-scope attribute, not by an id, so the same component can be rendered as many times as the page needs.
A gallery of themes, or a page that renders a scope per data record.
---
import ThemeScope from "@themvora/astro/ThemeScope.astro";
const families = ["mint", "plum", "cocoa"];
---
<div class="grid grid-cols-2 gap-4">
{families.map((family) => (
<ThemeScope theme={family} class="p-4 rounded-lg">
<strong>{family}</strong>
<p class="muted">The quick brown fox</p>
</ThemeScope>
))}
</div>Every instance is independent and correctly resolved; there is no id collision to avoid.
Relatedastro-scope-independent
Client islands (opt-in)
Only this group depends on React. Everything above works without a renderer, and adding one is a deliberate step rather than a default.
@themvora/astro/clientNo theme props neededclient:load, not client:onlyOne island, not severalOne runtime, sharedFramework renderers stay optionalAdding the rendererClient bundle cost@themvora/astro/client
The only entry that depends on React. It exports the ThemeProviderClient island, the useTheme* hooks, the React ThemeScope and ThemeReadout.
A component genuinely needs React — an existing React widget, or a control with state the native components cannot express.
import { useTheme } from "@themvora/astro/client";
export function ThemePanel() {
const { theme, effectiveMode, toggleTheme } = useTheme();
return (
<section>
<h2>{theme.name}</h2>
<p>Showing {effectiveMode}</p>
<button onClick={toggleTheme}>Toggle</button>
</section>
);
}The island shares the page's runtime and renders with the transported config, so it does not restate the theme.
No theme props needed
The registry, default theme and initial mode come from the transported theme.config.ts. Restating initialMode / initialFamily is a second source of truth.
Always. When the props drift from the config, the island's server markup disagrees with its hydrated markup (React error #418) and the text fluctuates on every load.
---
import { ThemeProviderClient } from "@themvora/astro/client";
import { ThemePanel } from "../components/ThemePanel";
---
<!-- No initialMode / initialFamily: they come from theme.config.ts. -->
<ThemeProviderClient client:load>
<ThemePanel />
</ThemeProviderClient>One config drives the document, the bootstrap, the components and the island.
Relatedastro-transportastro-client
client:load, not client:only
Astro server-renders the island and hydrates it in place, so the panel is in the first paint and hydration only attaches handlers.
Always for a themed island. client:only renders nothing on the server, which is exactly the flash the integration exists to prevent.
<!-- Server-rendered and hydrated in place. -->
<ThemeProviderClient client:load>
<ThemePanel />
</ThemeProviderClient>
<!-- client:only would render nothing on the server — the flash this avoids. -->The panel is present in the HTML; there is no empty placeholder that fills in after the bundle loads.
Relatedastro-clientastro-bootstrap
One island, not several
Astro hydrates islands in an unspecified order, so two separate islands would race. Write one island whose tree contains both the provider and its consumers — ThemeProviderClient renders nothing itself; it is the provider half of that tree.
Any page with more than one themed React component.
---
import { ThemeProviderClient } from "@themvora/astro/client";
import { ThemePanel } from "../components/ThemePanel";
---
<!-- One island, one runtime. Everything rendered inside shares it — a second
component here would not create a second runtime. -->
<ThemeProviderClient client:load>
<ThemePanel />
</ThemeProviderClient>One provider, one hydration order, no race. Components below it in the same tree share the runtime.
One runtime, shared
ThemeProviderClient adopts a runtime that already exists — one a <ThemeToggle /> or getThemeController() script created — instead of making a second one, and leaves its destruction to the owner.
A page mixes native controls with an island. This is the normal case, not an edge case.
---
import ThemeToggle from "@themvora/astro/ThemeToggle.astro";
import { ThemeProviderClient } from "@themvora/astro/client";
import { ThemePanel } from "../components/ThemePanel";
---
<!-- The toggle creates the runtime; the island adopts it. -->
<ThemeToggle />
<ThemeProviderClient client:load>
<ThemePanel />
</ThemeProviderClient>Islands and native controls share one runtime, so a toggle in the header updates the island and vice versa.
Framework renderers stay optional
React is an optional peer dependency. Astro-only projects never install it — choosing a React island is what pulls it in.
An Astro-only site. Keeping React out of the dependency tree is the point of the framework-neutral design.
{
"dependencies": {
"@themvora/astro": "^2.0.0",
"astro": "^5.0.0"
}
}No React in node_modules and no React in the client bundle unless an island exists.
Adding the renderer
Opting in is one explicit step: install @astrojs/react and react, then add react() next to themvora() in astro.config.mjs.
The moment you add the first React island — and not before.
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import { themvora } from "@themvora/astro";
export default defineConfig({
// `react()` is what makes a React island possible; `themvora()` is unchanged.
integrations: [themvora(), react()],
});Astro can render the island server-side and hydrate it. The Astro-only path never registers a renderer, so nothing changes for pages without islands.
Client bundle cost
<ThemeToggle /> and getThemeController() pull in the theme runtime — persistence, cross-tab sync and the DOM contract all come from @themvora/core rather than a second implementation. A page that only needs the themed document, with no interactive control, ships no client JavaScript at all.
A performance decision: a marketing page can be server-themed with zero script, while a settings page pays for the runtime once.
---
import Provider from "@themvora/astro/provider.astro";
---
<!-- Themed on the server. No toggle, no controller, no client script. -->
<Provider>
<main class="surface">
<h1>Server-themed, zero JavaScript</h1>
</main>
</Provider>Pages without controls ship nothing; pages with controls ship the shared runtime, not a bespoke implementation.
Transition
The island provider accepts a transition option; the native path relies on the runtime the page already has. Per-update suppression works the same way in both.
transition
Documents transition prop
Pass transition to the island provider (@themvora/astro/client) to enable CSS transitions on theme changes.
The page has an island and should cross-fade. A page with only native controls has no provider prop to set — configure the transition on the runtime the controller creates.
---
import { ThemeProviderClient } from "@themvora/astro/client";
import { ThemePanel } from "../components/ThemePanel";
---
<ThemeProviderClient client:load transition={{ enabled: true, duration: 250 }}>
<ThemePanel />
</ThemeProviderClient>Theme changes animate with the configured duration and easing; the DOM writes still happen in one flush.
runtime.store.set(theme, { suppressTransition: true })
Applies one theme change immediately, skipping the configured animation for that update only.
A print path, a screenshot, or a programmatic reset where animating would look like a glitch.
<script>
import { getThemeController } from "@themvora/astro/runtime";
const controller = getThemeController();
// Instant: a reset should not look like a deliberate theme change.
controller.runtime.store.set(controller.runtime.themes[0], {
suppressTransition: true,
});
</script>That change is instant; later changes animate again.
4A full layout, then pick your level
The full layout
Provider with the overlay scrollbar, native controls in the header, and a slot for page content — the shape most Astro sites end up with.
---
import Provider from "@themvora/astro/provider.astro";
import ThemeToggle from "@themvora/astro/ThemeToggle.astro";
import ThemeFamilySelect from "@themvora/astro/ThemeFamilySelect.astro";
import ThemeScrollbar from "@themvora/astro/ThemeScrollbar.astro";
---
<Provider scrollbar>
<header>
<ThemeToggle showMode />
<ThemeFamilySelect />
</header>
<main>
<slot />
</main>
<ThemeScrollbar />
</Provider>Which level to stop at
The same page expressed three ways. Start at the top; move down only when the level above cannot do the job.
<Provider>
<slot />
</Provider><Provider>
<ThemeToggle showMode />
<slot />
</Provider><ThemeProviderClient client:load>
<Chart />
</ThemeProviderClient>Not available in this integration
These exports exist in other integrations but not in @themvora/astro. Listing them is more useful than inventing an example: if you need one of them here, the workaround is named.
React APIs are not reachable from .astro frontmatter. Use the server helpers (getInitialThemeState(), createAstroThemePersistence()) on the server and @themvora/astro/client inside an island.