Skip to content

CSS, JS and assets

There is only one rule for theme assets: CPalius does not compile theme assets. Tailwind, Sass, Vite, esbuild or plain CSS; use whichever you like. You put the output under Resources/dist/ and declare it in theme.json. The system serves only the compiled file.

theme/
  src/              your own source (does not need to go into the package)
  package.json      your own build tools (optional, only on your machine)
  Resources/
    dist/
      style.css     ← compiled output
      main.js
      fonts/  img/  files the CSS calls with url()

Serving files: cp_theme_url

<link rel="stylesheet" href="{{ cp_theme_url('dist/style.css') }}">
<script{{ csp_nonce_attr() }} src="{{ cp_theme_url('dist/main.js') }}"></script>

cp_theme_url does the following:

  1. Validates the path. Only paths under dist/ or assets/ with [A-Za-z0-9._/-] characters are accepted; .. is rejected. Anything else returns empty.
  2. Finds the file. First in the active theme, then in other installed themes. In a partial theme, if the file is not with you, it comes from the default theme.
  3. Copies. On first use it copies under public/themes/<theme>/dist/...; if the source is newer it refreshes. After that the web server serves the file directly.
  4. Adds a version. The address comes back with ?v=<modification time>: when the file changes, the browser cache is busted automatically.

So do not copy CSS/JS by hand under public/themes/; keep it in the theme folder (Resources/dist).

Fonts and images inside CSS

Fonts and images that CSS calls with url() do not pass through cp_theme_url. That is why, when a CSS file is copied, the url() dependencies inside it are copied at the same time. A .deps marker file reduces this to the cost of a single stat() per request, without scanning the file system on every request. The dependencies must exist in the theme folder; a url() that calls a path that does not exist becomes a broken image.

public/themes and Git

public/themes/<theme>/dist/ is an output folder (produced automatically, together with *.css.deps files). Do not put these under version control; the real file is under cp-content/themes/<theme>/Resources/dist/.

No external calls

Themes do not call fonts, icons and scripts from outside (CDN). The reasons:

  • Privacy: a page that pulls a font from a third party tells that third party about every reader.
  • CSP: external domains are blocked by the content security policy.
  • Resilience: your site should not break when an external service goes down.

Put fonts and icon libraries in the theme's own dist/ folder or use the fonts the administrator installed from AACP (Menu, widgets and appearance).

CSP and scripts

  • Inline scripts/styles must carry csp_nonce_attr().
  • eval and inline event handlers (onclick="...") may be blocked; bind event listeners in the JS file.
  • If a third-party script is needed, the administrator must add it to the CSP settings (security.csp_script_src); this should not be a theme decision.

The core panel and a theme are not the same thing

Core panel (AACP/Studio) Theme
Build Symfony AssetMapper + standalone Tailwind binary Your tool, the system does not compile
Output location public/assets public/themes/<theme> (automatic copy)
Classes The Tailwind source scans core templates and module views The theme's own CSS
Node.js Not required Your choice
Module classes must work independently of your theme

Modules' admin screens are drawn with the core's Tailwind build; they do not affect your theme CSS and must not clash with it. For front-end components, modules bring their own CSS files where possible; a theme may override them but must not break them.

Local development loop

1. change in src/
2. build with your own tools         (npm run build, sass, …)
3. refresh the browser               (cp_theme_url takes the version from the modification time)

Thanks to the versioned address you do not need to clear the cache by hand. When you change a template (Twig), in the development environment the Twig cache refreshes itself; in production cache:clear is needed.

Dark / light theme

Switch with CSS variables and the data-theme attribute:

:root, [data-theme="light"] { --bg: #fff; --fg: #1f2328; }
[data-theme="dark"]         { --bg: #0d1117; --fg: #e6edf3; }

Also listen to the system preference (prefers-color-scheme) and write the choice to localStorage with try/catch (in a private tab or with cookies blocked localStorage throws). The Githeme theme applies this pattern with a single small githeme.js and offers temporary forcing with ?gh-theme=dark|light.

Generated files

If a CSS file of yours is generated from another CSS (like Githeme's githeme-recolor.css file), mark it clearly and keep the generator (tools/recolor.php) together with the theme; editing a generated file by hand is lost on the next build.

Last updated: 13.10.2026