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

  1. Wrap all variable definitions in a ':root { ... }' block at the start of the CSS
  2. If the CSS is empty, add a minimal ':root { --color-bg: #ffffff; }' or cancel the save
  3. Remove or relocate other selectors/at-rules — they are not supported in theme CSS
  4. 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

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


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/544db72b51cbd8f3. Report an issue: GitHub.