Skip to main content
The Members Portal uses Bootstrap 5.3 with SCSS customisation, CSS Modules for component-scoped styles, and CSS custom properties for runtime theming. This page explains how these layers work together and how to make changes at each level.

Architecture overview

Styles are compiled into a single CSS bundle at build time by Vite, and that bundle is the same for every location. Per-location colours are CSS custom properties written at runtime (see Runtime branding). The compilation order in style.scss determines which layer can override which.

Folder structure

All SCSS source files live under src/assets/scss/:

Default colour palette

Default colours are defined in _defaults.scss using the !default flag, which means they can be overridden by any value set before the import:
_variables.scss then derives contrast colours, subtle variants, and border colours from these base values and exports them as a $theme-colors map. Bootstrap uses this map to generate CSS custom properties like --bs-primary, --bs-primary-bg-subtle, etc. These defaults are what the bundle is compiled with; each location’s own values replace them in the browser (see Runtime branding), so src/theme/tokens.ts keeps a copy of them and a test fails if the two drift.

How components are styled

Components use one of three patterns (often combined):

CSS Modules (scoped styles)

Component-specific styles live in a .module.scss file next to the component. Class names are locally scoped at build time so they never leak.
Use CSS custom properties inside .module.scss files to reference theme colours: color: var(--bs-primary);

Bootstrap utility classes

Most layout and spacing is handled with Bootstrap 5 utility classes directly in JSX:
The project extends Bootstrap’s utility API in custom/_utilities.scss with additional helpers such as fixed-pixel heights (h-40px), viewport heights (vh-100), and more.

Global SCSS (unscoped)

Some components import a plain .scss file for global styles. These affect the entire page and are typically used for animation keyframes or third-party library overrides:
Prefer CSS Modules over global imports for new components. Global styles can cause unintended side-effects.

Runtime branding (per-location colours)

Each location sets its brand colours in the Nexudus dashboard (Theme.PrimaryColor, Theme.SecondaryColor, Theme.Success, Theme.Info, Theme.Warning, Theme.Alert, and their dark-mode twins). The stylesheet is compiled once, at build time, and is identical for every location. Everything that depends on the palette is a CSS custom property, and the browser fills the values in:
  1. LocationSettingsProvider fetches /api/sys/businesses/{id}/colors?mode=light|dark for the effective colour mode.
  2. ThemeTokens (src/theme/ThemeTokens.tsx) computes the tokens with src/theme/tokens.ts and writes them into a <style id="nexudus-theme-tokens"> at the end of <head>, where they win over the static bundle. That takes about a millisecond, so theme toggles and editor previews (?Theme.PrimaryColor=...) apply instantly.
  3. index.html carries a theme-fallback block with the default primary colour for the splash screen. ThemeTokens removes it once the real tokens are in.
There is no server-side compilation, no Blob storage and no CSS version constant. Shipping a stylesheet change is just a deploy: the bundle is a hashed, immutable Vite asset.

Where a branded value can live

src/theme/tokens.ts is the runtime twin of _branding.scss: it computes the same token names with the same formulas, using the Bootstrap colour functions ported in src/theme/color.ts.

Token names

For a theme colour <c> (primary, secondary, success, info, warning, danger, plus the derived $theme-colors entries such as primary-contrast and primary-bg-subtle):

Using a branded colour

Never write $primary (or any tenant colour variable, or a Sass function of one) into a rule. That bakes the default palette into the bundle for every location. Use the token:
If you need a mix that does not exist yet (say tint-color($primary, 25%)), add a token rather than computing it in Sass.

Adding a new token

1

Define it for the default palette in _branding.scss

Add the formula to the :root block (and to the color-mode(dark) block if it has a dark-mode value):
2

Compute it at runtime in src/theme/tokens.ts

Add the same formula to buildThemeTokens, under the same name, in light (and dark if applicable):
src/theme/color.ts has tint, shade, shift, mix, colorContrast, toHex and toRgbTriplet, all ports of the Sass functions Bootstrap uses.
3

