Modern CSS & HTML Standards
Write CSS and HTML using current standards. Every pattern here has shipped in WebKit and Chrome unless explicitly noted otherwise. Avoid legacy hacks when a native solution exists.
How to use this skill
- Read this file first for the quick-reference rules and snippets.
- For deeper detail on a specific area, read the relevant file in
references/:references/snippets.md— 82 old-vs-modern comparisons with browser support percentagesreferences/properties.md— modern CSS properties referencereferences/selectors.md— modern selectors and pseudo-classes/elementsreferences/at-rules.md— modern at-rules (@layer, @scope, @property, @container, etc.)references/functions.md— modern CSS functions (color-mix, clamp, anchor, etc.)references/css-2026.md— features shipping in 2026references/css-2025.md— features that shipped in 2025
Browser support policy
Only use features supported in both WebKit (Safari) and Chrome/Edge (Blink). This means:
- Safe to use (Widely Available, 90%+): ship without fallback
- Use with progressive enhancement (Newly Available, 80–90%): ship with a sensible fallback via
@supports - Do not use by default (Limited, <80% or single-engine only): mention as an option but provide a fallback
When a feature is Chrome-only (e.g. some Interop 2026 items), note it explicitly and provide a cross-browser alternative.
Core rules
Layout
- Centre elements with
display: grid; place-items: center— notposition: absolute+transform: translate(-50%, -50%) - Use
gapfor spacing in flex and grid — not margin hacks on children - Use
aspect-ratio: 16 / 9— not the padding-top percentage hack - Use
inset: 0— nottop: 0; right: 0; bottom: 0; left: 0 - Use
100dvhfor full-viewport height on mobile — not100vh - Use
object-fit: coverfor responsive images — notbackground-image+background-size: cover - Use
position: sticky— not JavaScript scroll listeners for sticky headers - Use
scroll-snap-typeandscroll-snap-align— not carousel JS libraries - Use
overscroll-behavior: contain— not JS wheel event prevention - Use
scrollbar-gutter: stable— notoverflow-y: scrollor hardcoded padding - Use
width: stretch— notcalc(100% - margins)or-webkit-fill-available - Use logical properties (
margin-inline-start,padding-block-end,border-inline) — not physicalleft/rightfor direction-aware layouts - Use
grid-template-areasfor named layout regions — not line number hacks - Use
subgridto align nested grid children to parent tracks — not duplicated column definitions - Use
field-sizing: contentfor auto-growing textareas — not JS resize listeners - Use anchor positioning (
anchor-name,position-anchor,position-area) for tooltips — not Floating UI/Popper.js (progressive enhancement, check support)
Container queries
- Use
@container (width < 400px)for component-level responsiveness — not viewport media queries for component layout - Use
container-type: inline-sizeon the parent
Media queries
- Use range syntax:
@media (600px <= width <= 1200px)— not(min-width: 600px) and (max-width: 1200px)
Typography
- Use
text-wrap: balanceon headings — not manual<br>or JS - Use
clamp()for fluid type:font-size: clamp(1rem, 2.5vw, 2rem)— not multiple media query breakpoints - Use
font-display: swapin@font-face— not invisible text during load - Use variable fonts with
font-weight: 100 900— not multiple@font-facedeclarations per weight - Use
initial-letter: 3for drop caps — notfloat: lefthacks - Use
-webkit-line-clamp/line-clampfor multiline truncation — not JS text slicing - Use
text-box: trim-both cap alphabeticfor optical vertical centering — not padding tweaks (newly available)
Colour
- Use
oklch()for perceptually uniform colour palettes — not hand-picked hex values - Use
color-mix(in oklch, ...)— not Sassmix()orlighten()/darken() - Use relative colour syntax
oklch(from var(--base) calc(l + 0.2) c h)for tints/shades — not hardcoded variants - Use
light-dark()withcolor-scheme: light dark— not duplicated@media (prefers-color-scheme)blocks - Use
accent-colorfor form control theming — notappearance: none+ full rebuild - Use
backdrop-filter: blur()for frosted glass — not pseudo-element image blur hacks
Animation & transitions
- Use individual transform properties (
translate,rotate,scale) — not the combinedtransformshorthand when animating independently - Use
@starting-stylefor entry animations — notrequestAnimationFrametiming hacks - Use
transition-behavior: allow-discreteto animatedisplay: none— notvisibility+opacity+pointer-eventsworkarounds - Use
animation-timeline: view()for scroll-driven animations — not IntersectionObserver JS (progressive enhancement) - Use
linear()easing for custom bounce/spring curves — not JS animation libraries for simple easing - Use View Transitions API (
document.startViewTransition()) for page transitions — not Barba.js - Use
interpolate-size: allow-keywords+height: autotransitions — not JSscrollHeightmeasurement - Use
offset-pathfor motion path animation — not GSAP motionPath plugin
Selectors
- Use
:has()for parent selection — not JavaScript class toggling - Use
:is()and:where()for grouping — not long repetitive selector lists - Use
:focus-visible— not:focus(which triggers on mouse click too) - Use
:user-invalid/:user-validfor form validation — not JS blur + class toggling - Use
@layerfor specificity control — not!importantchains - Use
@scopefor component-scoped styles — not BEM naming or CSS Modules (progressive enhancement)
CSS architecture
- Use native CSS nesting (
& a { }) — not Sass/Less nesting - Use CSS custom properties (
var(--primary)) — not Sass variables - Use
@propertyfor typed custom properties with animation — not untyped--varstrings - Use
@supportsfor feature detection — not Modernizr or JS checks - Use
color-scheme: light darkfor automatic dark mode defaults — not manual input/select restyling - Use
content-visibility: autofor lazy rendering of off-screen content — not IntersectionObserver
HTML
- Use
<dialog>for modals — notdiv.modal+ JS focus trap - Use
closedby="any"on<dialog>for light dismiss — not click-outside JS listeners - Use
<details>/<summary>for accordions — not JS toggle handlers - Use
<details name="group">for exclusive accordions — not JS close-all-others logic - Use
popoverattribute for dropdowns/tooltips — not JS show/hide + z-index management - Use
commandfor/commandattributes for declarative button actions — notonclickhandlers - Use
<input list="..." />+<datalist>for autocomplete — not JS typeahead libraries - Use
<output for="...">for live form output — not JS DOM writes - Use
loading="lazy"on images — not IntersectionObserver lazy loading - Use
inputmode="numeric"andenterkeyhint— not JS mobile keyboard detection - Use
appearance: base-selectfor customisable<select>— not Select2/Choices.js (newly available) - Use
interestfor+popover=hintfor hover tooltips — not JS mouseenter/mouseleave
CSS 2026 features (use with caution — check cross-browser support)
@mixin/@apply— native CSS mixins (Chrome 146 expected, not yet in Safari)- Cross-document view transitions — MPA page transitions via
@view-transition { navigation: auto } - Gap decorations —
column-rule/row-ruleon grid/flex (Chrome 147 expected) reading-flow— keyboard/screen reader order matching visual order- Masonry layout —
grid-template-rows: masonry(Firefox behind flag, Chrome in development) contrast-color()— automatic WCAG-compliant text colour (Firefox 146, Safari 26)
What NOT to write
These patterns are outdated. If you catch yourself writing them, stop and use the modern alternative above:
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)for centeringpadding-top: 56.25%for aspect ratiosheight: 100vhon mobile (use100dvh)overflow-y: scrollto prevent layout shift (usescrollbar-gutter: stable)- Sass variables, nesting,
mix(),lighten(),darken()when native CSS alternatives exist @media (min-width: ...) and (max-width: ...)verbose syntax- Float-based layouts
-webkit-prefixed properties where the unprefixed version is widely available- jQuery or JS for: accordions, modals, tooltips, dropdowns, lazy loading, scroll snapping, form validation styling, dark mode detection, feature detection