#8833·django-cms

Toolbar chrome in #cms-top is not protected from project CSS: replace .cms-reset with an id-anchored 'all: revert'

Author: fsbraunCreated Sep 4, 2026Updated Sep 4, 2026
Labelscomponent: frontend5.05.1

Problem

.cms-reset is the only thing protecting toolbar chrome from project CSS, and it is far too weak:

  • Compiled, it is .cms-reset a — specificity (0,1,1). Any theme rule with two classes beats it.
  • It resets a hand-written list of ~15 properties on a hand-written list of elements (no span, button, h4h6, img, svg; no text-transform, letter-spacing, white-space, visibility).
  • Components rely on it for properties they never set themselves. _toolbar.scss compiles to div.cms .cms-toolbar-item-navigation li a{color:…;padding:0 10px;…} — specificity (0,2,3), but it never sets text-decoration, delegating that to the (0,1,1) reset. So the strong rule and the weak rule govern different properties of the same element, and only the weak one is exposed.

Real-world hit on django-cms.org: an accessibility rule restoring underlines for links in running text,

scss
:is(p, li, td, dd, blockquote, figcaption) a:not(.btn, .nav-link, …) { text-decoration: underline; }

is (0,1,2) and underlines every item in the toolbar menus, because those are <ul><li><a>. Nothing in django CMS pushes back. The whole compiled cms.base.css contains exactly one !important.

Proposal

Replace the allowlist reset with a real one, anchored on the id:

scss
#cms-top *:not(svg, svg *) { all: revert; }
  • (1,0,1) — beats essentially any project rule that does not use ids or !important.
  • all closes the property and element allowlist holes in one go.
  • svg is excluded because all: revert would wipe presentation attributes on the icon sprites.

This outranks django CMS's own component rules at (0,2,x), so they have to be lifted above it. That looks like a one-line change, since everything is already nested under a single wrapper in cms.base.scss:

scss
// div.cms needs to beat .cms-reset a selectors
div.cms {   →   #cms-top {

giving #cms-top .cms-toolbar-item-navigation li a = (1,1,3) > (1,0,1). I checked that all chrome — toolbar, messages, tooltips, sideframe, modal — renders inside #cms-top via the toolbar_top/toolbar_bottom blocks in toolbar_with_structure.html, so re-anchoring should be safe. The one div class="cms" outside #cms-top is cms/templates/cms/noapphook.html.

Notes

  • Cascade layers do not help here: unlayered author styles beat layered ones for normal declarations, so wrapping cms.base.css in @layer would make it lose to every unlayered project stylesheet.
  • Shadow DOM on #cms-top with :host { all: initial } is the only complete isolation, but cms.base.js, plugin-contributed toolbar items and the structure board all assume light DOM — a major-version project, not a fix.
  • Cheap independent hardening, worth doing regardless: stop letting the reset be load-bearing. Adding text-decoration: none to .cms-toolbar-item-navigation li a fixes the reported symptom at (0,2,3) with zero risk.

Version: django CMS 5.1.1.