Theming
Colours, fonts and radius are CSS variables, not config. Override them in one stylesheet, mirror two selectors, and the panel is yours.
How do you get your brand colour into the panel? Not through config/admin.ts.
That file holds the name and the logo. Everything visual is CSS.
One stylesheet
create-questpie writes an admin stylesheet and links it from the admin route.
TanStack Start puts it at src/admin.css, Next at src/app/admin/admin.css.
This is the whole file before you add anything.
@import "tailwindcss";
@import "@questpie/admin/client/styles/index.css";
@source "../node_modules/@questpie/admin/";index.css is a one-line alias for base.css, so importing either one works.
base.css carries the tokens, the Tailwind mappings and the fonts.
@source points Tailwind at the package so it keeps the classes the admin uses.
The shipped examples add a second @source one directory further up, to cover
an install that hoists the package above the app.
Override the tokens
Put your rules after the import and mirror the two selectors base.css uses.
Dark values go on :root, .dark. Light values go on .light, :root.light.
Dark is the default, so :root alone is a dark rule.
:root,
.dark {
--primary: oklch(0.78 0.18 25);
--ring: oklch(0.78 0.18 25);
--font-heading: "Caveat Brush", system-ui, sans-serif;
}
.light,
:root.light {
--primary: oklch(0.65 0.2 25);
--ring: oklch(0.65 0.2 25);
--font-heading: "Caveat Brush", system-ui, sans-serif;
}Reload. Primary buttons and focus rings carry the new hue, and headings pick up
the new family. Load the webfont yourself, up beside the imports at the top of
the file. Nothing in config/admin.ts moved. Lighten the colour in the dark
block, as above, so it still reads on a dark surface.
A plain `.light` override silently loses
base.css writes light tokens on :root.light, which is more specific than a
single class. Your .light { … } rule never wins. The pairing above matches
that specificity, so source order decides, and yours is later.
The dark block is easier. base.css writes it on :root, .dark, so a plain
:root rule of yours ties on specificity and wins on source order. Only the
light block needs the pairing.
The tokens worth knowing
There are more. base.css opens with a reference comment naming most of them.
| Token | What it moves |
|---|---|
--primary, --primary-foreground | Solid primary buttons and their text |
--ring | The focus ring |
--background, --foreground | The page under everything |
--card, --popover, --muted, --accent | Panels, floating surfaces, hovers |
--destructive, --success, --warning | The semantic states |
--sidebar, --sidebar-active-background | The left rail, themed on its own |
--radius, --surface-radius | Corner rounding, base and on cards |
--control-radius, --control-height | Inputs and buttons |
--font-sans, --font-heading | Body text, and headings |
--font-chrome, --font-mono | Buttons and labels, and code or ids |
--spacing-card, --spacing-section | Padding inside a card, gap between sections |
Light and dark
The admin manages the mode itself. AdminLayoutProvider toggles .dark or
.light on <html> and sets color-scheme to match. The default is system,
which follows the operating system.
The user menu in the sidebar footer holds a switcher with three choices: light,
dark and system. The choice persists in localStorage under
questpie:admin-theme. Pass showThemeToggle={false} to take the switcher
away.
// Sets the mode the panel opens in. The switcher can still change it.
<AdminLayoutProvider theme="dark" ... />
// Pass both and the mode is yours. The switcher writes to your state.
<AdminLayoutProvider theme={theme} setTheme={setTheme} ... />Never wrap the admin in a dark-class div
Dialogs, popovers, selects and toasts render through portals into
document.body, outside your wrapper. They follow the class on html and
come out in the opposite theme. Pass theme on the provider instead. It sets
the class at the root, where the portals can see it.
Replacing chrome
Three parts of the shell take a component of yours instead of a stylesheet. Put
the file in src/questpie/admin/components/ and codegen picks it up by name.
| File | Replaces |
|---|---|
admin-sidebar-brand.tsx | The logo and name block at the top of the rail |
admin-sidebar-nav-item.tsx | How one navigation row renders |
admin-auth-layout.tsx | The shell around login, setup and invitations |
The brand component receives name and collapsed. The nav item receives
item, isActive and collapsed. Both prop types are exported from
@questpie/admin/client, as AdminSidebarBrandProps and
AdminSidebarNavItemProps.
import type { AdminSidebarBrandProps } from "@questpie/admin/client";
export default function Brand({ name, collapsed }: AdminSidebarBrandProps) {
return <span>{collapsed ? name.slice(0, 1) : name}</span>;
}For a whole screen rather than a piece of chrome, register a view or a page instead. See Custom views.
Next
Collections and globals is the layer under the paint. Labels, icons, which columns a list shows, and how a form is arranged.
Dashboard
Nine widget types under the dashboard key of config/admin.ts. Four read a collection for you. Three need a server loader you write.
Custom pages
A page is one React file under the admin that gets its own URL. It answers to no collection, so nothing about it is generated and nothing on the server declares it.