Use it

var(--bs-primary-tint-25) in any stylesheet or component.
4

Run the theme tests

tokens.test.ts compiles the real SCSS with several palettes and fails if a TypeScript value differs from the Sass one. branding.test.ts fails if anything in the bundle still bakes a tenant colour (it names the selector and property), if a runtime token has no static twin, or if a runtime rule targets a selector the bundle does not contain.

Making a Bootstrap variable follow the tenant colour

If a Bootstrap variable such as $pagination-hover-bg is only interpolated into CSS, set it to a token in _variables.scss: $pagination-hover-bg: var(--#{$prefix}primary);. If Bootstrap runs tint-color, shade-color, mix or color-contrast on it (buttons, tables, coloured links), leave the variable alone and re-declare the generated rule in _branding.scss on tokens, the way .btn-#{$name} is done. The bundle guard tells you which case you are in: it lists the compiled rule that still changes with the palette.

Colours embedded in SVG data URIs

var() does not work inside url(). Bootstrap embeds $success and $danger in the validation icons and the primary emphasis colour in the accordion chevron. Those are emitted as whole rules by buildThemeTokens (the rules array) with the same selector as Bootstrap’s rule. Add to that list if you introduce another one.

Border radius

Each location also picks a corner style with the Theme.BorderRadius setting (editor previews can pass ?Theme.BorderRadius=...). It takes one of three presets; empty or unknown values mean Default: _variables.scss derives the scale from $border-radius and seeds the custom properties with the Default preset; ThemeTokens emits the location’s preset from src/theme/radius.ts alongside the colours. Component variables ($btn-border-radius, $input-border-radius, $card-border-radius, …) point at the tokens, so Bootstrap components follow the preset on their own. .btn-close and kbd bake $border-radius through a mixin default and are re-declared in _branding.scss. Never write $border-radius, $border-radius-lg & co. into a rule: use var(--bs-border-radius), var(--bs-border-radius-lg), or the rounded-* utilities. tokens.test.ts compiles the SCSS with each preset and checks radius.ts against it, and the bundle guard in branding.test.ts names any rule that still bakes a radius.

Dark mode

Dark mode is controlled via the data-bs-theme attribute on the <html> element. The _dark-mode.scss file redefines CSS custom properties when this attribute is set:
Theme switching is managed by the useLayoutContext hook:
The preference is saved to localStorage and restored on load.
When writing component styles, always use CSS custom properties (var(--bs-body-bg)) instead of hard-coded colour values. This ensures your styles work in both light and dark modes.

Extending Bootstrap utilities

Custom utility classes are registered in custom/_utilities.scss using Bootstrap’s utility API. To add a new utility:
This generates classes like .custom-opacity-25, .custom-opacity-50, etc. with responsive variants if you add responsive: true.

Overriding Bootstrap component styles

Bootstrap component customisations live in custom/ as partial SCSS files (e.g., _buttons.scss, _card.scss). These are imported after Bootstrap core, so they can override default styles. To adjust a Bootstrap component:
1

Find the right partial

Look in src/assets/scss/custom/ for an existing file that matches the component (e.g., _buttons.scss for buttons).
2

Add your overrides

Write your SCSS rules in that file. You can use Bootstrap variables and mixins:
3

Verify the import

Make sure the file is imported in style.scss. All existing partials are already imported.

Adding styles to a new component

1

Create a CSS Module file

Add a .module.scss file next to your component:
2

Write scoped styles

3

Import and use in the component

Combine CSS Modules for component-specific layout with Bootstrap utilities for spacing and typography. This keeps styles scoped while reusing the design system.

The _user.scss file

_user.scss is imported last in style.scss and is intended for project-level custom rules that don’t belong to a specific component or Bootstrap override. It currently contains helpers like .sticky-top-20, .no-select, and .disabled-component. Add rules here when you need a global utility that doesn’t fit into Bootstrap’s utility API or a component module.

Key conventions