dssl / native /spec /05-cascade.md
EHS-Studio's picture
Claude Sonnet 5.5
v0.16.3: native DSSL spec, and the seven bugs writing it found
9da4a9b
|
Raw History Blame Contribute Delete
15.5 kB

5. Cascade

The resolver flattens the nested blocks of a file into a flat list of rules. The cascade then decides, per element, which declaration of each property wins. This chapter defines both.

5.1 The rule record

A rule is a record with these fields.

Field Meaning
id a number, unique inside the document, in creation order
sel the full selector text, nesting already joined (5.3)
media a list of normalized media conditions (5.5). All must hold.
signals a list of signal conditions (5.9). All must hold.
containers a list of container conditions (5.6). All must hold.
layer one of tokens, base, components, utilities, pin
order a number: this rule's place in the file, counted from 0 (5.4)
decls a list of declarations: { prop, value, line } plus extras
file, line where the rule came from
pseudo the pseudo-element name for a shadow rule, else null
states the set of state words found in sel (5.7)

A shadow rule has the same fields. It is a rule whose sel names a pseudo-element target (5.8).

5.2 Layers

  • CAS-1. The layers are ranked tokens (0), base (1), components (2), utilities (3), pin (4).
  • CAS-2. Rules outside any layer block are in components. @layer <name> { } puts the rules inside it in layer <name> when that is one of the five names, and in components for any other name, with the warning unknown layer "<name>", treated as "components". The hint is the did-you-mean over the five names, or the layers are tokens, base, components, utilities, pin when none is close. (D-3)
  • CAS-3. pin { } puts its rules in layer pin. Declarations in pin behave as if !important: they beat declarations from every other layer, and they beat inline styles that the page itself wrote (5.11).
  • CAS-4. The layer tokens is used only for token rules (chapter 4).
  • CAS-5. There is no specificity. For two matching rules, a lower layer rank loses to a higher one. Within a layer, the later order wins.
.a { color: red }
pin { .a { color: blue } }
@layer base { .a { color: green } }

All three match. base is below components, so red beats green, and pin beats both. The element is blue.

5.3 Nesting

A selector block may contain declarations, other selector blocks, state blocks, pseudo-element blocks, @media, @container, @signal, @layer and pin blocks, and use lines.

  • CAS-6. Nested selectors are joined to the parent selector. If the child contains &, each & is replaced by the parent selector. Otherwise the result is the parent, a space and the child (a descendant selector). A block at the top level is used as written.
  • CAS-7. Lists are split at top-level commas (not inside parentheses or brackets, so :is(.x, .y) stays whole) on both sides. Every parent part is joined with every child part, parent-major: for each parent part in order, each child part in order. The results are joined with , . So .a, .b { .c { } } means .a .c, .b .c, .a { .c, .d { } } means .a .c, .a .d, and & is replaced by each parent part in turn. With no commas on either side the result is the plain join of CAS-6. (Before 0.16.3 the lists were joined as whole text, giving .a, .b .c. That was D-7.)
  • CAS-8. A nested block whose header is a single plain word that is close to a state word but not equal to it (hove, focs) is still a selector, but the resolver reports a warning: "<word>" compiled as a descendant selector, with the hint naming the state. This applies only when the header is a bare word of letters, digits and dashes, and there is a parent selector. "Close" is the did-you-mean rule (9.3). The warning never fires for a word that is an HTML element name (li, td, menu, and the rest of the list in the reference engine), because a nested element name is a descendant selector on purpose. (Before 0.16.3 li was warned about, being two edits from link. That was D-8.)
  • CAS-9. Conditions nest. @media, @container and @signal add one entry to the media, containers or signals list of everything inside. A layer block replaces the layer of everything inside.
  • CAS-10. A state block adds :<state> to the parent selector (5.7). A pseudo-element block makes a shadow rule (5.8).
.card {
  background: color.card
  hover { background: color.paper }
  @media dark { background: color.ink }
  &.on { background: color.accent }
}

5.4 Order

  • CAS-11. Each rule, shadow rule and raw block gets the next order number at the moment it is emitted. The registry rule (4.3) gets 0. Token rules get a number at the moment their block is reached.
  • CAS-12. A rule is emitted when its block ends, or when a nested block starts, if it has declarations waiting. So a block with declarations before and after a nested block makes two rules, one for each fragment, in source order:
