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
- Remove all at-rules from inside :root; only `--variable: value;` declarations are allowed.
- Move conditional styling into plain variable values or let users pick an appropriately named theme variant.
- If fonts are needed, put @font-face at the top level of actual.css (it is validated separately), never inside :root.
- 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
- Keep :root strictly to --variable declarations.
- Never paste component-level CSS (media queries, keyframes) into :root.
- Lint theme CSS for '@' inside the root block before publishing.
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Invalid font-family value for "${property}": function calls
- Invalid font src: only data: URIs are allowed in @font-face.
- Invalid font-family value for "${property}": value must not
- Invalid font-family value for "${property}": empty font name
- Invalid value "${trimmedValue}" for property "${property}".
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/185a3f1b5c0dfe5e.
Report an issue: GitHub.