Skip to content
angulux
guide

Theming

Components are styled at runtime from a preset, not from a stylesheet you import. That makes theming a matter of configuration — and it puts the preset on the other side of a licence boundary worth understanding.

Where the styling comes from

angulux ships no theme presets of its own. provideAngulux({ theme: { preset } }) takes a preset from @primeuix/themes — PrimeTek's package, not ours — and injects the resulting CSS at runtime. Nothing is compiled into your bundle at build time, which is why changing a preset is a configuration change rather than a rebuild of your styles.

A practical consequence worth knowing early: a component that looks wrong on this site looks wrong in your application too, because this site styles its components exactly the way yours will. The chrome around them is the only thing written by hand here.

Customising a preset

definePreset takes an existing preset and overrides the tokens you name. Everything you do not name is inherited, so a brand colour is a few lines rather than a fork of a design system.

app.config.ts
import { definePreset } from '@primeuix/themes';import Aura from '@primeuix/themes/aura';const Brand = definePreset(Aura, {    semantic: {        primary: {            50: '{indigo.50}',            500: '{indigo.500}',            900: '{indigo.900}'        }    }});provideAngulux({ theme: { preset: Brand } });

Contrast

The default light preset does not meet WCAG AA on solid-coloured components. A sweep of this site — every text node inside a demo, measured against the background actually painted behind it — found 76 failing nodes across 20 of the 51 modules that have demos. The same sweep in dark mode finds none.

This is worth stating plainly rather than quietly fixing on the site: these colours are not angulux's, and the demos here are styled exactly the way your application will be. Special-casing them would hide the problem instead of showing it to you.

SeverityDefault fillOn whiteFirst step that passes
primary#10b9812.54:1#047857 — 5.48:1
success#22c55e2.28:1#15803d — 5.02:1
info#0ea5e92.77:1#0369a1 — 5.93:1
warn#f973162.80:1#c2410c — 5.18:1
danger#ef44443.76:1#dc2626 — 4.83:1
help#a855f73.96:1#9333ea — 5.38:1

The pattern behind every row: the fill is the -500 step of a palette carrying white text. That step is built for about 3:1 — the bar for a border or a large heading, not for a label. Note that one step darker is not enough for four of the six: at -600, green still measures 3.30 and sky 4.10. Only danger and help clear AA there.

definePreset reaches these without forking anything. Overriding the primitive steps fixes the fill everywhere it is used — button, badge, progress bar, split button, toast — rather than component by component.

app.config.ts
import { definePreset } from '@primeuix/themes';import Aura from '@primeuix/themes/aura';// Every severity fill is the -500 step of a palette with white text on it, and// -500 is built for roughly 3:1 — enough for a border, short of AA for a label.// Move each one to the first step that clears 4.5:1.//// Shift 600 and 700 along with it. Overriding 500 alone leaves hover reading// the untouched 600, which is then LIGHTER than the resting state and still// below AA — emerald.600 is 3.77:1.const Accessible = definePreset(Aura, {    primitive: {        emerald: { 500: '#047857', 600: '#065f46', 700: '#064e3b' }, // primary  2.54 -> 5.48        green: { 500: '#15803d', 600: '#166534', 700: '#14532d' },   // success  2.28 -> 5.02        sky: { 500: '#0369a1', 600: '#075985', 700: '#0c4a6e' },     // info     2.77 -> 5.93        orange: { 500: '#c2410c', 600: '#9a3412', 700: '#7c2d12' },  // warn     2.80 -> 5.18        red: { 500: '#dc2626', 600: '#b91c1c', 700: '#991b1b' },     // danger   3.76 -> 4.83        purple: { 500: '#9333ea', 600: '#7e22ce', 700: '#6b21a8' }   // help     3.96 -> 5.38    }});provideAngulux({ theme: { preset: Accessible } });

What this costs you: those palette steps are now darker everywhere, including borders and focus rings. That direction is safe for text on a coloured fill, but if your own UI puts dark text on a light primary tint, re-check those places. If you would rather keep the change narrow, the same values work on components.button.colorScheme.light.root.<severity> — more precise, and repeated per component.

Dark mode

The default is 'system': the operating system decides and nothing in your application overrides it. If you want a toggle, name a selector instead.

app.config.ts
provideAngulux({    theme: {        preset: Aura,        options: {            // Default is 'system' — the operating system decides and nothing you            // write can override it. Name a selector to drive it yourself.            darkModeSelector: '.dark'        }    }})

Then put that class on the document — and put it there before the first paint. This is the part that is easy to get subtly wrong: a service that sets the class during bootstrap runs after the page has already been painted, so anyone on dark mode gets a white flash on every navigation. On a prerendered or server-rendered site that flash is the whole page.

index.html
// Decide the class BEFORE the first paint, in index.html <head>.// An Angular service cannot: by the time a bundle has run, the page is painted.(function () {    try {        var stored = localStorage.getItem('scheme');        var dark = stored ? stored === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;        if (dark) document.documentElement.classList.add('dark');    } catch (e) {        /* storage blocked: light is the correct fallback */    }})();

Use the same selector for both halves. If your own chrome follows a button while the components follow darkModeSelector: 'system', a reader who switches gets half a dark page. This site drives both from one .dark class for exactly that reason — the button in the header is the proof.

Running without the preset package

Supported, and not a degraded mode by accident — it is what makes the dependency genuinely optional. Structure, behaviour, accessibility and the p-* class names are all still there; the colours are not.

app.config.ts
// Drop @primeuix/themes entirely. provideAngulux() still works;// components render with structure and behaviour but no preset colours.provideAngulux()

The licence boundary

@primeuix/themes is MIT through 2.0.3, and 3.0.0 is the first commercial release. That release exists and is what the registry serves as latest, so the MIT line is closed rather than paused. angulux declares the package as an optional peer ranged ^2.0.0, and installing angulux alone pulls in zero PrimeTek packages.

Be clear about what that range buys you, though — it is a warning, not a lock. Installing a version above it still succeeds and only prints a resolution warning naming the range it broke. If the boundary matters to your legal review, pin it exactly and run a licence check in your own build rather than trusting ours.

terminal
# The range angulux declares is a WARNING, not a lock: installing 3.x# still succeeds and only prints a resolution warning. If the licence# boundary matters to your review, pin it and check it in your own build.pnpm add @primeuix/themes@2.0.3 --save-exact

What this means in practice: the preset package will not receive new MIT releases, so you are choosing to sit on a version that is finished rather than one that is maintained. That is a real cost and it is the honest reason to read the migration page before adopting anything here — it names the alternatives, including the ones that are not angulux.

Next: Getting started if you have not installed it yet, or the module list if you have.