.a {
  color: red
  hover { color: blue }
  padding: 4
}

This makes .a { color } (order 1), .a:hover { color } (order 2) and .a { padding } (order 3). The hover rule beats the base rule because it comes later. Writing the hover block before color: red would make color: red win while hovering.

  • CAS-13. A nested block is fully walked, and its own rule is emitted, before the rest of its parent. Blocks nested inside it are emitted first.
  • CAS-14. A rule with no declarations after resolution is not emitted.
  • CAS-15. Declarations that appear with no selector around them are the error declarations must live inside a selector, at the line of the first (line 0 for the file top level).
  • CAS-16. order is local to a file. When the host loads several files, it MUST place them in load order and give every rule one position in that single sequence: the numbers of a later file continue after the largest number of the earlier one. A later file wins over an earlier one for equal layers.

Why: before 0.8.2 each file counted from zero and the numbers were compared across files, which made a later file lose to an earlier one for no visible reason.

5.5 Media conditions

@media <query> adds one normalized query to media.

  • CAS-17. The query is trimmed. An empty query is the error @media needs a condition and the query (min-width: 0).
  • CAS-18. The single words dark, light and reduced-motion (any case) become (prefers-color-scheme: dark), (prefers-color-scheme: light) and (prefers-reduced-motion: reduce).
  • CAS-19. N or >= N becomes (min-width: Npx). <= N becomes (max-width: Npx). A..B becomes (min-width: Apx) and (max-width: Bpx). N, A and B are numbers.
  • CAS-20. Any other query is kept, except that in any comparison, a bare number after >, < or = that has no unit gets px: (width >= 768) becomes (width >= 768px).
  • CAS-21. A condition holds when the host's media query evaluation says it holds.
.a {
  @media 768 { padding: 24 }
  @media <= 480 { padding: 12 }
  @media 480..960 { gap: 4 }
}

5.6 Container conditions

  • CAS-22. @container <query> adds one normalized query (same rules as 5.5, with the empty case (min-width: 0)) to containers.
  • CAS-23. A container condition holds for an element when the nearest ancestor (or the element itself) that the page marked as a container has a content width that satisfies every bound in the query. A bound is (min-width: N), (max-width: N), (width >= N) or (width <= N). If no ancestor is a container, the condition does not hold.

5.7 States

  • CAS-24. A state block name { ... } in a block with selector S makes a rule with selector S:name. When S is a list, the state is added to every part (.a, .b with hover is .a:hover, .b:hover). The state words are in appendix A. State blocks can nest (hover { focus { } } gives S:hover:focus).
  • CAS-25. A state block with no selector around it is the error "<name>" needs a selector around it.
  • CAS-26. All state words except seen are the CSS pseudo-classes of the same name and are matched by the host.
  • CAS-27. seen is not a CSS pseudo-class. An element is seen once at least part of it has been inside the viewport, and stays seen. A rule whose selector contains :seen matches when the selector with :seen removed (or * if nothing is left) matches and the element is seen. An implementation MUST re-evaluate the element when it becomes seen.
  • CAS-28. Inside a seen block, a value that contains -> is the error "<prop>: <value>" - the -> arrow is motion syntax and does nothing inside seen { }. The rule is still emitted.
  • CAS-29. states is the set of state words found in sel as :word, from the list: hover, focus-visible, focus, active, disabled, checked, empty, target, invalid, valid, required, open, closed, read-only, read-write, placeholder-shown, first-child, last-child, only-child, seen.

5.8 Pseudo-elements and shadow rules

