Pindoba ships a default theme with light and dark modes — and the entire palette is already re-themeable: the preset accepts your own color definitions and re-derives every semantic token from them.
---
import Panel from "@pindoba/astro-panel";
import Badge from "@pindoba/astro-badge";
import Button from "@pindoba/astro-button";
import { css } from "@pindoba/styled-system/css";
// The five semantic groups the default theme derives from its five base
// palettes (gray, blue, green, orange, red).
const groups = [
{ tone: "neutral", palette: "gray" },
{ tone: "primary", palette: "blue" },
{ tone: "success", palette: "green" },
{ tone: "warning", palette: "orange" },
{ tone: "danger", palette: "red" },
] as const;
const grid = css({
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 170px), 1fr))",
gap: "md",
});
const stack = css({ display: "flex", flexDirection: "column", gap: "sm" });
const row = css({
display: "flex",
flexWrap: "wrap",
gap: "xs",
alignItems: "center",
});
const name = css({
fontSize: "md",
fontWeight: "bold",
color: "neutral.text.bold",
});
const mono = css({
fontSize: "xs",
fontFamily: "mono",
color: "neutral.text.muted",
});
---
<div class={grid}>
{groups.map((g) => (
<Panel emphasis="secondary" tone={g.tone} padding="md" radius="lg">
<div class={stack}>
<span class={name}>{g.tone}</span>
<span class={mono}>from {g.palette}</span>
<div class={row}>
<Badge emphasis="primary" tone={g.tone}>
filled
</Badge>
<Badge emphasis="secondary" tone={g.tone}>
soft
</Badge>
</div>
<div>
<Button emphasis="primary" tone={g.tone} size="sm">
Action
</Button>
</div>
</div>
</Panel>
))}
</div>Color modes are controlled via data-theme-mode on the root element. All semantic tokens resolve to the correct palette automatically:
<html data-theme-mode="light">
<!-- light palette -->
</html>
Without the attribute, pages render dark (semantic tokens use base =
dark, _light as the override) — see Theme Modes
for the bootstrap that applies the user/OS preference before first paint.
Components don’t need to know which mode is active:
import { css } from "@pindoba/styled-system/css";
<div
className={css({
backgroundColor: "neutral.surface.peak",
color: "neutral.text.bold",
})}
>
Works in both modes without conditional logic
</div>;
The preset accepts your own W3C DTCG token files — the same format the devtools export — and re-derives the entire semantic layer from them: text tiers are re-solved for contrast (WCAG + APCA floors), surface bands, borders, and the accent band all regenerate from your ramps’ generator parameters.
// panda.config.ts
import { preset } from "@pindoba/panda-preset";
import light from "./tokens/light.json";
import dark from "./tokens/dark.json";
export default defineConfig({
presets: [preset({ baseColors: { light, dark } })],
});
Families you don’t override fall back to the bundled palette, so a brand
re-theme can be as small as replacing blue. The devtools are the editor
for these files: tune anchors, offsets, and semantic mappings live, then
export light.json / dark.json.
Themes are built on three layers:
Color ramps are defined in W3C DTCG format (light.json and dark.json) and converted to Panda CSS tokens at build time. Five base palettes (gray, blue, green, orange, red) generate the raw values, and five semantic groups (neutral, primary, success, warning, danger) reference them through structured sub-tokens.
Style recipes reference semantic tokens, not raw values. When a theme changes the underlying token values, all components update automatically:
// Recipes use semantic tokens
root: {
backgroundColor: "colorPalette.accent.surface.peak",
color: "colorPalette.text.contrast",
}
Page-level defaults (body background, text color, text selection, scrollbars, focus rings) are token-driven and adapt to the active theme — including elevation: shadows.* are semantic tokens with per-mode values.
A theme for users who need enhanced visibility:
Independent of color themes, Pindoba will support density variants that control spacing and sizing — relaxed (content-focused: larger padding, 48px+ touch targets) and compact (data-dense: tighter padding, 24px minimum targets). Themes and density will be independent axes; the data-density attribute does not exist yet.
primary.text.bold so your UI adapts to future themesblue.500) rather than the mode-pinned ramps (blue.light.500) — aliases flip with the mode.text.contrast on filled accent surfaces — it passes on every accent-band stop by constructionpadding: "md" will scale with density variants_light / _dark overrides — colors and shadows are both token-covered