Modern CSS
Modern CSS rules for creating robust, responsive and accessible UIs.
The rules work best when you apply a progressive enhancement approach. The CSS features are within Baseline Newly Available. Thanks to Interop, most are within Widely Available.
The rules, constraints, and examples are not intended to be prescriptive. Use your best judgment and consider the specific needs of the project when applying them. However, if you use a rule, apply it consistently.
Rules
Architecture
Organizing styles (@layer)
- Rule: Use
@layerto organize groups of styles. - Constraint: Avoid global styles outside of layers.
- Rationale: Prevents style conflicts through managing specificity.
- References:
@layeron MDN. - Example:
@layer elements, components;
@layer elements {
a {
text-decoration-skip-ink: auto;
}
}
Encapsulating styles (@scope)
- Rule: Use
@scopeto encapsulate styles. - Constraint: Avoid custom element and components styles that are outside of a scope.
- Rationale: Prevents styles from bleeding into other components because selectors are scoped to the component.
- References:
@scopeon MDN. - Example:
@scope (.card) {
h2 {
font-size: var(--large);
}
}
Nesting rules and at-rules (&)
- Rule: Use
&for nesting rules and at-rules. - Constraint: Avoid unnested selectors (e.g.
a {} a:hover {}). - Rationale: Clarifies the relationship between the nested selector and the parent selector.
- References:
&nesting selector on MDN. - Example:
a {
color: var(--blue);
&:hover {
text-decoration: underline;
}
@container (width > 20em ) {
place-self: center;
}
}
Relational styles (:has())
- Rule: Use
:has()for relational styles. - Constraint: Avoid
.has--like class names (e.g..has-img {}). - Rationale: Creates “smart” components that adapt to relationships.
- References:
:has()on MDN. - Example:
.card {
&:has(img) {
grid-template-rows: auto 1fr;
}
}
Additive properties (:not() and 20em < width <= 40em ))
- Rule: Use
:not()and ranged queries (e.g.@media (20em < width <= 40em)) to create additive styles. - Constraint: Avoid overriding styles (e.g.
div { margin: 1rem; &:first-child { margin-block-start: 0; } }) - Rationale: Simplifies the mental model because you don’t have to keep track of which styles are being overridden.
- References:
:not()on MDN, media query range syntax on MDN. - Example:
.card {
/* Apply to all conditions */
color: red;
/* Apply based on the selector */
&:not(:first-child) {
margin-block-start: var(--medium);
}
/* Apply based on non-overlapping container conditions */
@container example (width <= 20em) {
background-color: var(--primary);
}
@container example (20em < width <= 40em ) {
background-color: var(--secondary);
}
@container example (width > 40em ) {
background-color: var(--tertiary);
}
}
Typography
Fluid type sizes (clamp())
- Rule: Use
clamp()for font sizes to create harmonious rhythmic scales that are appropriate to the screen size, e.g. Major Second (1.125) on narrow viewports and Major Third (1.25) wide ones. - Constraint: Avoid fixed font sizes (e.g.,
px,rem) and central values without aremaddition (e.g.clamp(1.75rem, 5cqi, 2.25rem)). - Rationale: Ensures text is appropriately sized across different viewport sizes and can be zoomed for accessibility.
- References:
clamp()on MDN, Responsive design: seams & edges. and Designing with fluid type scales . - Example:
:root {
--medium: clamp(1.3125rem, 1.1821rem + 0.6522cqi, 1.6875rem);
--large: clamp(1.75rem, 1.5761rem + 0.8696cqi, 2.25rem);
}
Widow and orphan words (text-wrap)
- Rule: Use
text-wrapwithprettyorbalanceto avoid widow and orphan words. - Constraint: Avoid default wrapping outside of inputs and text areas.
- Rationale: Improves the readability and aesthetics of text blocks.
- References:
text-wrapon MDN. - Example:
h1,
h2,
h3 {
text-wrap: balance;
}
p {
text-wrap: pretty;
}
Colors
Perceptual uniform lightness (oklch())
- Rule: Use
oklch()for all colors. - Constraint: Avoid
hex,rgb(),hsl()and other color formats. - Rationale: Easier to maintain perceptual uniform lightness to ensure text is accessible regardless of background color.
- References:
oklch()on MDN. - Example:
:root {
--success: oklch(40% 0.15 150deg);
--danger: oklch(40% 0.2 25deg);
}
Respecting color preferences (color-scheme)
- Rule: Use
color-schemeandlight-dark()to support color schemes. - Constraint: Avoid hardcoding colors that don’t adapt to light and dark modes.
- Rationale: Improves accessibility by respecting a person’s preference for light or dark mode.
- References:
color-schemeon MDN,light-dark()on MDN. - Example:
body {
color-scheme: light dark;
background-color: light-dark(oklch(98% 0.03 250deg), oklch(14% 0 0deg));
}
Relative color functions (oklch(from /* .. */) & color-mix() )
- Rule: Use relative color syntax (e.g.
oklch(from var(--primary) l + 10%)) and functions (e.g.color-mix()) to create color relationships. - Constraint: Avoid hardcoding colors that relate to other colors.
- Rationale: Creates a cohesive color palette that is easier to maintain and adjust.
- References: relative colors on MDN,
color-mix()on MDN. - Example:
button {
background-color: var(--primary);
&:hover {
background-color: oklch(from var(--primary) l c calc(h - 10deg));
}
}
Layout
Container queries and units (@container, cqi etc.)
- Rule: Use container queries and units (e.g.
cqi,cqb) for responsive layouts. - Constraint: Avoid fixed units for spacing (e.g.
padding-block: 16px,margin-inline: 1rem) as they create hard edges and seams. - Rationale: Improves modularity and reusability as components adapt to their container size, and work across all device sizes not just a few.
- References:
containeron MDN, container query length units on MDN, Responsive design: seams & edges. and Designing with fluid type scales . - Example:
.card {
container: card / inline-size;
padding: 2cqi;
p {
@container card (width > 30cqi) {
place-self: center;
}
}
}
Intrinsic sizing (*-content)
- Rule: Use intrinsic sizing (e.g.
max-inline-size: fit-content,block-size: max-content). - Constraint: Avoid fixed sizes (e.g.
width: 300px,height: 200px) for content elements. - Rationale: Improves flexibility and prevents overflow issues as content determines its own size.
- References:
fit-contenton MDN,max-contenton MDN. - Example:
nav {
max-inline-size: fit-content;
}
Motion
Respecting motion preferences (prefers-reduced-motion)
- Rule: Use
prefers-reduced-motion: no-preferencewhen applying large animations and transitions. - Constraint: Avoid
prefers-reduced-motion: reduce. - Rationale: Respects a person’s preferences for motion to prevent motion sickness and improve accessibility.
- References:
prefers-reduced-motionon MDN. - Example:
@media (prefers-reduced-motion: no-preference) {
.hero {
animation: bounce-in 0.5s ease;
}
}
Example in practice
This site follows the rules. Study the implementation on GitHub for concrete examples of each:
src/styles.css—@layersrc/variables.css—oklch(),light-dark(),clamp()src/elements.css—&,text-wrap,prefers-reduced-motion,cqisrc/components/signpost/signpost.css—@scope