This reference covers how to contribute to the in-repo Sass theming source of Ignite UI for Angular.
Scope boundary: This guide covers the files inside
projects/igniteui-angular/core/src/core/styles/. Schema files (the$light-avatar,$dark-avatar, etc. Sass maps) live in the separateigniteui-themingnpm package and are not modified here. If a new design token needs to be added to a schema, that work happens in theigniteui-themingrepository and is consumed here as a package update.
projects/igniteui-angular/core/src/core/styles/
base/ ← shared functions, mixins, component registry
components/ ← per-component theme + structural SCSS (55 dirs)
_index.scss ← @forward barrel for all *-theme files
<component>/
_<name>-theme.scss ← theme mixin: emits CSS custom props + placeholders
_<name>-component.scss ← structural mixin: BEM selectors + @extend placeholders
themes/
_core.scss ← core() mixin: @use + @include every *-component mixin
generators/
_base.scss ← theme() mixin: calls every *-theme() function (defines the material theme)
_bootstrap.scss ← bootstrap-light-theme() / bootstrap-dark-theme()
_fluent.scss ← fluent-light-theme() / fluent-dark-theme()
_indigo.scss ← indigo-light-theme() / indigo-dark-theme()
presets/ ← compilable entry-point SCSS files (one per variant)
spec/ ← sass-true unit tests
The base/_index.scss re-exports everything from igniteui-theming/sass/ that
component files need (tokens, var-get, sizable, rem, BEM mixins, etc.),
so every component file only needs one import:
@use '../../base' as *;Every component has exactly two SCSS files. Never merge them.
| File | Mixin name | Responsibility |
|---|---|---|
_<name>-theme.scss |
@mixin <name>($theme) |
Calls tokens($theme, $mode: 'scoped'), reads $variant, defines %placeholder selectors with all visual styles |
_<name>-component.scss |
@mixin component |
Defines BEM selectors using b/e/m mixins, registers the component, extends placeholders |
The theme mixin is called once per palette/schema combination inside theme() in
generators/_base.scss. The component structural mixin is called once globally
inside core() in themes/_core.scss. Keeping them separate ensures structural
CSS is emitted only once regardless of how many theme overrides a consumer applies.
@use 'sass:map';
@use '../../base' as *;
// Import animation easings only if the component uses transitions
@use 'igniteui-theming/sass/animations/easings' as *;
/// @deprecated Use the `tokens` mixin instead.
/// @see {mixin} tokens
/// @param {Map} $theme - The theme used to style the component.
@mixin <name>($theme) {
// 1. Emit all theme tokens as CSS custom properties on the host element
@include tokens($theme, $mode: 'scoped');
// 2. Read variant and theme-variant for conditional styling
$variant: map.get($theme, '_meta', 'theme'); // 'material' | 'fluent' | 'bootstrap' | 'indigo'
$theme-variant: map.get($theme, '_meta', 'variant'); // 'light' | 'dark'
// 3. Define placeholder selectors — one per logical visual state or element
%<name>-display {
@include sizable();
--component-size: var(--ig-size, #{var-get($theme, 'default-size')});
// All colors come from var-get, never from hard-coded hex values
color: var-get($theme, 'color');
background: var-get($theme, 'background');
}
// 4. Variant-specific overrides — only when behavior genuinely differs
@if $variant == 'indigo' {
%<name>-display {
// indigo-specific styles
}
}
}Key rules for theme files:
- Call
@include tokens($theme, $mode: 'scoped')first — always, without exception. This emits all design tokens as--ig-<name>-*CSS custom properties. - Use
var-get($theme, 'token-name')for every color, size, and spacing value. Never write hard-coded hex, RGB, HSL, or pixel values for anything that has a corresponding design token. - Name placeholders
%<block>for the root,%<block>__<element>for elements, and%<block>--<modifier>for modifiers. - Use
@if $variant == '<name>'only when the design genuinely differs across design systems. Prefer token differences (handled by the schema) over branching in the theme mixin. - Read both
$variant(design system name) and$theme-variant(light/dark) — use the one appropriate to your condition.
@use '../../base' as *;
@use 'sass:string';
/// @access private
@mixin component {
@include b(igx-<name>) {
// Register this component in the global component registry.
// Use bem--selector-to-string(&) to extract the block name.
$this: bem--selector-to-string(&);
@include register-component(
$name: string.slice($this, 2, -1),
$deps: (
igx-icon,
// list every component this one visually depends on
)
);
// Extend the root placeholder from the theme file
@extend %<name>-display !optional;
// Elements
@include e(suffix) {
@extend %<name>__suffix !optional;
}
// Modifiers (boolean)
@include m(disabled) {
@extend %<name>--disabled !optional;
}
// Element with modifier (combined)
@include e(label, $m: 'focused') {
@extend %<name>__label--focused !optional;
}
}
}Key rules for component files:
- Always call
register-componentinside the rootb()block with the component's BEM block name as$name. The$depslist must include every other Ignite UI component whose theme this component visually relies on (e.g., if the component rendersigx-iconinternally, listigx-icon). - Use
!optionalon every@extend. Theme mixin placeholders are only defined when a theme is applied; without!optional, Sass errors in partial builds. - Never put visual styles (colors, fonts, sizes, box shadows) in the component
file. Structural layout (
display,position,width,flex) that is truly independent of the theme belongs here; everything else belongs in the theme file. - Do not import
igniteui-themingmodules directly — all BEM mixins and utilities come through../../base.
CSS classes follow BEM Two Dashes style. See
css-naming-convention.md for the full
spec. Quick reference:
| BEM entity | Pattern | Example |
|---|---|---|
| Block | igx-<name> |
igx-avatar |
| Element | igx-<name>__<element> |
igx-avatar__image |
| Modifier | igx-<name>--<modifier> |
igx-avatar--circle |
| Element + modifier | igx-<name>__<element>--<modifier> |
igx-avatar__item--selected |
BEM mixin usage in component files:
@include b(igx-avatar) { ... } // → .igx-avatar
@include e(image) { ... } // → .igx-avatar__image
@include m(circle) { ... } // → .igx-avatar--circle
@include e(item, $m: 'selected') { ... } // → .igx-avatar__item--selectedPlaceholder names must resemble the BEM selectors they represent(if present):
%avatar-display ← root display placeholder (named -display, not bare block)
%avatar__image ← element placeholder
%avatar--circle ← modifier placeholder
%avatar__item--selected ← element+modifier placeholder
Adding a new component requires changes in three files beyond the two new SCSS files themselves:
// Add alphabetically:
@forward '<name>/<name>-theme';// At the top, @use the component file:
@use '../components/<name>/<name>-component' as <name>;
// Inside the core() mixin body, call it:
@include <name>.component();Inside the theme-internal mixin body, follow the existing pattern:
@if is-used('igx-<name>', $exclude) {
$<name>-theme-map: <name>-theme(
$schema: $schema,
);
$<name>-theme-map: meta.call($theme-handler, $<name>-theme-map);
@include <name>($<name>-theme-map);
}Also add the same block to each of the variant-specific generator files
(_bootstrap.scss, _fluent.scss, _indigo.scss) if the component has
variant-specific theme overrides.
Order matters: Add the new
@if is-used(...)block in the same relative position as the corresponding@include <name>.component()call in_core.scss. The two files should stay in the same component order for maintainability.
When a feature adds a new modifier, element, or interaction state:
1. In _<name>-theme.scss — add a new placeholder:
%<name>--<new-modifier> {
background: var-get($theme, '<new-token>');
color: var-get($theme, '<new-token>-contrast');
}If the new state requires a new design token, that token must be added to the
schema in the igniteui-theming package first, then consumed here via
var-get($theme, '<new-token>'). DO NOT introduce a hard-coded fallback value
for a token that should be schema-driven.
2. In _<name>-component.scss — add the corresponding BEM structure and
extend the new placeholder:
@include m(<new-modifier>) {
@extend %<name>--<new-modifier> !optional;
}Use @if $variant == '...' only when the structural or behavioral difference
cannot be expressed through design tokens alone.
// Read both axes if needed
$variant: map.get($theme, '_meta', 'theme'); // design system name
$theme-variant: map.get($theme, '_meta', 'variant'); // 'light' | 'dark'
// Pixel values that differ per design system — acceptable branching
$chip-padding: (
comfortable: rem(if($variant != 'indigo', 12px, 7px)),
cosy: rem(if($variant != 'indigo', 6px, 5px)),
compact: rem(if($variant != 'indigo', 2px, 3px)),
);
// Property that is absent in some variants — acceptable
$box-shadow-focus: map.get(
(
'material': null,
'fluent': null,
'bootstrap': 0 0 0 rem(4px) var-get($theme, 'focus-outline-color'),
'indigo': 0 0 0 rem(3px) var-get($theme, 'focus-outline-color'),
),
$variant
);Use a Sass map keyed by variant name when the value differs for all four systems.
Use if($variant != 'indigo', ...) for simple binary differences.
Never branch on $theme-variant to change structural layout — light/dark
differences must be expressed entirely through token values in the schema.
| Situation | Use |
|---|---|
| Consuming a token from the current component's theme | var-get($theme, 'token-name') |
| Consuming a global layout token (not component-specific) | var(--ig-size), var(--ig-spacing), var(--ig-radius-factor) |
| Consuming a palette color directly in custom CSS | var(--ig-primary-500), var(--ig-gray-200) |
var-get($theme, 'token-name') resolves to var(--ig-<component>-token-name)
with appropriate fallback chaining. It must only be used inside a theme mixin
where $theme is in scope.
When touching only SCSS files, run the style linter first:
npm run lint:stylesRun the full linter before finishing to catch any TypeScript/template issues introduced alongside the style changes:
npm run lint:libSCSS functions and mixins in base/ have unit tests using
sass-true. Tests live in:
projects/igniteui-angular/core/src/core/styles/spec/
_index.scss ← barrel: @use the spec files
_functions.spec.scss ← tests for functions (is-used, is-component, list-diff)
_mixins.spec.scss ← tests for mixins (register-component, gen-color-class)
tests.mjs ← Jasmine runner (do not modify)
Run style tests:
npm run test:stylesWhen to add a style test:
- You add a new Sass function to
base/_functions.scss - You add a new utility mixin to
base/_mixins.scss - You modify the component registry logic in
register-componentoris-used
You do not need to write style tests for new component theme or component structural mixins — those are covered by visual/Karma tests on the Angular component side.
Test structure (sass-true):
@use 'sass-true' as *;
@use '../base' as *;
@include describe('My Mixin') {
@include it('should do X') {
@include assert() {
@include output() {
@include my-mixin(args);
}
@include expect() {
// expected CSS output
}
}
}
}The themes/presets/ directory contains one SCSS file per design-system/variant
combination. Each file is compiled to a standalone CSS file by the build script.
If you add a new design system variant (rare, if ever), add a preset file following this structure:
@use '../../themes' as *;
@include core();
@include typography($font-family: $<variant>-typeface, $type-scale: $<variant>-type-scale);
@include theme($schema: $<light|dark>-<variant>-schema, $palette: $<light|dark>-<variant>-palette);Compile with:
npm run build:libThe build script at scripts/build-styles.mjs compiles all presets and copies
the raw SCSS source tree to dist/igniteui-angular/lib/core/styles/.
The following are not in this repository. They live in the igniteui-theming
package and are consumed as a versioned npm dependency:
| Area | What it provides |
|---|---|
sass/bem/ |
b(), e(), m(), mx(), bem--selector-to-string() mixins |
sass/color/ |
palette(), color() functions, palette presets |
sass/elevations/ |
elevations() mixin, elevation presets |
sass/themes/schemas/ |
$light-avatar, $dark-avatar, etc. — per-component token maps |
sass/themes/components/ |
avatar-theme(), button-theme(), etc. — component theme functions |
sass/utils/ |
tokens(), var-get(), sizable(), rem(), pad() |
sass/typography/ |
type-style(), typography() mixin, type scale presets |
sass/animations/ |
Easing variables ($ease-in-out-quad, etc.) |
When a component's visual change requires a new design token (new color,
new size dimension, new border radius), the token must be added to the
relevant schema in igniteui-theming first and released as a package update.
Only then can it be consumed here via var-get($theme, 'new-token').
If a task calls for adding a new token without a corresponding igniteui-theming update available,
flag this dependency clearly rather than introducing a hard-coded value as a stopgap.
-
_<name>-theme.scsscreated with@mixin <name>($theme), callstokens($theme), defines all placeholders usingvar-get -
_<name>-component.scsscreated with@mixin component, callsregister-componentinside the rootb()block, extends all placeholders with!optional -
components/_index.scss—@forward '<name>/<name>-theme'added -
themes/_core.scss—@useadded at top,@include <name>.component()added in mixin body -
themes/generators/_base.scss—@if is-used('igx-<name>', $exclude)block added - BEM class names follow the Two Dashes convention (
css-naming-convention.md), covered by the appropriateb(),e(), andm()mixins - No hardcoded hex/RGB/HSL values — all colors come from
var-get($theme, '...') -
npm run lint:stylespasses -
npm run test:stylespasses (if base functions/mixins were modified)
- New placeholder added to
_<name>-theme.scssusingvar-getfor all token references - New
@include m(...)or@include e(...)block added to_<name>-component.scssextending the new placeholder with!optional - If a new design token is needed, flagged that
igniteui-themingpackage must be updated first -
npm run lint:stylespasses