Skip to main content

Theming and white-labeling

The CCX UI reads every colour it draws from a CSS custom property. To re-brand the UI you publish a small stylesheet that restates the properties you want to change, and point FE_EXTERNAL_CSS_URL at it. No application changes are needed, and a complete brand theme is typically 30–50 lines of CSS.

ccx:
services:
uiapp:
env:
FE_EXTERNAL_CSS_URL: "https://example.com/branding/acme.css"
FE_REACT_APP_LOGO_URL: "https://example.com/branding/logo.svg"
FE_REACT_APP_FAVICON_URL: "https://example.com/branding/favicon.png"

How the stylesheet is loaded​

The URL is loaded as an ordinary <link rel="stylesheet"> and appended last in the document head when the application starts. Two consequences are worth knowing:

  • Your file needs no CORS headers. It is linked, not fetched, so any static host (S3, a CDN, your own web server) works as-is.
  • You never need !important. The application's own styles live in CSS cascade layers; your stylesheet does not. Unlayered author styles win over layered ones regardless of selector specificity, so a plain declaration in your file always takes effect.

If CCX is served over HTTPS, the stylesheet must also be served over HTTPS — browsers block mixed content and the file will silently fail to load.

The supported surface: theme tokens​

These custom properties are the supported customisation surface. Everything in the UI derives its colour from them.

TokenLight defaultDark defaultControls
--background#ffffff#171717Page background
--foreground#171717#fafafaDefault text colour
--card#ffffff#262626Card and panel background
--card-foreground#171717#fafafaText on cards
--popover#ffffff#262626Dropdown and popover background
--popover-foreground#171717#fafafaText in dropdowns and popovers
--primary#2b00ad#7c3aedPrimary buttons, links, active states
--primary-foreground#fafafa#fafafaText on primary surfaces
--secondary#f5f5f5#3a3a3aSecondary buttons
--secondary-foreground#262626#fafafaText on secondary surfaces
--muted#f5f5f5#3a3a3aMuted surfaces
--muted-foreground#636363#a3a3a3Secondary and helper text
--accent#f5f5f5#3a3a3aHover and highlight surfaces
--accent-foreground#262626#fafafaText on hover/highlight surfaces
--destructive#e5484d#e5484dDelete actions and error states
--destructive-foreground#fafafa#fafafaText on destructive surfaces
--success#2b9a3finherits lightSuccess states
--success-foreground#fafafainherits lightText on success surfaces
--warning#e5a000inherits lightWarning states
--warning-foreground#262626inherits lightText on warning surfaces
--border#e5e5e5#404040Borders and dividers
--input#e5e5e5#404040Form control borders
--ring#2b00ad#7c3aedKeyboard focus ring
--radius0.5reminherits lightCorner radius (not a colour)
--sidebar-background#1a0055#1a0044Navigation sidebar background
--sidebar-foreground#f0f0f0#f0f0f0Navigation sidebar text
--sidebar-accent#2b00ad#3a0088Active navigation item
--sidebar-accent-foreground#fafafa#fafafaText on the active navigation item
--sidebar-border#2a0078#2d0066Sidebar dividers

Defaults are shown as hex for readability. The shipped theme declares them in oklch() with a hex fallback; your overrides may use any CSS colour syntax.

--success, --warning and --radius are not redefined for dark mode — the light value carries over. Override them in :root if you want them changed everywhere, or in :root.dark as well if dark mode needs a different value.

Light and dark mode​

Users choose light, dark, or "follow system" from the user menu, so both modes will be seen. CCX signals dark mode with a dark class on the <html> element.

Put values that are the same in both modes in :root, and dark-mode overrides in :root.dark. Note the selector: :root.dark outranks both the application's own .dark block and your own :root block, so it reliably wins in both directions. A bare .dark selector does not.

Template​

Copy this, replace the values, and delete the properties you do not need. Anything you omit keeps its CCX default.

/* ============================================================
ACME white-label theme for CCX
Loaded at runtime via FE_EXTERNAL_CSS_URL.
============================================================ */

/* --- Values shared by light and dark mode ----------------- */
:root {
--primary: #fd822c;
--primary-foreground: #ffffff;
--ring: #fd822c;

--sidebar-background: #31323d;
--sidebar-foreground: #f0f0f0;
--sidebar-accent: #fd822c;
--sidebar-accent-foreground: #ffffff;
--sidebar-border: #43444f;
}

/* --- Light mode only -------------------------------------- */
:root:not(.dark) {
--accent: #fff1e6;
--accent-foreground: #31323d;
}

/* --- Dark mode only --------------------------------------- */
:root.dark {
--primary: #fd9a52;
--primary-foreground: #1a1a1a;
--ring: #fd9a52;

--sidebar-background: #25262e;
--sidebar-border: #3a3b45;

--accent: #3a2c22;
--accent-foreground: #ffd9bd;
}

What is not supported​

The token list above is the contract. The following will break without notice and are not covered by support:

  • Targeting class names. Class names in the CCX UI are internal implementation detail. They change whenever a component is refactored, and they are not stable across releases.
  • Selectors that depend on the DOM structure — element, descendant, or positional selectors such as nav li:last-child.
  • Layout properties. Restrict your stylesheet to colours, and to --radius if needed. Setting display, position, width, margin, padding, overflow or similar will break the responsive layout, and the result is not supportable.
  • !important. It is never necessary (see How the stylesheet is loaded), and it makes problems harder to diagnose.

Migrating a stylesheet written for the previous CCX UI​

Stylesheets written for the previous CCX UI do not apply to the current one. They typically target Ant Design class names (.ant-btn, .ant-spin, .ant-breadcrumb) or hashed CSS-module class names ([class*='AppHeader_'], [class*='UserMenu_']). The current UI is built on a different component library and emits none of those class names, so such a file loads successfully but has no visible effect.

Converting one is usually straightforward: take the colours out of the old file and restate them as the tokens above. A typical old stylesheet of 60–80 lines becomes 30–40 lines of tokens, and it gains dark-mode support in the process. If you have an existing stylesheet you would like converted, send it to Severalnines support and we will produce the token version for you.

Logo and favicon​

The logo and favicon are set separately, and are not part of the stylesheet:

  • FE_REACT_APP_LOGO_URL — shown at the top of the navigation sidebar and on the login, registration and password-reset pages. An SVG scales best.
  • FE_REACT_APP_FAVICON_URL — the browser tab icon.

Both are described in Frontend customization.

Operational notes​

  • Cache-busting. Browsers and CDNs cache the stylesheet. When you change it, publish under a new filename or add a version query string (acme.css?v=3) and update FE_EXTERNAL_CSS_URL, otherwise users may keep the previous version for some time.
  • Check contrast. Each *-foreground token must remain readable against its matching background. Aim for a contrast ratio of at least 4.5:1 for text.
  • Test both modes before rolling out, by switching light and dark from the user menu.
  • Availability. If the URL is unreachable the UI still loads and renders with the default CCX theme, so an outage on your asset host degrades branding rather than breaking the application.