actualbudget/actual · error

Theme CSS contains forbidden at-rules (@import, @media, @key

Error message

Theme CSS contains forbidden at-rules (@import, @media, @keyframes, etc.). Only CSS variable declarations are allowed inside :root { ... }.

What it means

validateRootContent validates the body of the :root { ... } block in custom theme CSS. Custom themes may only declare CSS custom properties; any at-rule (@import, @media, @keyframes, @font-face nested inside :root, etc.) anywhere in the root content triggers this error, since at-rules could alter page behavior beyond theming.

Source

Thrown at packages/desktop-client/src/style/customThemes.ts:350

    }
  }

  const trimmed = content.substring(start).trim();
  if (trimmed) declarations.push(trimmed);

  return declarations;
}

// ─── :root block validation ─────────────────────────────────────────────────

/**
 * Validate the content inside a :root { ... } block.
 * Only CSS custom properties (--*) with safe values are allowed.
 */
function validateRootContent(rootContent: string): void {
  // Check for forbidden at-rules inside :root
  if (/@[a-z-]+/i.test(rootContent)) {
    throw new Error(
      'Theme CSS contains forbidden at-rules (@import, @media, @keyframes, etc.). Only CSS variable declarations are allowed inside :root { ... }.',
    );
  }

  // Check for nested blocks
  if (/\{/.test(rootContent)) {
    throw new Error(
      'Theme CSS contains nested blocks or additional selectors. Only CSS variable declarations are allowed inside :root { ... }.',
    );
  }

  for (const decl of splitDeclarations(rootContent)) {
    const colonIndex = decl.indexOf(':');
    if (colonIndex === -1) {
      throw new Error(`Invalid CSS declaration: "${decl}"`);
    }

    const property = decl.substring(0, colonIndex).trim();

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Remove all at-rules from inside :root; only `--variable: value;` declarations are allowed.
  2. Move conditional styling into plain variable values or let users pick an appropriately named theme variant.
  3. If fonts are needed, put @font-face at the top level of actual.css (it is validated separately), never inside :root.
  4. Drop any @import — remote stylesheet loading is not permitted.

Example fix

// before
:root { --color-bg: #fff; @media (prefers-color-scheme: dark) { --color-bg: #000; } }
// after
:root { --color-bg: #fff; }
Defensive patterns

Strategy: validation

Validate before calling

function rootHasNoAtRules(css) {
  const root = css.match(/:root\s*{([\s\S]*?)}/)?.[1] ?? '';
  return !/@[a-z-]+/i.test(root);
}
if (!rootHasNoAtRules(css)) throw new Error('at-rule inside :root');

Try / catch

try {
  await installTheme(css);
} catch (err) {
  if ((err as Error).message.includes('forbidden at-rules')) {
    // strip at-rules from :root or reject the theme with a clear author message
  } else throw err;
}

Prevention

When it happens

Trigger: A theme's actual.css places an at-rule inside the :root block, e.g. `:root { --color-bg: #fff; @media (prefers-color-scheme: dark) { ... } }` or an @import inside :root.

Common situations: Theme authors pasting standard web-app CSS (with @media/@supports) into :root; CSS preprocessors outputting nested at-rules; attempts to sneak @import for remote stylesheets.

Understand the failure class

Related errors


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