{"record":{"id":"185a3f1b5c0dfe5e","repo":"actualbudget/actual","slug":"theme-css-contains-forbidden-at-rules-import-m","errorCode":null,"errorMessage":"Theme CSS contains forbidden at-rules (@import, @media, @keyframes, etc.). Only CSS variable declarations are allowed inside :root { ... }.","messagePattern":"Theme CSS contains forbidden at-rules \\(@import, @media, @keyframes, etc\\.\\)\\. Only CSS variable declarations are allowed inside :root (.+?)\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/desktop-client/src/style/customThemes.ts","lineNumber":350,"sourceCode":"    }\n  }\n\n  const trimmed = content.substring(start).trim();\n  if (trimmed) declarations.push(trimmed);\n\n  return declarations;\n}\n\n// ─── :root block validation ─────────────────────────────────────────────────\n\n/**\n * Validate the content inside a :root { ... } block.\n * Only CSS custom properties (--*) with safe values are allowed.\n */\nfunction validateRootContent(rootContent: string): void {\n  // Check for forbidden at-rules inside :root\n  if (/@[a-z-]+/i.test(rootContent)) {\n    throw new Error(\n      'Theme CSS contains forbidden at-rules (@import, @media, @keyframes, etc.). Only CSS variable declarations are allowed inside :root { ... }.',\n    );\n  }\n\n  // Check for nested blocks\n  if (/\\{/.test(rootContent)) {\n    throw new Error(\n      'Theme CSS contains nested blocks or additional selectors. Only CSS variable declarations are allowed inside :root { ... }.',\n    );\n  }\n\n  for (const decl of splitDeclarations(rootContent)) {\n    const colonIndex = decl.indexOf(':');\n    if (colonIndex === -1) {\n      throw new Error(`Invalid CSS declaration: \"${decl}\"`);\n    }\n\n    const property = decl.substring(0, colonIndex).trim();","sourceCodeStart":332,"sourceCodeEnd":368,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/style/customThemes.ts#L332-L368","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n:root { --color-bg: #fff; @media (prefers-color-scheme: dark) { --color-bg: #000; } }\n// after\n:root { --color-bg: #fff; }","handlingStrategy":"validation","validationCode":"function rootHasNoAtRules(css) {\n  const root = css.match(/:root\\s*{([\\s\\S]*?)}/)?.[1] ?? '';\n  return !/@[a-z-]+/i.test(root);\n}\nif (!rootHasNoAtRules(css)) throw new Error('at-rule inside :root');","typeGuard":null,"tryCatchPattern":"try {\n  await installTheme(css);\n} catch (err) {\n  if ((err as Error).message.includes('forbidden at-rules')) {\n    // strip at-rules from :root or reject the theme with a clear author message\n  } else throw err;\n}","preventionTips":["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."],"tags":["css","security","validation","at-rules"],"backgroundTag":"css-injection-attempt","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}