Layout comparisons
28 old vs modern layout CSS techniques, side by side.
Browser compatibility:
Layout
Beginner
Exclusive accordions without JavaScript
Old
// JS: close all, open clicked
details.forEach(d => d.open = false);
this.open = true;
details.forEach(d => d.open = false);
this.open = true;
Modern
<details name="faq">...</details>
<details name="faq">...</details>
/* browser closes others */
see modern →
<details name="faq">...</details>
/* browser closes others */
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
Modal controls without onclick handlers
Old
<button onclick="
document.querySelector('#dlg')
.showModal()">Open</button>
document.querySelector('#dlg')
.showModal()">Open</button>
Modern
<button commandfor="dlg"
command="show-modal">Open</button>
<dialog id="dlg">...</dialog>
see modern →
command="show-modal">Open</button>
<dialog id="dlg">...</dialog>
Layout
Beginner
Dialog light dismiss without click-outside listeners
Old
// JS: listen for click on ::backdrop
dialog.addEventListener('click',
(e) => { /* check bounds */ })
dialog.addEventListener('click',
(e) => { /* check bounds */ })
Modern
<dialog closedby="any">
Click outside to close
</dialog>
/* no JS listeners */
see modern →
Click outside to close
</dialog>
/* no JS listeners */
Layout
Intermediate
Customizable selects without a JavaScript library
Old
// Select2 or Choices.js
new Choices('#my-select');
/* rebuilds entire DOM */
new Choices('#my-select');
/* rebuilds entire DOM */
Modern
select,
select ::picker(select) {
appearance: base-select;
}
see modern →
select ::picker(select) {
appearance: base-select;
}
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
Beginner
Dropdown menus without JavaScript toggles
Old
.menu { display: none; }
.menu.open { display: block; }
/* + JS: click, clickOutside, ESC, aria */
.menu.open { display: block; }
/* + JS: click, clickOutside, ESC, aria */
Modern
button[popovertarget=menu] { }
#menu[popover] {
position: absolute;
}
see modern →
#menu[popover] {
position: absolute;
}
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
Intermediate
Modal dialogs without a JavaScript library
Old
.overlay { position: fixed; z-index: 999; }
/* + JS: open/close, ESC, focus trap */
/* + JS: open/close, ESC, focus trap */
Modern
dialog {
padding: 1rem;
}
dialog::backdrop { background: rgb(0 0 0 / .5); }
see modern →
padding: 1rem;
}
dialog::backdrop { background: rgb(0 0 0 / .5); }
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;
}