Skip to content

CSS Classes/Styling System in Ignite UI for Angular #17669

Open
0 of 2 issues completed
Open

Description

@simeonoff

Summary

Our component styles use BEM, a naming convention that predates native CSS scoping and cascade layers. BEM relies on a contract: a block never styles another block’s internals. Our tooling doesn’t enforce that, and we have broken it in many places. Components restyle each other from the outside, design systems repeat the same rules, and contributors have no shared, enforced authoring standard.

This epic replaces BEM with an approach built on modern CSS and the scoped-styles architecture we already shipped. It follows the same ideas used in Ignite UI for Web Components, with lint rules to keep the implementation consistent.

The migration is staged. Existing styling hooks remain during a deprecation window, with their removal planned for a future major.

Why

An analysis of the current stylesheets found:

Every component ships all four design systems. Each component’s CSS contains its Material, Bootstrap, Fluent and Indigo rules, and the browser picks one at runtime. Today, 53% of compiled component CSS lives in these per-design-system rules, while an app typically uses one design system at a time. Many rules are copies that differ by a value or two; the stepper’s four theme files, for example, are 73% identical. Others contain structural differences that will stay. How much can become plain token values is something we will measure component by component, not assume.

Components reach into each other. 362 of the grid’s 759 selectors style the internals of other components: input, combo, checkbox, icon and button. Most of the grid’s !important declarations are in those rules. Because our component styles are global, changes to one component’s internals can break the grid. We have seen this repeatedly: #16452, #16614, #16360, #16817, #16267 and #16355.

The same state is tracked in several places. “Selected”, “disabled” or “invalid” often exists as a CSS class, an ARIA attribute and native element state at the same time. Those copies can disagree. We maintain roughly 370 class bindings, most of which only mirror state.

The code we read isn’t the CSS we ship. Selectors are generated by BEM mixins (b(), e(), m(), over 2,900 calls), so what appears in DevTools rarely matches the source. The grid even has five comments reading WARN: This doesn't follow the BEM structure.

Nothing enforces consistency. RTL, focus rings, motion and spacing are each handled several different ways. There are 55 distinct transition definitions, for example. Formatting is configured but not enforced.

Angular and Web Components drift apart. The two libraries share design tokens but structure their styles differently, contributing to parity issues such as #17423, #16391 and #15726.

The maintenance figures reflect this: since October 2024, 122 of the 816 issues filed in this repo carry the themes label. Of the commits that touched styles, 59% were fixes, compared with 46% for the codebase as a whole.

Why now

Native scoping (@scope), cascade layers, :has() and style queries give us browser-level tools for addressing these problems. The scoped-styles architecture already relies on several of them. The proposed browser floor and the outstanding Firefox ESR check are listed below.

What changes

  1. Each component styles itself. A block() helper wraps its styles in a native @scope with boundaries at nested components. This gives us an enforceable boundary for component rules.
  2. Value-only design-system differences become tokens. Where an override only changes a value, the structural rule is written once and the value comes from a design token.
  3. Styles use existing element state. We use :disabled, :focus-visible or an ARIA attribute the component already sets. Other variants and states use data-* attributes instead of parallel classes.
  4. Common layouts are shared. Small helpers such as ig-layout="cluster" and ig-truncate replace repeated flex and ellipsis rules and can be shared with Web Components.
  5. An authoring guide and lint rules define the conventions. These apply to both human and AI-authored changes.

Each component keeps its root class, such as .igx-combo, as a stable public hook. Internal elements use data-part="…", state uses native pseudo-classes or attributes, and layouts use ig-* attributes. Existing BEM classes remain during the migration.

// Today — checkbox/themes/_base.scss (abridged):
// a class mirrors aria-invalid, and each affected element is targeted.
@include m(invalid) {
    @include e(composite) { border-color: var-get($_theme, 'error-color'); }
    @include e(label) { color: var-get($_theme, 'error-color'); }
    …
}

// Proposed — read the state from the existing input.
@include block(checkbox) {
    :scope:has(> input[aria-invalid='true']) {
        --_border: var(--error-color);
        --_label-color: var(--error-color);
    }
}

Plan

Each stage should leave the codebase in a state we can maintain if we pause the migration. Old and new styles will coexist while components are converted.

