State belongs in data-ui-*, not data-*
Datastar owns the data-* space. Put your styling state hooks under the
framework's existing data-ui-* namespace (it already uses
data-ui-theme/size/motion and .tag[data-ui-state]). This avoids any
collision with Datastar and makes selectors self-documenting:
.cal .ngrid > button[aria-current="date"] { outline: 2px dashed var(--focus); }
.cal .ngrid > button[data-ui-selected] { outline: 2px solid var(--focus); }
.cal .ngrid > button[data-ui-adjacent] { --bg: 0.1; }
<button data-attr:aria-current="cellIsToday($cal.v,i) ? 'date' : 'false'"
data-attr:data-ui-selected="cellIso($cal.v,i) === $cal.sel"
data-attr:data-ui-adjacent="!cellInMonth($cal.v,i)">…</button>
Classes are identity; data-ui-* (or ARIA) is state. A class says what the
element is — a layout primitive or component (.card, .column, .ngrid,
.tag), authored once, never toggled. A data-ui-* attribute says what state
it's in, toggled at runtime by data-attr. Keep them visibly separate:
class="column" (it's a column) carrying data-ui-selected (it's selected).
If the element already carries ARIA state, style off that — don't add a
redundant flag. Selection grids set aria-pressed/aria-checked; a combobox
option sets aria-selected. Style &[aria-pressed="true"] directly so the
accessible state and its look share one source of truth and one binding. Reserve
data-ui-* for state with no ARIA equivalent — a calendar's adjacent
(out-of-month) day, a range's band / preview / endpoints. (A calendar's today
is a real ARIA fact, aria-current="date", so it stays ARIA.) Two house
reservations on top of that rule: data-ui-state="active" is the ENGINE's
painted lens-control state (--bg 1 / --fg −1 — an applied sort/filter/scope
is a condition ARIA has no word for), and data-ui-state="on|off" is the
legend-toggle pair. Never stamp an ARIA state AND a data-ui-* flag for the
same fact — one source of truth, one binding.
data-class(reactively toggling a class) is still fine for genuinely class-shaped things — toggling a layout primitive, or the container-band.medium/.columnreveal. The rule is only that visual state lives indata-ui-*/ARIA, not that every reactive toggle is an attribute.
Where component CSS lives
Wrap each component's rules in @layer components (with a comment banner) and
nest the state inside the structural class, so an element and its states
read as one block:
@layer components {
/* calendar — standalone month surface */
.cal .ngrid > button {
aspect-ratio: 1; display: grid; place-items: center; --bg: 0.3;
& > b { --type: -1; --fg: -0.9; }
&[data-ui-adjacent] { --bg: 0.1; & > b { --fg: -0.6; } }
&[aria-current="date"] { outline: 2px dashed var(--focus); }
&[data-ui-selected] { outline: 2px solid var(--focus); }
}
}
Your components then become genuine peers of .card/.tag in the same layer,
so the cascade stays predictable instead of "app CSS wins by being unlayered."
The framework declares the spine up front (@layer reset, core.color, core.type, core.layout, theme, base, components, utility, media, skin, view-transitions;), so an
appended @layer components {} block joins that layer after the framework's
components — it doesn't become a new top layer. Do not put component rules
in project (or leave them unlayered): they would out-cascade the media
layer and break the revert-layer gates (.small/.medium/.c-*). Reactive
paints are unaffected: data-style:--bg and inline style="--bg:…" are inline
styles, which beat any layer.