Inline styles cannot reach pseudo-elements, so the reference engine puts them in a separate stylesheet. A native implementation has no such limit, but the resolver output still keeps them in shadowRules, so that results are comparable.

  • CAS-30. A block ::name { } inside a selector block S makes a shadow rule with sel = S and pseudo = name. When S is a list, there is one shadow rule per part (and per state block inside it), each with its own copy of the declarations. At the top level (no selector), sel is *.
  • CAS-31. Inside a pseudo-element block, only declarations and state blocks are allowed. Anything else is the error only declarations and states (hover { }) may appear inside a pseudo-element block. A state block inside makes a second shadow rule with sel = S:<state>. seen there is the error "seen" is engine-native and has no meaning on a pseudo-element.
  • CAS-32. A bare content: (no value) inside a pseudo-element becomes content: "".
  • CAS-33. Engine-native declarations (the @ ones in 6.8) are not allowed on a pseudo-element, except @animate. Each is the error "<prop>" is engine-native and has no meaning on ::<name> - a pseudo-element has no DOM handle, and is dropped.
  • CAS-34. The same applies when the pseudo-element is written in the selector itself (.card::before { }). The selector is split at the first top-level ::. If every part of a selector list has a pseudo-element, the rule becomes one shadow rule per part. If only some do, the error is selector list mixes pseudo-element and plain selectors: "<sel>" and nothing is emitted.
  • CAS-35. A pseudo-element name MUST be in the pseudo-element list or match the vendor pseudo pattern (appendix A), otherwise: unknown pseudo-element "::<name>". ::slotted() is the error ::slotted() only exists inside a shadow tree, and DSSL styles the light DOM. :seen combined with a pseudo-element is an error.
  • CAS-36. If every declaration of a shadow rule was rejected, nothing is emitted.
.btn {
  ::after {
    content: ""
    opacity: 0
    hover { opacity: 1 }
  }
}

5.9 Signals

A signal is a named value that the page sets. It is how DSSL reads state that CSS cannot see (a theme chosen in a menu, a game screen). Chapter 11 says how a browser could expose it.

  • CAS-37. @signal <expr> adds one condition to signals. The forms are:

    Form Condition record
    name { name, op: 'truthy', value: null }
    name == value { name, op: '==', value }
    name != value { name, op: '!=', value }
    name in (a, b) { name, op: 'in', value: 'a,b', values: ['a','b'] }
    name not in (a, b) { name, op: 'notin', value: 'a,b', values: ['a','b'] }

    A name matches [A-Za-z_][\w-]*. A value may be quoted with " or ', and the quotes are removed. List values are trimmed, unquoted, and empty ones removed.

  • CAS-38. An expression in none of these forms is the error bad @signal expression "<expr>" and is treated as truthy on the whole text. An empty list is the error @signal <name> <in|not in> ( ) has no values.

  • CAS-39. A signal's value is looked up on the element, then on each ancestor, then at the document level. The first place that has the name wins. Values are compared as strings.

  • CAS-40. A signal condition holds when:

    • truthy: the name has a value that is not false, null, the string false, 0 or the string 0,
    • ==: the name has a value and its string equals the condition's value,
    • !=: the name has no value, or its string differs,
    • in: the name has a value and it is in the list,
    • notin: the name has no value, or it is not in the list.

5.10 Which rules apply to an element

  • CAS-41. A rule applies to an element when all of these hold:
    1. its selector matches the element. A rule with selector :root applies only to the document's root element;
    2. if the selector contains :seen, CAS-27 holds;
    3. every media condition holds;
    4. every signals condition holds;
    5. every containers condition holds.
  • CAS-42. Applicable rules are sorted by layer rank, then by order (both ascending). The declarations of the sorted rules are flattened in that order, each rule's declarations in their written order.
  • CAS-43. For each property, the last declaration in that sequence wins. Engine-native declarations (6.8) each have their own combination rule, given with their feature.

5.11 What else decides the value

These rules are about where DSSL meets the page.

  • CAS-44. Inline styles written by the page. A declaration that the page itself put in the element's style attribute, or set from a script, beats any rule that is not in pin. A pin rule beats it. A property the page removes is free for rules again.
  • CAS-45. Shorthand and longhand. A shorthand expands into its longhands in cascade position. Of a shorthand and one of its longhands, the one that comes later in the flattened sequence wins for that longhand. This holds for border and its -color, -width and -style longhands, and for padding, margin and gap and their sides.
  • CAS-46. Handing back with yield. After the sequence is flattened, properties named by the last applicable yield: (6.7) are removed from the DSSL result, so ordinary CSS decides them. yield: none clears the list.
  • CAS-47. Alignment makes a box. If the winning declarations include any of place-items, place-content, align-items, justify-items, align-content or justify-content, and the element has no declared display, and the element's natural display is not flex, inline-flex, grid or inline-grid (and is not none or contents), then display is set to inline-grid when the natural display is inline or starts with inline-, and to grid otherwise. An explicit display from any rule wins over this.

Why: alignment properties do nothing on a plain span or button, and DSSL treats that as a silent failure.

  • CAS-48. Border and positional shorthands in the inline lane. A browser that computes styles natively does not need extra rules for CAS-45: the CSS cascade already does it.