Modern CSS Cheatsheet
Old patterns replaced with native CSS. 76 comparisons across 6 categories: Layout, Animation, Color, Selectors, Typography, and Workflow. Each row shows what you used to write and what replaces it.
Layout
| Topic | Then | Now | |
|---|---|---|---|
| Centering | position: absolute; top: 50%; transform: translate(-50%, -50%) |
display: grid; place-items: center |
Details → |
| Aspect ratio | padding-bottom: 56.25%; height: 0 |
aspect-ratio: 16 / 9 |
Details → |
| Element spacing | margin on children + :not(:last-child) |
gap: 1rem |
Details → |
| Full-bleed positioning | top: 0; right: 0; bottom: 0; left: 0 |
inset: 0 |
Details → |
| Logical properties | margin-left, padding-top |
margin-inline-start, padding-block |
Details → |
| Nested grid alignment | Duplicate track definitions on child element |
grid-template-columns: subgrid |
Details → |
| Grid area naming | grid-column: 1 / 3; grid-row: 1 |
grid-area: header + grid-template-areas |
Details → |
| Responsive components | @media (min-width: 600px) |
@container (min-width: 400px) |
Details → |
| Sticky elements | JS scroll listener + class toggle |
position: sticky; top: 0 |
Details → |
| Mobile viewport height | height: 100vh (breaks on mobile) |
height: 100svh or dvh |
Details → |
| Viewport units | vw / vh only |
svh, dvh, lvh, svw, dvw |
|
| Scroll chaining | JS wheel/touchmove preventDefault |
overscroll-behavior: contain |
Details → |
| Scrollbar layout shift | overflow-y: scroll or hardcoded padding |
scrollbar-gutter: stable |
Details → |
| Lazy rendering | IntersectionObserver + visibility toggle |
content-visibility: auto |
Details → |
| Element scaling | transform: scale(0.5) + negative margins |
zoom: 0.5 |
Details → |
| Clip-path shapes | clip-path: path("M 0 0...") — px only, not responsive |
clip-path: shape(from 0% 0%, line to 100% 0%, ...) |
Details → |
| Responsive clip paths | SVG <clipPath> + clipPathUnits markup |
clip-path: polygon(0 0, 100% 0, ...) |
Details → |
| Non-round corners | clip-path polygon() hacks |
corner-shape: squircle / notch / scoop |
Details → |
| Image fit | background-image + background-size: cover |
object-fit: cover |
Details → |
| Fill available space | width: calc(100% - 48px) |
width: stretch |
Details → |
| Tooltip positioning | Popper.js / JS getBoundingClientRect |
anchor-name + position-anchor |
Details → |
| Frosted glass | Pseudo-element blurred background copy |
backdrop-filter: blur(12px) |
Details → |
Animation
| Topic | Then | Now | |
|---|---|---|---|
| Scroll animations | IntersectionObserver + class toggle |
animation-timeline: view() |
Details → |
| Scroll snapping | Swiper.js / custom carousel JS |
scroll-snap-type: x mandatory |
Details → |
| Page transitions | JS framework (GSAP / Barba.js) |
view-transition-name + @view-transition |
Details → |
| Entry animations | requestAnimationFrame / setTimeout delay |
@starting-style { opacity: 0 } |
Details → |
| Animating display: none | JS class + visibility workaround |
transition-behavior: allow-discrete |
Details → |
| Independent transforms | transform: rotate(45deg) scale(1.2) |
rotate: 45deg; scale: 1.2; translate |
Details → |
| Height auto animation | JS scrollHeight measurement |
interpolate-size: allow-keywords; height: auto |
Details → |
| Custom easing | cubic-bezier() trial and error |
linear(0, 0.5 50%, 1) |
Details → |
| Staggered animations | nth-child + hardcoded delay per item |
animation-delay: calc(sibling-index() * 100ms) |
Details → |
| Scroll spy | IntersectionObserver + JS active class |
:target-current { color: active } |
Details → |
| Carousel navigation | Swiper.js prev/next button handlers |
::scroll-button(next); ::scroll-marker |
Details → |
Color
| Topic | Then | Now | |
|---|---|---|---|
| Color mixing | Sass mix() / darken() |
color-mix(in oklch, blue 60%, white) |
Details → |
| Perceptually uniform colors | #hex / hsl() — non-uniform steps |
oklch(0.55 0.2 264) |
Details → |
| Dark mode colors | Duplicate rules in @media prefers-color-scheme |
color: light-dark(#111, #eee) |
Details → |
| Dark mode defaults | @media (prefers-color-scheme: dark) |
color-scheme: light dark |
Details → |
| Color variants | Sass lighten() / darken() |
oklch(from var(--brand) calc(l + 0.2) c h) |
Details → |
| Wide gamut colors | sRGB hex only |
oklch() / color(display-p3 ...) |
Details → |
| Auto readable text | color: white / black hardcoded per background |
color: contrast-color(var(--bg)) |
Details → |
Selectors
| Topic | Then | Now | |
|---|---|---|---|
| Parent selection | JS classList on parent element |
.form:has(input:invalid) { border: red } |
Details → |
| Selector grouping | h1, h2, h3 { } repeated selectors |
:is(h1, h2, h3) { } |
Details → |
| Zero-specificity resets | Complex specificity tricks |
:where(ul, ol) { margin: 0 } |
Details → |
| Focus styles | :focus { outline } — all input types |
:focus-visible { outline } |
Details → |
| Form validation styles | JS blur event + .touched class |
input:user-valid / :user-invalid |
Details → |
| CSS nesting | Sass / Less nesting |
Native CSS & nesting |
Details → |
| Scoped styles | BEM / CSS Modules |
@scope (.card) { .title { } } |
Details → |
| Text highlighting | innerHTML.replace() wrapping <mark> |
CSS.highlights.set() + ::highlight(search) |
Details → |
| Sticky/snapped state | JS scroll position detection |
@container scroll-state(stuck: top) { } |
Details → |
| Range style queries | Multiple separate @container blocks |
@container style(--n > 50) { } |
Details → |
| Form control styling | appearance: none + full custom rebuild |
accent-color: purple |
Details → |
| Hover tooltips | JS mouseenter / mouseleave events |
popover=hint + interestfor |
Details → |
Typography
| Topic | Then | Now | |
|---|---|---|---|
| Fluid typography | calc() + @media breakpoints per size |
font-size: clamp(1rem, 2.5vw, 2rem) |
Details → |
| Balanced headlines | Manual <br> or max-width trial and error |
text-wrap: balance |
Details → |
| Body text orphans | Default wrapping |
text-wrap: pretty |
|
| Multiline truncation | JS height measurement + appended "..." |
line-clamp: 3; overflow: hidden |
Details → |
| Drop caps | float: left + manual font-size adjustment |
::first-letter { initial-letter: 3 } |
Details → |
| Variable fonts | Separate @font-face per weight |
font-weight: 100 900 (variable font) |
Details → |
| Font loading | FOIT — flash of invisible text |
font-display: swap |
Details → |
| Vertical text centering | padding-top/bottom trial and error |
text-box: trim-both cap alphabetic |
Details → |
Workflow
| Topic | Then | Now | |
|---|---|---|---|
| Theme variables | Sass $variables |
--custom-property + var() |
Details → |
| Specificity control | !important overrides |
@layer base, components, utilities |
Details → |
| Typed custom properties | Untyped --var (no animation support) |
@property --hue { syntax: "<angle>" } |
Details → |
| CSS feature detection | JS feature sniffing |
@supports (display: grid) { } |
Details → |
| Media query ranges | @media (min-width: 600px) and (max-width: 900px) |
@media (600px <= width <= 900px) |
Details → |
| Reusable CSS logic | Sass @mixin |
@function --space($n) { result: calc($n * 8px) } |
Details → |
| Inline conditionals | JS class toggling per variant |
color: if(style(--size: lg): 2rem; else: 1rem) |
Details → |
| Typed attribute values | JS dataset access + parseInt() |
width: attr(data-size type(<length>)) |
Details → |
| Auto-growing textarea | JS oninput + scrollHeight resize |
field-sizing: content |
Details → |
| Scrollbar styling | ::-webkit-scrollbar (non-standard) |
scrollbar-color + scrollbar-width |
Details → |
| Modal dialogs | Custom div + JS focus trap |
<dialog> element |
Details → |
| Dropdown menus | JS click toggle + aria-expanded |
popover API |
Details → |
| Light dismiss | JS click-outside detection |
closedby="any" attribute |
Details → |
| Exclusive accordions | JS accordion open/close logic |
<details name="group"> |
Details → |
| Modal controls | onclick handlers + getElementById |
commandfor + command attributes |
Details → |
| Custom select | Select2 / Choices.js libraries |
appearance: base-select |
Details → |