CSS comparisons
69 old vs modern CSS techniques, side by side.
Browser compatibility:
Layout 24 snippets
Layout
Advanced
Path shapes without SVG clip paths
Old
clip-path: path("M 0 0 L 800 0...");
/* px values — not responsive */
/* px values — not responsive */
Modern
clip-path: shape(
from 0% 0%, line to 100% 0%,
curve to 0% 80% via 50% 105%);
see modern →
from 0% 0%, line to 100% 0%,
curve to 0% 80% via 50% 105%);
Layout
Beginner
Scaling elements without transform hacks
Old
transform: scale(0.5);
margin-bottom: -50%; /* hack */
margin-bottom: -50%; /* hack */
Modern
.thumb { zoom: 0.5; }
see modern →
Layout
Beginner
Preventing layout shift from scrollbar appearance
Old
body { overflow-y: scroll; }
/* or hardcode the scrollbar width */
body { padding-right: 17px; }
/* or hardcode the scrollbar width */
body { padding-right: 17px; }
Modern
body {
scrollbar-gutter: stable;
}
/* scrollbar space always reserved */
see modern →
scrollbar-gutter: stable;
}
/* scrollbar space always reserved */
Layout
Beginner
Media query ranges without min-width and max-width
Old
@media (min-width: 600px)
and (max-width: 1200px) {
/* styles */
}
and (max-width: 1200px) {
/* styles */
}
Modern
@media (600px <= width <= 1200px) {
/* styles */
}
see modern →
/* styles */
}
Layout
Beginner
Preventing scroll chaining without JavaScript
Old
// JS: block page scroll when inside modal
modal.addEventListener('wheel', e =>
e.preventDefault(), { passive: false })
modal.addEventListener('wheel', e =>
e.preventDefault(), { passive: false })
Modern
.modal-content {
overflow-y: auto;
overscroll-behavior: contain;
}
/* page stays still */
see modern →
overflow-y: auto;
overscroll-behavior: contain;
}
/* page stays still */
Layout
Beginner
Responsive images without the background-image hack
Old
.card-image {
background-image: url(...);
background-size: cover;
background-position: center;
}
background-image: url(...);
background-size: cover;
background-position: center;
}
Modern
img {
object-fit: cover;
width: 100%;
height: 200px;
}
see modern →
object-fit: cover;
width: 100%;
height: 200px;
}
Layout
Beginner
Scrollbar styling without -webkit- pseudo-elements
Old
/* webkit only */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-thumb { background: #888; }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-thumb { background: #888; }
Modern
* {
scrollbar-width: thin;
scrollbar-color: #888 transparent;
}
see modern →
scrollbar-width: thin;
scrollbar-color: #888 transparent;
}
Layout
Beginner
Mobile viewport height without the 100vh hack
Old
.hero {
height: 100vh;
}
/* overflows on mobile */
height: 100vh;
}
/* overflows on mobile */
Modern
.hero {
height: 100dvh;
}
/* adapts to browser chrome */
see modern →
height: 100dvh;
}
/* adapts to browser chrome */
Layout
Beginner
Auto-growing textarea without JavaScript
Old
// JS: resize on every keystroke
el.addEventListener('input', () => {
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px'; })
el.addEventListener('input', () => {
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px'; })
Modern
textarea {
field-sizing: content;
min-height: 3lh;
}
/* grows with content, no JS */
see modern →
field-sizing: content;
min-height: 3lh;
}
/* grows with content, no JS */
Layout
Beginner
Corner shapes beyond rounded borders
Old
.card {
clip-path: polygon(
... /* 20+ points */
);
}
clip-path: polygon(
... /* 20+ points */
);
}
Modern
.card {
border-radius: 2em;
corner-shape: squircle;
}
see modern →
border-radius: 2em;
corner-shape: squircle;
}
Layout
Beginner
Filling available space without calc workarounds
Old
.full {
width: calc(100% - 40px);
/* or width: 100% and overflow */
}
width: calc(100% - 40px);
/* or width: 100% and overflow */
}
Modern
.full {
width: stretch;
}
/* fills container, keeps margins */
see modern →
width: stretch;
}
/* fills container, keeps margins */
Layout
Advanced
Carousel navigation without a JavaScript library
Old
// Swiper.js or Slick carousel
new Swiper('.carousel', {
navigation: { /* … */ },
pagination: { /* … */ },
});
new Swiper('.carousel', {
navigation: { /* … */ },
pagination: { /* … */ },
});
Modern
.carousel::scroll-button(right) {
content: "➡";
}
.carousel li::scroll-marker {
content: '';
}
see modern →
content: "➡";
}
.carousel li::scroll-marker {
content: '';
}
Layout
Intermediate
Hover tooltips without JavaScript events
Old
// JS: mouseenter + mouseleave
btn.addEventListener('mouseenter',
() => showTooltip())
/* + focus, blur, positioning */
btn.addEventListener('mouseenter',
() => showTooltip())
/* + focus, blur, positioning */
Modern
<button interestfor="tip">Hover me</button>
<div id="tip" popover=hint>
Tooltip content
</div>
see modern →
<div id="tip" popover=hint>
Tooltip content
</div>
Layout
Beginner
Positioning shorthand without four properties
Old
.overlay {
top: 0; right: 0;
bottom: 0; left: 0;
}
top: 0; right: 0;
bottom: 0; left: 0;
}
Modern
.overlay {
position: absolute;
inset: 0;
}
see modern →
position: absolute;
inset: 0;
}
Layout
Advanced
Tooltip positioning without JavaScript
Old
/* Popper.js / Floating UI: compute rect,
position: fixed, update on scroll */
.tooltip { position: fixed; }
position: fixed, update on scroll */
.tooltip { position: fixed; }
Modern
.trigger { anchor-name: --tip; }
.tooltip {
position-anchor: --tip;
top: anchor(bottom);
}
see modern →
.tooltip {
position-anchor: --tip;
top: anchor(bottom);
}
Layout
Intermediate
Scroll snapping without a carousel library
Old
// Slick, Swiper, or scroll/touch JS
$('.carousel').slick({ … })
touchstart / scroll handlers
$('.carousel').slick({ … })
touchstart / scroll handlers
Modern
.carousel { scroll-snap-type: x mandatory; }
.carousel > * { scroll-snap-align: start; }
/* no lib, no touch handlers */
see modern →
.carousel > * { scroll-snap-align: start; }
/* no lib, no touch handlers */
Layout
Intermediate
Direction-aware layouts without left and right
Old
margin-left: 1rem;
padding-right: 1rem;
[dir="rtl"] .box { margin-right: ... }
padding-right: 1rem;
[dir="rtl"] .box { margin-right: ... }
Modern
margin-inline-start: 1rem;
padding-inline-end: 1rem;
border-block-start: 1px solid;
see modern →
padding-inline-end: 1rem;
border-block-start: 1px solid;
Layout
Beginner
Naming grid areas without line numbers
Old
float: left; /* clearfix, margins */
grid-column: 1 / 3;
grid-row: 2;
grid-column: 1 / 3;
grid-row: 2;
Modern
.layout {
display: grid;
grid-template-areas: "header header" "sidebar main" "footer footer";
}
see modern →
display: grid;
grid-template-areas: "header header" "sidebar main" "footer footer";
}
Layout
Advanced
Aligning nested grids without duplicating tracks
Old
.child-grid {
grid-template-columns: 1fr 1fr 1fr;
/* duplicate parent tracks */
}
grid-template-columns: 1fr 1fr 1fr;
/* duplicate parent tracks */
}
Modern
.child-grid {
display: grid;
grid-template-columns: subgrid;
}
see modern →
display: grid;
grid-template-columns: subgrid;
}
Layout
Beginner
Spacing elements without margin hacks
Old
.grid > * { margin-right: 16px; }
.grid > *:last-child { margin-right: 0; }
.grid > *:last-child { margin-right: 0; }
Modern
.grid {
display: flex;
gap: 16px;
}
see modern →
display: flex;
gap: 16px;
}
Layout
Beginner
Aspect ratios without the padding hack
Old
.wrapper { padding-top: 56.25%; position: relative; }
.inner { position: absolute; inset: 0; }
.inner { position: absolute; inset: 0; }
Modern
.video-wrapper {
aspect-ratio: 16 / 9;
}
see modern →
aspect-ratio: 16 / 9;
}
Layout
Beginner
Sticky headers without JavaScript scroll listeners
Old
// JS: scroll listener + getBoundingClientRect
// then add/remove .fixed class
.header.fixed { position: fixed; }
// then add/remove .fixed class
.header.fixed { position: fixed; }
Modern
.header {
position: sticky;
top: 0;
}
see modern →
position: sticky;
top: 0;
}
Layout
Intermediate
Responsive components without media queries
Old
@media (max-width: 768px) {
.card { … }
}
/* viewport, not container */
.card { … }
}
/* viewport, not container */
Modern
@container (width < 400px) {
.card { flex-direction: column; }
}
see modern →
.card { flex-direction: column; }
}
Layout
Beginner
Centering elements without the transform hack
Old
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%,-50%);
top: 50%; left: 50%;
transform: translate(-50%,-50%);
Modern
.parent {
display: grid;
place-items: center;
}
see modern →
display: grid;
place-items: center;
}
Animation 11 snippets
Animation
Beginner
Reduced motion without JavaScript detection
Old
const mq = window.matchMedia('(prefers-reduced-motion)');
if (mq.matches) el.style.animation = 'none';
if (mq.matches) el.style.animation = 'none';
Modern
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; }
}
see modern →
* { animation-duration: 0.01ms !important; }
}
Animation
Intermediate
Custom easing curves without cubic-bezier guessing
Old
/* JS animation library */
anime({ targets: el,
easing: 'easeOutBounce' });
anime({ targets: el,
easing: 'easeOutBounce' });
Modern
transition: transform 1s
linear(0, 1.2, 0.9, 1.05, 1);
/* bounce — no JS needed */
see modern →
linear(0, 1.2, 0.9, 1.05, 1);
/* bounce — no JS needed */
Animation
Beginner
Smooth height auto animations without JavaScript
Old
// measure, set px, then snap to auto
el.style.height = el.scrollHeight + 'px';
el.addEventListener('transitionend', ...)
el.style.height = el.scrollHeight + 'px';
el.addEventListener('transitionend', ...)
Modern
:root { interpolate-size: allow-keywords; }
.accordion { height: 0; overflow: hidden;
transition: height .3s ease; }
.accordion.open { height: auto; }
see modern →
.accordion { height: 0; overflow: hidden;
transition: height .3s ease; }
.accordion.open { height: auto; }
Animation
Intermediate
Sticky & snapped element styling without JavaScript
Old
window.addEventListener(
'scroll', () => {
/* check position */
});
'scroll', () => {
/* check position */
});
Modern
@container scroll-state(
stuck: top
) {
.header { ... }
}
see modern →
stuck: top
) {
.header { ... }
}
Animation
Advanced
Responsive clip paths without SVG
Old
.shape {
clip-path: path(
'M0 200 L100 0...'
);
}
clip-path: path(
'M0 200 L100 0...'
);
}
Modern
.shape {
clip-path: shape(
from 0% 100%, ...
);
}
see modern →
clip-path: shape(
from 0% 100%, ...
);
}
Animation
Intermediate
Staggered animations without nth-child hacks
Old
li:nth-child(1) { --i: 0; }
li:nth-child(2) { --i: 1; }
li:nth-child(3) { --i: 2; }
/* repeat for every item… */
li:nth-child(2) { --i: 1; }
li:nth-child(3) { --i: 2; }
/* repeat for every item… */
Modern
li {
transition-delay:
calc(0.1s * (sibling-index() - 1));
}
see modern →
transition-delay:
calc(0.1s * (sibling-index() - 1));
}
Animation
Beginner
Independent transforms without the shorthand
Old
.icon { transform: translateX(10px) rotate(45deg) scale(1.2); }
/* change one = rewrite all */
/* change one = rewrite all */
Modern
.icon {
translate: 10px 0;
rotate: 45deg;
scale: 1.2;
}
/* animate any one without touching the rest */
see modern →
translate: 10px 0;
rotate: 45deg;
scale: 1.2;
}
/* animate any one without touching the rest */
Animation
Intermediate
Animating display none without workarounds
Old
// wait for transitionend then display:none
el.addEventListener('transitionend', …)
visibility + opacity + pointer-events
el.addEventListener('transitionend', …)
visibility + opacity + pointer-events
Modern
.panel { transition: opacity .2s, overlay .2s;
transition-behavior: allow-discrete; }
.panel.hidden { opacity: 0; display: none; }
/* no JS wait or visibility hack */
see modern →
transition-behavior: allow-discrete; }
.panel.hidden { opacity: 0; display: none; }
/* no JS wait or visibility hack */
Animation
Intermediate
Entry animations without JavaScript timing
Old
// add class after paint
requestAnimationFrame(() => {
el.classList.add('visible');
});
requestAnimationFrame(() => {
el.classList.add('visible');
});
Modern
.card { transition: opacity .3s, transform .3s; }
.card { @starting-style { opacity: 0; transform: translateY(10px); } }
/* no rAF/setTimeout */
see modern →
.card { @starting-style { opacity: 0; transform: translateY(10px); } }
/* no rAF/setTimeout */
Animation
Advanced
Page transitions without a framework
Old
// Barba.js or React Transition Group
Barba.init({ … })
transition hooks + duration state
Barba.init({ … })
transition hooks + duration state
Modern
document.startViewTransition(() => updateDOM());
.hero { view-transition-name: hero; }
/* no Barba, no React TG */
see modern →
.hero { view-transition-name: hero; }
/* no Barba, no React TG */
Animation
Advanced
Scroll-linked animations without a library
Old
// JS + IntersectionObserver
observer.observe(el)
el.style.opacity = …
observer.observe(el)
el.style.opacity = …
Modern
animation-timeline: view();
animation-range: entry;
/* pure CSS, GPU-accelerated */
see modern →
animation-range: entry;
/* pure CSS, GPU-accelerated */
Color 8 snippets
Colors
Beginner
Readable text without manual contrast checks
Old
/* Pick white or black manually */
color: white; /* hardcoded */
color: white; /* hardcoded */
Modern
color: contrast-color(var(--bg));
see modern →
Color
Intermediate
Perceptually uniform colors with oklch
Old
--brand: #4f46e5;
--brand-light: #818cf8;
--brand-dark: #3730a3;
/* guess-and-check each shade */
--brand-light: #818cf8;
--brand-dark: #3730a3;
/* guess-and-check each shade */
Modern
--brand: oklch(0.55 0.2 264);
--brand-light: oklch(0.75 0.2 264);
--brand-dark: oklch(0.35 0.2 264);
/* only L changes, same perceived hue */
see modern →
--brand-light: oklch(0.75 0.2 264);
--brand-dark: oklch(0.35 0.2 264);
/* only L changes, same perceived hue */
Color
Intermediate
Frosted glass effect without opacity hacks
Old
.card::before { content: '';
background-image: url(bg.jpg);
filter: blur(12px);
z-index: -1; }
background-image: url(bg.jpg);
filter: blur(12px);
z-index: -1; }
Modern
.glass {
backdrop-filter: blur(12px);
background: rgba(255,255,255,.1);
}
see modern →
backdrop-filter: blur(12px);
background: rgba(255,255,255,.1);
}
Color
Intermediate
Vivid colors beyond sRGB
Old
.hero {
color: rgb(200, 80, 50);
}
/* sRGB only, washed on P3 */
color: rgb(200, 80, 50);
}
/* sRGB only, washed on P3 */
Modern
.hero {
color: oklch(0.7 0.25 29);
}
/* or color(display-p3 1 0.2 0.1) */
see modern →
color: oklch(0.7 0.25 29);
}
/* or color(display-p3 1 0.2 0.1) */
Color
Advanced
Color variants without Sass functions
Old
/* Sass: lighten($brand, 20%), darken($brand, 10%) */
.btn { background: #e0e0e0; }
.btn { background: #e0e0e0; }
Modern
.btn {
background: oklch(from var(--brand) calc(l + 0.2) c h);
}
see modern →
background: oklch(from var(--brand) calc(l + 0.2) c h);
}
Color
Intermediate
Dark mode colors without duplicating values
Old
@media (prefers-color-scheme: dark) {
color: #eee;
}
color: #eee;
}
Modern
color: light-dark(#111, #eee);
color-scheme: light dark;
see modern →
color-scheme: light dark;
Color
Beginner
Styling form controls without rebuilding them
Old
appearance: none;
// + 20+ lines of custom box/border/background
// + 20+ lines of custom box/border/background
Modern
input[type="checkbox"],
input[type="radio"] {
accent-color: #7c3aed;
}
see modern →
input[type="radio"] {
accent-color: #7c3aed;
}
Colors
Intermediate
Mixing colors without a preprocessor
Old
// Sass required
$blend: mix(
$blue, $pink, 60%);
$blend: mix(
$blue, $pink, 60%);
Modern
background: color-mix(
in oklch, #3b82f6,
#ec4899);
see modern →
in oklch, #3b82f6,
#ec4899);
Selector 7 snippets
Selectors
Advanced
Text highlighting without DOM manipulation
Old
el.innerHTML = el.innerHTML
.replace(/term/g, '<mark>$&</mark>');
.replace(/term/g, '<mark>$&</mark>');
Modern
::highlight(search) {
background: yellow;
}
see modern →
background: yellow;
}
Selector
Beginner
Form validation styles without JavaScript
Old
// JS: add .touched on blur
el.addEventListener('blur', () =>
el.classList.add('touched'))
/* .touched:invalid { color: red } */
el.addEventListener('blur', () =>
el.classList.add('touched'))
/* .touched:invalid { color: red } */
Modern
input:user-invalid {
border-color: red;
}
input:user-valid {
border-color: green;
}
see modern →
border-color: red;
}
input:user-valid {
border-color: green;
}
Selector
Intermediate
Scroll spy without IntersectionObserver
Old
const observer = new
IntersectionObserver(cb);
/* 15+ lines of JS */
IntersectionObserver(cb);
/* 15+ lines of JS */
Modern
nav a:target-current {
color: var(--accent);
}
see modern →
color: var(--accent);
}
Selector
Intermediate
Low-specificity resets without complicated selectors
Old
.reset ul, .reset ol { ... }
/* or (0,0,1) specificity, still wins */
/* or (0,0,1) specificity, still wins */
Modern
:where(ul, ol) {
margin: 0;
padding-inline-start: 1.5rem;
}
see modern →
margin: 0;
padding-inline-start: 1.5rem;
}
Selector
Beginner
Grouping selectors without repetition
Old
.card h1, .card h2, .card h3, .card h4 {
margin-bottom: 0.5em;
}
margin-bottom: 0.5em;
}
Modern
.card :is(h1, h2, h3, h4) {
margin-bottom: 0.5em;
}
see modern →
margin-bottom: 0.5em;
}
Selector
Beginner
Focus styles without annoying mouse users
Old
:focus { outline: 2px solid blue; }
// Shows on mouse click too, or people remove it (a11y fail)
// Shows on mouse click too, or people remove it (a11y fail)
Modern
:focus-visible {
outline: 2px solid var(--focus-color);
}
see modern →
outline: 2px solid var(--focus-color);
}
Selectors
Intermediate
Selecting parent elements without JavaScript
Old
// JavaScript required
el.closest('.parent')
.classList.add(…)
el.closest('.parent')
.classList.add(…)
Modern
.card:has(img) {
grid-template: auto 1fr;
}
see modern →
grid-template: auto 1fr;
}
Typography 7 snippets
Typography
Beginner
Vertical text centering without padding hacks
Old
.btn {
padding: 10px 20px;
/* looks off-center, tweak top/bottom */
padding-top: 8px; /* hack */
}
padding: 10px 20px;
/* looks off-center, tweak top/bottom */
padding-top: 8px; /* hack */
}
Modern
h1, button {
text-box: trim-both cap alphabetic;
}
/* true optical centering */
see modern →
text-box: trim-both cap alphabetic;
}
/* true optical centering */
Typography
Beginner
Multiline text truncation without JavaScript
Old
/* JS: slice text by chars/words, add "..." */
.card-title { overflow: hidden; }
.card-title { overflow: hidden; }
Modern
.card-title {
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
}
see modern →
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
}
Typography
Beginner
Drop caps without float hacks
Old
.drop-cap::first-letter {
float: left;
font-size: 3em; line-height: 1;
}
float: left;
font-size: 3em; line-height: 1;
}
Modern
.drop-cap::first-letter {
-webkit-initial-letter: 3;
initial-letter: 3;
}
see modern →
-webkit-initial-letter: 3;
initial-letter: 3;
}
Typography
Beginner
Balanced headlines without manual line breaks
Old
// manual <br> or Balance-Text.js
h1 { text-align: center; }
.balance-text /* JS lib */
h1 { text-align: center; }
.balance-text /* JS lib */
Modern
h1, h2 {
text-wrap: balance;
}
/* no br or JS */
see modern →
text-wrap: balance;
}
/* no br or JS */
Typography
Beginner
Font loading without invisible text
Old
@font-face { ... }
/* Default: invisible text until load */
/* Default: invisible text until load */
Modern
@font-face {
font-family: "MyFont";
font-display: swap;
}
see modern →
font-family: "MyFont";
font-display: swap;
}
Typography
Intermediate
Multiple font weights without multiple files
Old
@font-face { font-weight: 400; }
@font-face { font-weight: 700; }
/* 4+ files */
@font-face { font-weight: 700; }
/* 4+ files */
Modern
@font-face {
font-family: "MyVar";
src: url("MyVar.woff2");
font-weight: 100 900;
}
see modern →
font-family: "MyVar";
src: url("MyVar.woff2");
font-weight: 100 900;
}
Typography
Intermediate
Fluid typography without media queries
Old
h1 { font-size: 1rem; }
@media (min-width: 600px) { h1 { font-size: 1.5rem; } }
@media (min-width: 900px) { h1 { font-size: 2rem; } }
@media (min-width: 600px) { h1 { font-size: 1.5rem; } }
@media (min-width: 900px) { h1 { font-size: 2rem; } }
Modern
h1 {
font-size: clamp(1rem, 2.5vw, 2rem);
}
see modern →
font-size: clamp(1rem, 2.5vw, 2rem);
}
Workflow 12 snippets
Workflow
Beginner
CSS feature detection without JavaScript
Old
// Modernizr or manual check
if (CSS.supports('display', 'grid')) {
el.classList.add('grid');
}
if (CSS.supports('display', 'grid')) {
el.classList.add('grid');
}
Modern
@supports (display: grid) {
.layout { display: grid; }
}
/* no JS required */
see modern →
.layout { display: grid; }
}
/* no JS required */
Workflow
Advanced
Range style queries without multiple blocks
Old
/* Multiple style() blocks */
@container style(--p: 51%) {}
@container style(--p: 52%) {}
/* ...for each value */
@container style(--p: 51%) {}
@container style(--p: 52%) {}
/* ...for each value */
Modern
@container style(
--progress > 50%
) {
.bar { ... }
}
see modern →
--progress > 50%
) {
.bar { ... }
}
Workflow
Intermediate
Typed attribute values without JavaScript
Old
// JS reading dataset
el.style.width =
el.dataset.pct + '%';
el.style.width =
el.dataset.pct + '%';
Modern
.bar {
width: attr(
data-pct type(<percentage>)
);
}
see modern →
width: attr(
data-pct type(<percentage>)
);
}
Workflow
Intermediate
Inline conditional styles without JavaScript
Old
// JavaScript toggling
el.classList.toggle(
'primary', isPrimary
);
el.classList.toggle(
'primary', isPrimary
);
Modern
.btn {
background: if(
style(--variant: primary):
blue; else: gray
);
}
see modern →
background: if(
style(--variant: primary):
blue; else: gray
);
}
Workflow
Intermediate
Reusable CSS logic without Sass mixins
Old
// Sass function
@function fluid($min, $max) {
@return clamp(...);
}
@function fluid($min, $max) {
@return clamp(...);
}
Modern
@function --fluid(
--min, --max
) {
@return clamp(...);
}
see modern →
--min, --max
) {
@return clamp(...);
}
Workflow
Intermediate
Lazy rendering without IntersectionObserver
Old
// JS IntersectionObserver
new IntersectionObserver(
(entries) => { /* render */ }
).observe(el);
new IntersectionObserver(
(entries) => { /* render */ }
).observe(el);
Modern
.section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
see modern →
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
Workflow
Advanced
Scoped styles without BEM naming
Old
// BEM: .card__title, .card__body
.card__title { … }
.card__body { … }
// or CSS Modules / styled-components */
.card__title { … }
.card__body { … }
// or CSS Modules / styled-components */
Modern
@scope (.card) {
.title { font-size: 1.25rem; }
.body { color: #444; }
}
/* .title only inside .card */
see modern →
.title { font-size: 1.25rem; }
.body { color: #444; }
}
/* .title only inside .card */
Workflow
Advanced
Typed custom properties without JavaScript
Old
// --hue was a string, no animation
:root { --hue: 0; }
hsl(var(--hue), …) /* no interpolation */
:root { --hue: 0; }
hsl(var(--hue), …) /* no interpolation */
Modern
@property --hue {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
/* animatable, validated */
see modern →
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
/* animatable, validated */
Workflow
Beginner
Dark mode defaults without extra CSS
Old
@media (prefers-color-scheme: dark) {
input, select, textarea { ... }
}
input, select, textarea { ... }
}
Modern
:root {
color-scheme: light dark;
}
see modern →
color-scheme: light dark;
}
Workflow
Intermediate
Controlling specificity without !important
Old
.card .title { ... }
.page .card .title { ... }
.page .card .title.special { color: red !important; }
.page .card .title { ... }
.page .card .title.special { color: red !important; }
Modern
@layer base, components, utilities;
@layer utilities { .mt-4 { margin-top: 1rem; } }
see modern →
@layer utilities { .mt-4 { margin-top: 1rem; } }
Workflow
Beginner
Theme variables without a preprocessor
Old
// Sass: $primary: #7c3aed;
// Compiles to static #7c3aed
.btn { background: $primary; }
// Compiles to static #7c3aed
.btn { background: $primary; }
Modern
:root {
--primary: #7c3aed;
}
.btn { background: var(--primary); }
see modern →
--primary: #7c3aed;
}
.btn { background: var(--primary); }
Workflow
Beginner
Nesting selectors without Sass or Less
Old
// requires Sass compiler
.nav {
& a { color: #888; }
}
.nav {
& a { color: #888; }
}
Modern
.nav {
& a { color: #888; }
}
/* plain .css, no build */
see modern →
& a { color: #888; }
}
/* plain .css, no build */