actualbudget/actual · error · Error
Theme CSS must contain :root { ... } with CSS variable defin
Error message
Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed. What it means
validateThemeCss strips comments, extracts optional @font-face blocks, and then requires the remaining CSS to start exactly with ':root {'. This error is thrown when the remaining content does not begin with a :root block — including when there is no content at all, or a selector other than :root appears first. The theme system deliberately accepts nothing besides optional @font-face blocks and a single :root variable block.
Source
Thrown at packages/desktop-client/src/style/customThemes.ts:443
* @font-face blocks must appear before :root.
* Returns the validated CSS or throws an error.
*/
export function validateThemeCss(css: string): string {
// Strip multi-line comments before validation
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, '').trim();
// Extract @font-face blocks (if any) from the CSS
const { fontFaceBlocks, remaining } = extractFontFaceBlocks(cleaned);
// Validate @font-face blocks (reject remote URLs, enforce size limits)
validateFontFaceBlocks(fontFaceBlocks);
// Now validate the remaining CSS (should be exactly :root { ... })
const rootMatch = remaining.match(/^:root\s*\{/);
if (!rootMatch) {
// If there are @font-face blocks but no :root, that's an error
// If there's nothing at all, that's also an error
throw new Error(
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
);
}
const rootStart = remaining.indexOf(':root');
const openBrace = remaining.indexOf('{', rootStart);
if (openBrace === -1) {
throw new Error(
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
);
}
const closeBrace = remaining.indexOf('}', openBrace + 1);
if (closeBrace === -1) {
throw new Error(
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',View on GitHub (pinned to d4334cb6e6)
Solutions
- Wrap all variable definitions in a ':root { ... }' block at the start of the CSS
- If the CSS is empty, add a minimal ':root { --color-bg: #ffffff; }' or cancel the save
- Remove or relocate other selectors/at-rules — they are not supported in theme CSS
- Fix typos in the selector: it must be exactly ':root' followed by optional whitespace and '{'
Example fix
// before
body {
--color-bg: #fff;
}
// after
:root {
--color-bg: #fff;
} Defensive patterns
Strategy: validation
Validate before calling
function startsWithRootBlock(css: string): boolean {
return /^:root\s*\{[\s\S]*\}\s*$/.test(css.replace(/\/\*[\s\S]*?\*\//g, '').trim());
} Type guard
null
Try / catch
try {
const validated = validateThemeCss(userCss);
} catch (e) {
if (e instanceof Error && e.message.includes('must contain :root')) {
showError('Theme CSS must be a single :root { ... } block (optionally preceded by @font-face blocks).');
}
} Prevention
- Always wrap theme variables in a leading :root { ... } block
- Never save an empty theme — require at least one variable
- Strip other selectors/at-rules before submitting theme CSS
- Verify the exact ':root' spelling (no space between : and root)
When it happens
Trigger: Calling validateThemeCss('') (empty string), CSS starting with another selector like 'body { ... }' or '.theme { ... }', CSS starting with an at-rule (@media, @import), or a typo like ': root {' / ':Root {' that fails the /^:root\s*\{/ match.
Common situations: Pasting an entire stylesheet instead of only variable overrides, saving an empty theme, migrating from an older theme format that allowed full CSS, or typos in the :root selector.
Related errors
- Invalid CSS declaration: "${decl}"
- Invalid property "${property}". Only CSS custom properties (
- Invalid property "${property}". Property name cannot be empt
- Invalid property "${property}". Property name cannot be empt
- Invalid property "${property}". Property name contains inval
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/544db72b51cbd8f3.
Report an issue: GitHub.