class Can::CssScoper

Overview

Rewrites CSS to scope every selector to a given attribute.

.card { color: red }
.card:hover, h2 { color: blue }
@media (max-width: 600px) { h2 { font-size: 1rem } }

becomes (with attr data-c-foo):

.card[data-c-foo] { color: red }
.card[data-c-foo]:hover, h2[data-c-foo] { color: blue }
@media (max-width: 600px) { h2[data-c-foo] { font-size: 1rem } }

Pass-through (not scoped): @keyframes, @font-face, @import, @charset, @page, and any unknown at-rule body. Recurses into @media, @supports, @container, @layer blocks.

Tokenizer is brace/string/comment-aware but doesn't fully implement the CSS syntax module; uncommon edge cases (escapes inside selectors, attribute selectors with nested brackets) may not roundtrip identically.

TODO revisit per-element stamping once CSS @scope { ... } to (...) is broadly supported (Chromium ships it; Firefox/Safari behind as of 2026). @scope provides a real scope barrier with a stop selector, which would let us stamp only the component's root element and wall off nested components via to (.child-component-root). Same isolation semantics, much lighter markup. Conservative target: revisit when all evergreen browsers ship it and a reasonable LTS window has passed.

Defined in:

can/css_scope.cr

Constant Summary

SCOPED_AT_RULES = {"media", "supports", "container", "layer"}

Class Method Summary

Class Method Detail

def self.scope(css : String, attr : String) : String #

[View source]