Skip to content

CSS Cascade Layers (@layer) for CKEditor 5 #20025

@pszczesniak

Description

@pszczesniak

While working on UI customization by introducing more granular CSS design tokens, I identified a few potential problems that we could address:

  • bundle-order independence,
  • stylesheet order → reliable token overrides,
  • avoiding the need for !important.

Proposal: CSS Cascade Layers (@layer) for CKEditor 5

Problem

When CKEditor's CSS is bundled with integrator styles, the output order is unpredictable. An integrator's :root { --ck-radius-base: 5px } might appear before the editor's :root { --ck-radius-base: 2px } in the bundle - and the editor's value silently wins. Integrators resort to !important or high-specificity selectors to force their overrides.

Solution

Wrap CKEditor's CSS in a single ckeditor5 layer with internal sub-layers, and provide a dedicated ck-overrides layer for integrator customizations.

Layer architecture

@layer ckeditor5, ck-overrides;

@layer ckeditor5 {
    @layer reset, tokens, ui, content;
}

External API (what integrators see)

Layer Who uses it Priority
ckeditor5 Editor internals (don't touch) Lower
ck-overrides Integrator customizations Higher

Internal sub-layers (implementation detail)

Sub-layer What goes in Priority within ckeditor5
ckeditor5.reset Element resets (box-sizing, margin, font) Lowest
ckeditor5.tokens All :root token definitions (foundation, semantic, component)
ckeditor5.ui Component selectors (.ck-button, .ck-toolbar, etc.)
ckeditor5.content Editable area styles (.ck-content typography, lists, tables) Highest (within editor)

Integrator usage - simple

@layer ck-overrides {
    :root {
        --ck-radius-base: 5px;
        --ck-spacing-sm: 5px;
        --ck-toolbar-background-color: red;
        --ck-balloon-panel-arrow-display: none;
        --ck-dropdown-panel-uniform-border-radius: var(--ck-radius-base);
    }

    .ck.ck-toolbar {
        padding: 0 10px;
    }
}

Integrator usage - don't want layers at all

/* Unlayered CSS beats all layers per spec. Still works. */
:root {
    --ck-radius-base: 5px;
}

What this solves

  • Bundle ordering - layer priority is declared once upfront. Source order in the bundled file doesn't matter.
  • No more !important - the layer hierarchy guarantees priority.
  • Reliable token overrides - integrator tokens in ck-overrides always beat editor tokens in ckeditor5.tokens.
  • CSS selector overrides - .ck.ck-toolbar {} in ck-overrides beats the same selector in ckeditor5.ui.

Why nested sub-layers

  • For integrators: Simple - they only interact with ck-overrides. One instruction, one layer.
  • For us: Full internal control - reset < tokens < ui < content priority. Adding new sub-layers (e.g., ckeditor5.plugins for feature styles, ckeditor5.themes for dark mode) doesn't change the integrator API.
  • For advanced integrators: Sub-layers are documented but optional. Someone who needs to override only content styles without affecting UI can target ckeditor5.content directly.
  • Future-proof: Internal reorganization never breaks the external ck-overrides contract.

Backward compatibility

  • Integrators who don't use @layer at all still work - unlayered CSS beats all layers per the CSS spec.
  • The ck-overrides layer is opt-in for integrators who want guaranteed priority within the layered system.
  • The three-tier design token system (#19910) maps naturally to the tokens / ui / content sub-layer boundaries.

Risks to investigate

  • Integrators with existing unlayered CSS that was previously losing on specificity would now automatically win over all editor layers - could cause unintended visual changes.
  • Requires coordination across all packages (ui, feature packages, theme).
  • Browser support: @layer is supported in all browsers since 2022 (Chrome 99+, Firefox 97+, Safari 15.4+). Verify this matches CKEditor's browser support matrix.

Prerequisite

The three-tier design token system (done in #19910) provides the architectural foundation. The token/semantic/component file organization already maps to the proposed sub-layer boundaries.


If you'd like to see this improvement implemented, add a 👍 reaction to this post.

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:dxThis issue reports a developer experience problem or possible improvement.squad:coreIssue to be handled by the Core team.type:improvementThis issue reports a possible enhancement of an existing feature.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions