| name | css |
| description | CSS-conventies voor design-system componenten — MECE breakpoints (expliciet per breakpoint, geen mobile-first overrides), at-rule nesting, lokale variabelen, comments |
| user-invocable | false |
CSS conventie
CSS-schrijfconventies voor design-system componenten: responsive (MECE breakpoints), at-rule nesting, lokale variabelen, comments.
Responsive: expliciet per breakpoint
Schrijf responsive component-CSS expliciet per breakpoint. Geen mobile-first base-waarde die in @media of @container queries wordt overschreven.
Waarom
Voor een design-system zwaarder dan voor een gewone app. Componenten worden door derden geconsumeerd, geaudit, en visueel gecontroleerd per breakpoint. Een mobile-first override creëert een impliciete cascade die de developer moet "uitrekenen": "wat is de actuele waarde op md?". Met expliciet-per-breakpoint staat het antwoord direct in de regel.
Trade-off: méér CSS-regels per property dat verschilt per breakpoint. Voor design-system componenten weegt determinisme zwaarder dan kort-en-bondig.
Regel
Een property die varieert per breakpoint mag niet een base-waarde buiten queries hebben. Elke variant zit in z'n eigen @media/@container block.
Keerzijde van dezelfde regel — de breakpoint-bereiken zijn MECE (Mutually Exclusive, Collectively Exhaustive):
- Collectively Exhaustive: zodra een property in één breakpoint wordt gezet, moet 'ie in álle breakpoint-bereiken gezet worden (sm én md én lg). Er mag geen viewport zijn waar de property ongezet blijft — omdat er geen fallback buiten de queries staat, resolveert 'ie anders leeg op een niet-gedekte viewport.
- Mutually Exclusive: de bereiken overlappen niet, zodat op elke viewport precies één query matcht en er nooit een override in de browser plaatsvindt.
Samen: elke viewport valt in precies één breakpoint, geen gat en geen overlap.
Een property die niet varieert per breakpoint blijft buiten queries — dat is de natuurlijke base.
Voorbeeld
❌ Mobile-first met override (vermijd):
.simple-section {
display: flex;
padding: var(--sm-padding);
@container (min-width: 641px) {
padding: var(--md-padding);
}
@container (min-width: 1008px) {
padding: var(--lg-padding);
}
}
✅ Expliciet per breakpoint:
.simple-section {
display: flex;
@container (max-width: 640px) {
padding: var(--sm-padding);
}
@container (min-width: 641px) and (max-width: 1007px) {
padding: var(--md-padding);
}
@container (min-width: 1008px) {
padding: var(--lg-padding);
}
}
At-rule positie: nest binnen de selector
Voorkeur: @media/@container genest binnen de selector via native CSS
nesting. Niet gehoist als top-level wrapper. Argumenten:
- Lokaliteit: alle responsive-regels voor een component-element staan
bij elkaar — om uit te zoeken hoe
.foo zich gedraagt zoek je op .foo,
niet op alle media queries verspreid door 't bestand
- Geen selector-duplicatie: één keer
.foo {}, daarbinnen alle varianten
- Past bij component-architectuur: elk element heeft één regel met
z'n volledige verantwoordelijkheid
❌ Wrapper rond selector (vermijd):
.foo {
display: flex;
}
@media (max-width: 640px) {
.foo {
padding: 8px;
}
}
✅ Nested in selector:
.foo {
display: flex;
@media (max-width: 640px) {
padding: 8px;
}
}
Lokale CSS-variabelen bovenaan
Declareer lokale custom properties (--_* voor private, --components-*/
--context-* voor public) bovenaan het :host (of vergelijkbare top-level
selector) blok, gescheiden van de rest met een witregel. Zo zie je in één
oogopslag wat er instelbaar is voordat je de eigen properties leest.
:host {
--_max-height: calc(100vh - var(--semantics-overlays-inset) * 2);
--components-popover-default-width: 320px;
display: flex;
margin: 0;
padding: 0;
}
Volgorde: kleinste breakpoint eerst
Plaats @media/@container blokken in oplopende volgorde — sm, md, lg.
Binnen elk paar (eerst @media, dan @container) ook in die volgorde.
Maakt scannen voorspelbaar: linksboven sm, rechtsonder lg.
.foo {
display: flex;
@media (max-width: 640px) { … }
@media (min-width: 641px) and (max-width: 1007px) { … }
@media (min-width: 1008px) { … }
@container layout-container (max-width: 640px) { … }
@container layout-container (min-width: 641px) and (max-width: 1007px) { … }
@container layout-container (min-width: 1008px) { … }
}
Witregels tussen breakpoint-blokken
Zet een witregel tussen elk @media/@container blok binnen dezelfde
selector — ook tussen twee opeenvolgende @media of twee opeenvolgende
@container blokken. Dat geeft elk breakpoint visueel z'n eigen ruimte.
.foo {
display: flex;
@media (max-width: 640px) {
padding: 8px;
}
@media (min-width: 641px) and (max-width: 1007px) {
padding: 16px;
}
@media (min-width: 1008px) {
padding: 24px;
}
@container layout-container (max-width: 640px) {
padding: 8px;
}
@container layout-container (min-width: 641px) and (max-width: 1007px) {
padding: 16px;
}
@container layout-container (min-width: 1008px) {
padding: 24px;
}
}
Wanneer welke breakpoint-bereiken
Gebruik altijd de bestaande tokens uit src/assets/styles/breakpoints.ts:
smMax (640px) — kleinste viewport
mdMin (641px) — vanaf medium
mdMax (1007px) — bovengrens medium
lgMin (1008px) — vanaf large
Bereik-conventie:
- sm only:
(max-width: ${smMax})
- md only:
(min-width: ${mdMin}) and (max-width: ${mdMax})
- lg only:
(min-width: ${lgMin})
- md+:
(min-width: ${mdMin}) — gebruik alleen als de waarde voor md én lg gelijk is
- sm+md (niet lg):
(max-width: ${mdMax})
@container versus @media
@container: voorkeursroute voor componenten die in panes/wrappers leven — reageert op de container-breedte ipv viewport. Vereist container-type: inline-size op de host of een ancestor.
@media: alleen waar de viewport echt 't relevante meetpunt is (top-level layout, of als er geen container-context is).
Veel componenten gebruiken beide als fallback: @container layout-container (...) voor componenten binnen een nldd-page/nldd-card, plus @media (...) als fallback voor losstaand gebruik. Beide blokken volgen dezelfde regel — geen base, alleen explicit-per-breakpoint.
Wat blijft buiten breakpoint-queries
Properties die op alle breakpoints hetzelfde zijn:
.foo {
display: flex;
flex-direction: column;
width: 100%;
box-sizing: border-box;
}
Geen probleem met deze base. De regel is alleen: als een property varieert per breakpoint, niet één waarde buiten en de andere binnen — alle waarden binnen, elk in z'n eigen breakpoint.
States en pseudo-classes
Override-semantiek voor non-breakpoint condities (states, attributes, pseudo-classes) blijft normaal:
.button {
background-color: var(--default);
}
.button:hover {
background-color: var(--hover);
}
.button[disabled] {
background-color: var(--disabled);
}
Het anti-override patroon is alleen voor breakpoint-varianten.
Pragmatische uitzondering
Voor properties met veel breakpoint-varianten (bv. heading sizes met 6 levels × 3 breakpoints) wordt de duplicatie aanzienlijk. Als de duplicatie de leesbaarheid eerder schaadt dan helpt — bv. wanneer je elke regel min of meer kopieert met alleen één var-naam-verschil — overweeg dan een tabel-achtige indeling met CSS-custom-properties die je per breakpoint omschakelt:
.foo {
--_padding: var(--sm-padding);
}
@container (min-width: ${mdMin}) {
.foo { --_padding: var(--md-padding); }
}
@container (min-width: ${lgMin}) {
.foo { --_padding: var(--lg-padding); }
}
.foo {
padding: var(--_padding);
}
Deze workaround is acceptabel als de regel-explosie anders te erg wordt. Document de keuze in het component zelf.
Let op: dit voorbeeld breekt bewust de eerder beschreven nesting-conventie (at-rules genest binnen selectors). Bij dit var-swap patroon is het @container blok niet "een eigenschap toevoegen aan deze selector", maar "een hele property-waarde omschakelen voor de hele cascade". Top-level wrapping leest dan duidelijker dan nesting in elke selector die de var leest. Houd het wel binnen één component.
State-conditional layouts (label-alignment, etc.)
Sommige componenten hebben layouts die alleen wisselen op basis van een
combinatie van state en breakpoint. Bijvoorbeeld nldd-form-field gaat
naar row-layout alleen wanneer [label-alignment='left'/'right'] én
viewport ≥ md. Dat is een state-conditional override, geen mobile-first
override. Patroon:
.foo { flex-direction: column; }
:host([variant='wide']) .foo {
@container (min-width: ${mdMin}) {
flex-direction: row;
}
}
De convention is niet van toepassing hier — de base (column) is een
universele default die overal geldt tenzij de state-selector matcht. Dit
mag zo blijven.
Geen overbodige comments
CSS-properties spreken voor zichzelf. Comments alleen voor het uitleggen
van iets niet-vanzelfsprekends (een workaround, browser-quirk, of een
ongebruikelijk patroon). Geen comments die simpelweg herhalen wat de code
doet (bv. "padding per breakpoint" boven een blok met padding-rules).
Checklist bij review