Stage Work Expected result Customer impact
0 — Foundations Authoring guide, block() helper, CSS size report, lint rules, visual-regression checks and an @scope spike in the grid Establish conventions and validate the approach No intended visible change
1 — Hygiene Remove dead and duplicated styles and @extend; standardize RTL handling Reduce existing inconsistencies No intended visible change
2 — Design systems as tokens Convert value-only design-system overrides to tokens Smaller CSS and fewer rules to maintain Reduced CSS size
3 — Shared layout helpers Introduce ig-layout, ig-truncate and similar helpers, shared with Web Components Reduce repeated layout declarations No intended visible change
4 — Per-component conversion Move each component to block(), data-part and native state Scope component rules and simplify selectors New hooks; old classes deprecated
5 — Decouple components Add a supported “embedded” input style for the grid and remove remaining cross-component overrides Remove the grid’s dependency on other components’ internal styles New hooks; old classes deprecated
6 — Grid structure Row-level tokens, indentation without the 10-level cap, z-index isolation and a CSS-grid columns spike Address grid correctness and assess rendering cost Bug fixes
7 — Retire BEM Remove deprecated classes and provide an ng update migration Complete the migration Breaking change in a future major

After Stage 3, we review how much CSS the token work removed and what the @scope spike showed for the grid. Those results determine the details of Stages 4–7.

Risks

Risk How we handle it
Visual regressions Stage 0 adds screenshot or CSS-snapshot checks across all four design systems in light and dark. Component conversions happen one per PR.
Breaking customer overrides Keep old classes through a deprecation window and check override compatibility during conversion. Remove them only in a major, with an ng update migration.
@scope performance in the grid Measure in the Stage 0 spike before committing to grid conversion.
Browser support Chrome/Edge move to 131+. The proposed Firefox and Safari floors match the library’s existing requirements; Firefox ESR still needs checking.
Pausing halfway Keep each stage independently maintainable and use lint baselines to prevent new violations.
Scope creep ARIA audits, token renames and Shadow DOM are out of scope. File related findings separately.
Cross-team dependencies Build shared work for igniteui-theming here first, then move it upstream.

How we’ll know it’s working

The Stage 0 size report will track CSS metrics on each PR. We will also track state-binding cleanup and themes-labelled issues over time.

Metric Today Direction
Component CSS (minified / gzip) ~636 KB / ~82 KB Down; set a target after the first Stage 2 conversions
Share of CSS in per-design-system rules 53% Down where overrides are value-only
Grid selectors targeting other components 362 Toward zero
!important in compiled CSS ~240 Toward zero
Class bindings that only mirror state ~370 Reduce in Stage 4; remove remaining bindings in Stage 7
Themes-labelled issues per quarter ~15 Down over time

Decisions so far

Area Decision
Browser floor Formalize Chrome/Edge 131, Firefox 151 and Safari/iOS 18. Firefox and Safari match what we already require. Firefox ESR check pending.
Layout helpers Use ig-layout="…" and boolean ig-* attributes, shared with Web Components.
Internal elements Use data-part inside native @scope, set up by block(). Reserve native part for Shadow DOM rather than making it an Angular styling hook.
State attributes Use data-variant, data-type, data-orientation, data-state and data-pinned, only on elements the library owns.
ARIA as a hook Use it only where the component already sets that ARIA attribute. Otherwise use data-*. This epic does not audit ARIA.
Deprecation Keep old classes in the markup until a future major, but stop using them in library styles as components are converted. Supported customer hooks will be design tokens, the root class, data-part and state attributes.
Sass Keep it for theme and token generation and the block() helper. Remove the BEM mixins and write selectors as plain CSS.

Raise proposed changes in a comment so we can discuss them and update this table.

Working on this epic

Each stage has a sub-issue with its own tasks. We are opening tasks for Stages 0–3 now; later stages will be broken down after the Stage 3 review. Work that belongs in igniteui-theming will be tracked there.

The Stage 0 authoring guide is a rewrite of .github/themes-contributing.md, which replaces css-naming-convention.md.

Glossary
  • BEM: a naming convention using block, element and modifier classes, such as .block__element--modifier.
  • CUBE CSS: a methodology that separates Composition, Utility, Block and Exception styles (cube.fyi). This epic adapts it for a component library.
  • Design token: a named design value, such as a color, size or font, represented here by a CSS custom property. A theme supplies a set of token values.
  • Cascade layer (@layer): explicit ordering of groups of styles to control their precedence.
  • @scope: limits where selectors apply. Here, it scopes rules to a component, with boundaries at nested components.
  • Part: a named internal element used as a styling hook. Angular uses data-part; Web Components uses the native Shadow DOM part attribute.
  • Style query: a query such as @container style(--ig-theme: material) that applies styles based on a custom property’s value. It selects the design system at runtime.

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions