# modern-css.com — Full Content Reference > Every old CSS hack next to its clean, modern replacement. 85 side-by-side comparisons. Site: https://modern-css.com Author: Naeem Noor (https://naeemnur.com) License: Content is copyright modern-css.com. Code examples are freely usable. --- ## Random values per element without JavaScript URL: https://modern-css.com/random-values-without-javascript/ Category: Animation Difficulty: Intermediate Baseline: limited Browser support: 19% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/random Randomising layout details like rotation, size, or delay used to mean Math.random() in JavaScript and inline style attributes. The CSS random() function generates a random value per element directly in CSS. ### How it works The random(min, max) function takes a minimum and maximum value and returns a random number in that range. Both values must use the same unit, so random(-15deg, 15deg) gives back an angle, and random(0s, 2s) gives back a time. An optional third argument step snaps the result to a multiple of that step. By default, each element that uses random() gets its own independent draw. This replaces the common pattern of looping over elements in JS to set a custom property on each one. The browser handles the per-element distinction without any markup changes. Since this is a CSS Values Level 5 feature, browser support is currently limited to Safari. Use @supports (rotate: random(-1deg, 1deg)) to apply it progressively, with a fixed fallback for other browsers. ### Why use this - No JavaScript at all: Math.random() plus inline style mutations are gone. The browser picks the value per element automatically. - Per-element by default: Each element gets its own independently drawn value. No loop, no forEach, no dataset juggling. - Works in any property: Use it for rotation, delay, size, color lightness, or any numeric value. Units are respected so you get real CSS values back. ### Modern CSS ```css .card { rotate: random(-15deg, 15deg); animation-delay: random(0s, 1s); } ``` --- ## Data-driven layouts without JavaScript charts URL: https://modern-css.com/attr-driven-layouts-without-js-charts/ Category: Layout Difficulty: Advanced Baseline: widely (2015) Browser support: 99% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/attr Small dashboards often pull in a charting library just to draw a few bars. Typed attr() lets CSS read numbers from data-* attributes and size blocks directly so you can skip the JavaScript chart layer. ### How it works The classic bar chart pattern stores a number in a data-* attribute and then copies that value into a --pct custom property with JavaScript. Layout runs off the custom property, not the data itself, so you end up with two sources of truth. Typed attr() extends the long-standing attr() function so it can return non-string values. In this pattern a bar reads attr(data-pct percentage) and uses the result directly for its inline-size, which keeps the data in the markup and lets CSS do the math. Note: attr() is widely supported when used with content, but using attr() on other properties with typed values is still experimental. Support for this pattern is currently limited to Chromium-based browsers, so treat it as progressive enhancement only and keep a custom property or fixed-size fallback for everyone else. ### Why use this - Less glue JavaScript: Instead of copying numbers from data-* attributes into custom properties, CSS reads them directly. You keep the data in one place and let layout respond to it. - Inline dashboards: Typed attr() works well for tiny in-context charts: value bars in cards, capacity meters, or progress indicators. No charting library, no canvas, no SVG boilerplate. - Progressive enhancement: The feature ships first in Chromium browsers. You can gate the behavior behind @supports and keep a custom property based fallback for all other engines. ### Modern CSS ```css .bar-chart { display: grid; gap: 0.5rem; } .bar { block-size: 1.5rem; border-radius: 999px; background: color-mix(in oklch, var(--accent) 70%, black); /* Experimental: typed attr() reads a percentage from the attribute */ inline-size: attr(data-pct percentage); } /* Fallback: width from a custom property, set in JS or HTML */ @supports not (inline-size: attr(data-pct percentage)) { .bar { inline-size: calc(1% * var(--pct)); } } /* Example markup */
``` --- ## Automatic type scales from document structure URL: https://modern-css.com/automatic-type-scale-without-manual-sizes/ Category: Typography Difficulty: Advanced Baseline: limited Browser support: Unknown MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Nesting Heading hierarchies often ship as hardcoded font-size ladders. This experimental pattern derives sizes from the document outline with :heading(), sibling-index(), and pow() so the scale adjusts itself. ### How it works Early CSS type scales usually ship as a hardcoded ladder: h1 at 2.5rem, h2 at 2rem, h3 at 1.5rem, and so on. That works, but it is easy to drift out of sync when the design changes because every size is a one-off decision. The combination of :heading, sibling-index(), and pow() lets you express the scale as a formula instead: take a base size, raise a ratio to a power, and adjust that power based on the element's position. In its simplest form the heading near the top of the document gets the biggest exponent and headings further down get smaller exponents. Because :heading() and sibling-index() are still experimental and have no stable baseline support, this pattern should be treated as progressive enhancement only. Keep a simple manual ladder or token-based scale as your primary path and layer the formula on top in browsers that implement these features. ### Why use this - Outline-aware sizing: The math uses the document structure instead of a fixed h1–h6 ladder. You get a consistent ratio between levels without editing six font-size rules by hand. - One ratio, many headings: Adjust the base size or ratio once and the whole heading scale updates. No more hunting for every size token in your CSS or design system. - Experimental playground: This pattern is best suited to demos, prototypes, or design tooling today. Use the manual ladder as the production path until :heading() ships widely. ### Modern CSS ```css :root { /* Choose a base size and ratio */ --scale-base: 1rem; --scale-ratio: 1.2; } /* Experimental: requires :heading(), sibling-index(), and pow() */ :heading { font-weight: 600; font-size: calc( var(--scale-base) * pow(var(--scale-ratio), 5 - sibling-index()) ); } /* Fallback for all other browsers */ h1 { font-size: 2.5rem; } h2 { font-size: 2rem; } h3 { font-size: 1.5rem; } h4 { font-size: 1.25rem; } h5 { font-size: 1.1rem; } h6 { font-size: 1rem; } ``` --- ## Motion path animation without JavaScript URL: https://modern-css.com/motion-path-animation-without-javascript/ Category: Animation Difficulty: Intermediate Baseline: widely (2022) Browser support: 95% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/offset-path Animating an element along a curve used to mean GSAP's motionPath plugin or manually calculating x/y coordinates in a JS loop. CSS offset-path lets you define the path, then animate offset-distance from 0% to 100%. ### How it works offset-path defines the route an element travels. You give it an SVG path string, the same format used in a element. The element gets placed at position 0% by default. offset-distance is a percentage that moves the element along that path. Animate it from 0% to 100% and the element travels the full route. Combine with offset-rotate: auto and the element also rotates to stay aligned with the curve direction. You can use circle(), ellipse(), or ray() as the path value instead of a full SVG path string if your route is a simple shape. ### Why use this - No library needed: Drop GSAP and the motionPath plugin. The path lives in CSS, the animation runs on the compositor. - Auto-rotation built in: offset-rotate: auto tilts the element to follow the curve. No angle math required. - Any path shape: Use any SVG path string: straight lines, bezier curves, circles. The element follows it exactly. ### Modern CSS ```css @keyframes along-path { from { offset-distance: 0%; } to { offset-distance: 100%; } } .ball { offset-path: path("M 0 0 C 150 -100 300 100 450 0"); offset-distance: 0%; offset-rotate: auto; animation: along-path 2s linear infinite; } ``` --- ## Better mobile keyboards without JavaScript URL: https://modern-css.com/mobile-keyboard-hints-without-javascript/ Category: HTML Difficulty: Beginner Baseline: widely (2024) Browser support: 96% MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode Mobile users got stuck with the default text keyboard on every input, or developers shipped JS workarounds to detect device type. inputmode and enterkeyhint let HTML request the right keyboard directly. ### How it works inputmode hints which virtual keyboard to show. Values: none (no keyboard), text (default), decimal (number with decimal point), numeric (digits only), tel, search, email, url. It does not affect the input type or validation. enterkeyhint controls the label on the return/enter key of the virtual keyboard. Values: enter, done, go, next, previous, search, send. Browsers may ignore it if the key is not customizable. The key distinction between inputmode and type: type affects browser validation and the submitted value format. inputmode only affects the keyboard — no validation, no format change. Use inputmode="numeric" when you want digits but need to accept leading zeros or dashes that type="number" would reject. ### Why use this - Right keyboard, right type: inputmode controls the virtual keyboard independently from input type. Get a numeric pad while keeping type=text validation — no type=tel hack needed. - Custom return key: enterkeyhint labels the return key: go, done, next, search, send. Users know what happens when they tap it. - Works on contenteditable too: Both attributes work on contenteditable elements, not just inputs. Useful for rich text editors and custom input components. ### Modern CSS ```css ``` --- ## Text highlighting without DOM manipulation URL: https://modern-css.com/text-highlighting-without-dom-manipulation/ Category: Selectors Difficulty: Advanced Baseline: newly (2025) Browser support: 93% MDN: https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API Highlighting arbitrary text used to mean mutating the DOM: wrapping ranges in <mark> elements via innerHTML.replace() or surroundContents(). The CSS Custom Highlight API lets you style ranges directly in CSS with no DOM changes. ### How it works The classic approach was innerHTML.replace(): grab the element's HTML as a string, wrap matches in , and write it back. This nukes event listeners, breaks React and other framework rendering, and silently fails when a match crosses a tag boundary. The CSS Custom Highlight API keeps highlighting entirely separate from the DOM. You create a Range, pass it to new Highlight(range), register it with CSS.highlights.set('name', highlight), and style it with the ::highlight(name) pseudo-element in CSS. No elements are created or destroyed. To clear highlights, call CSS.highlights.delete('name') or CSS.highlights.clear(). Firefox 140 shipped support in June 2025, completing full cross-browser coverage. This is an Interop 2026 focus area. ### Why use this - No DOM mutations: The DOM stays untouched. No wrapped <mark> elements to insert or remove, no lost event listeners, no broken element boundaries. - Crosses element boundaries: Range can span across multiple elements. The old surroundContents() approach throws when a range crosses tag boundaries. - Multiple highlight layers: Register as many named highlights as you need. Each gets its own ::highlight(name) pseudo-element and its own styles. ### Modern CSS ```css /* CSS */ ::highlight(search) { background: yellow; color: black; } /* JS */ const range = new Range(); range.setStart(node, startOffset); range.setEnd(node, endOffset); CSS.highlights.set('search', new Highlight(range)); ``` --- ## Live form output without DOM writes URL: https://modern-css.com/live-form-output-without-javascript-dom-writes/ Category: HTML Difficulty: Beginner Baseline: widely (2018) Browser support: 96% MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/output Showing a live result from a form used to mean wiring up input listeners that wrote to a div on every change. The output element connects directly to form inputs with no JavaScript. ### How it works The element is a form-associated element that displays a result. Connect it to one or more inputs using the for attribute, which takes a space-separated list of input IDs. The initial value is whatever text content you put inside the tag. Note: the browser does not automatically update the output value when the input changes. You still need a small JS listener to write to outputEl.value. What you gain is semantics: the element belongs to the form, announces as a live region to screen readers, and resets with the form. For a truly zero-JS live display, combine output with a CSS custom property updated via an inline oninput: . This keeps JS inline and minimal. ### Why use this - Semantic: output is a form element. It participates in the form, has a for attribute that links it to inputs, and is announced correctly by screen readers. - Part of the form: Unlike a div, output belongs to the form element. You can reference it with form.elements and it resets with the form on reset. - Multiple inputs: The for attribute takes a space-separated list of input IDs. One output can reflect multiple inputs. ### Modern CSS ```css 50 ``` --- ## Lazy load images without JavaScript URL: https://modern-css.com/lazy-load-images-without-javascript/ Category: HTML Difficulty: Beginner Baseline: widely (2023) Browser support: 95% MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/loading Lazy loading images used to mean writing an IntersectionObserver, swapping src attributes, and handling edge cases per browser. One attribute does it all now. ### How it works The loading attribute tells the browser how to prioritize fetching the resource. loading="lazy" defers the fetch until the image or iframe is near the viewport. loading="eager" is the default — fetch immediately regardless of position. Never add loading="lazy" to above-the-fold images. Those need to load immediately for good LCP scores. Apply it only to images that start off-screen. A good rule: the first image on a page should not be lazy. Always include width and height attributes on lazy images. Without them the browser does not know the image dimensions before it loads, causing layout shift when the image finally appears. ### Why use this - One attribute: Add loading="lazy" to any img or iframe. The browser decides when to fetch it based on scroll position and network conditions. - Faster initial load: Off-screen images are not fetched on page load. The browser only requests them as the user scrolls near them, saving bandwidth and speeding up LCP. - Works on iframes too: loading="lazy" works on iframe elements as well. Useful for maps, embedded videos, and third-party widgets that would otherwise block rendering. ### Modern CSS ```css Hero Photo ``` --- ## Path shapes without SVG clip paths URL: https://modern-css.com/path-shapes-without-svg-clip-paths/ Category: Layout Difficulty: Advanced Baseline: limited Browser support: 85% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/basic-shape/shape Creating non-rectangular clip paths used to mean SVG <clipPath> elements or path() with fixed pixel coordinates that break as soon as the element resizes. shape() brings path-style drawing to CSS using percentage units and responsive coordinates. ### How it works The clip-path: path() function takes SVG path data, which uses absolute pixel coordinates. A path drawn for an 800px-wide element looks wrong at any other width. The workaround was SVG elements with clipPathUnits="objectBoundingBox" and 0–1 fractional coordinates — a complex setup hidden in HTML. shape() brings path drawing commands directly into CSS with support for responsive units. from 0% 0% sets the starting point, line to 100% 0% draws a line, curve to draws a cubic Bézier, and close closes the path. Every coordinate can use percentages, vw, rem, or calc(). Support is Chrome 135+, Edge 135+, and Safari 18.4+. Firefox support is arriving in early 2026. This is an Interop 2026 focus area, so cross-browser coverage will improve throughout the year. ### Why use this - Responsive units: shape() accepts percentages, calc(), and any CSS length. The shape scales with the element automatically. - No SVG required: No inline SVG, no <clipPath> element, no hidden SVG containers. The shape lives in CSS where it belongs. - Animatable: Unlike SVG clip paths, shape() supports CSS transitions and animations between compatible shapes. ### Modern CSS ```css .hero { clip-path: shape( from 0% 0%, line to 100% 0%, line to 100% 80%, curve to 0% 80% via 50% 105%, close ); } ``` --- ## Scaling elements without transform hacks URL: https://modern-css.com/scaling-elements-without-transform-hacks/ Category: Layout Difficulty: Beginner Baseline: newly (2024) Browser support: 97% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/zoom transform: scale() shrinks an element visually but keeps its original layout space, requiring negative margins to collapse the gap. zoom scales the element and its layout footprint together — no hacks needed. ### How it works transform: scale() is a visual-only transform. The element renders smaller but its layout box stays the original size, leaving a gap where the element used to be. The workaround was setting transform-origin: top left and applying equal negative margins — a fragile hack that breaks with different origins or zoom levels. zoom scales the element and its layout footprint together. Set zoom: 0.5 and the element takes up half the space in the flow. No negative margins, no transform-origin tweaking. The key difference: transform applies after layout, zoom applies during layout. Use zoom when you want surrounding content to reflow around the scaled element. Use transform: scale() for animations or when you intentionally want to preserve the layout space. ### Why use this - Layout-aware scaling: zoom scales the element and the space it occupies. Surrounding elements reflow naturally, no negative margins needed. - Simpler code: One property does the job. No transform-origin, no margin overrides, no layout compensation. - Now cross-browser: zoom was Chrome-only for years. Firefox 126 added it in 2024, making it safe to use everywhere. ### Modern CSS ```css .thumbnail { zoom: 0.5; } ``` --- ## Accordion disclosure without JavaScript URL: https://modern-css.com/accordion-without-javascript/ Category: HTML Difficulty: Beginner Baseline: widely (2020) Browser support: 96% MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details Accordions used to need JavaScript to toggle visibility, update aria-expanded, and manage keyboard events. The details and summary elements handle all of that natively. ### How it works The
element is a disclosure widget. Wrap content in it and add a as the first child — the summary becomes the visible toggle. Everything else inside details is hidden until the user opens it. The browser adds the open attribute when expanded. You can hook into it with CSS: details[open] summary { ... }. The disclosure triangle comes from ::marker on the summary — replace it with list-style: none and your own indicator. For exclusive accordions where only one item is open at a time, give all details elements the same name attribute. The browser enforces mutual exclusivity automatically — no JavaScript needed for that either. ### Why use this - No JavaScript: Click to expand, click to collapse. Keyboard accessible. The browser manages open state with zero JS. - Accessible by default: The summary element is a button in the accessibility tree. No manual aria-expanded wiring needed. - Styleable with CSS: Target the open attribute to style the expanded state. Use ::details-content for the panel and ::marker or list-style to customize the triangle. ### Modern CSS ```css
What is CSS?

CSS styles the web.

``` --- ## Readable text without manual contrast checks URL: https://modern-css.com/readable-text-without-manual-contrast-checks/ Category: Colors Difficulty: Beginner Baseline: limited Browser support: 6% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/contrast-color Picking readable text used to mean hardcoding color: white or color: black for each background. contrast-color() does the math automatically, returning whichever meets WCAG AA contrast against the given color. ### How it works contrast-color(bg) takes a background color and returns either black or white — whichever meets WCAG AA contrast requirements against that background. No JavaScript, no preprocessor, no lookup table. The old approach meant manually choosing color: white or color: black for each background, and repeating that decision every time a new color was added. With user-generated colors or dynamic themes, this quickly becomes unmanageable in plain CSS. Combine it with custom properties for maximum flexibility: color: contrast-color(var(--bg)) means you only define --bg and the browser handles the rest. Support is still early-stage but it is an Interop 2026 focus, so all major browsers are actively implementing it. ### Why use this - Zero guesswork: The browser calculates contrast automatically. No WCAG math, no hardcoded overrides for each background. - Themeable by default: Change --bg to any color and the text color adjusts. Works with dynamic colors and user preferences. - Accessible by design: contrast-color() targets WCAG AA contrast. Readable text is the default, not an afterthought. ### Modern CSS ```css .badge { background: var(--bg); color: contrast-color(var(--bg)); } ``` --- ## Native autocomplete without JavaScript URL: https://modern-css.com/native-autocomplete-without-javascript/ Category: HTML Difficulty: Beginner Baseline: limited Browser support: 96% MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/datalist Building autocomplete used to mean pulling in a library like Typeahead or Awesomplete and wiring up event listeners. The datalist element gives browsers native autocomplete with zero JavaScript. ### How it works The element holds a list of , not the value attribute. The browser renders the dropdown in its native UI. That means the position, size, and styling of the suggestion list is entirely browser-controlled and varies by OS. The input itself can be styled normally, but the dropdown cannot. Unlike ``` --- ## Reduced motion without JavaScript detection URL: https://modern-css.com/reduced-motion-without-javascript-detection/ Category: Animation Difficulty: Beginner Baseline: widely (2020) Browser support: 96% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion Respecting a user's reduced motion preference required JavaScript to check window.matchMedia and conditionally skip animations. The prefers-reduced-motion media query lets CSS respond directly to the OS accessibility setting. ### How it works Animations and transitions can cause problems for users with vestibular disorders or motion sensitivity. The traditional approach was to check window.matchMedia('(prefers-reduced-motion: reduce)') in JavaScript and remove animation classes. Because JavaScript runs after the page renders, animated elements could briefly play before being stopped. @media (prefers-reduced-motion: reduce) applies before paint. Styles inside are active immediately when the OS setting is enabled. Setting animation-duration to 0.01ms effectively disables animations while preserving any JavaScript that listens for animationend events — using 0 skips the event entirely. ### Why use this - Accessibility without JavaScript: Users who experience motion sickness or vestibular disorders can set reduced motion in their OS. This media query lets CSS respond directly — no JavaScript needed. - Instant, no flash of animation: JavaScript runs after page load, so animations can briefly play before being disabled. CSS applies before paint, so reduced-motion users never see the animation at all. - Reduce, not necessarily remove: Use 0.01ms instead of 0 to preserve animation events that JavaScript might listen for. Or slow animations down with a longer duration rather than disabling them entirely. ### Modern CSS ```css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } ``` --- ## Perceptually uniform colors with oklch URL: https://modern-css.com/perceptually-uniform-colors-with-oklch/ Category: Color Difficulty: Intermediate Baseline: widely (2023) Browser support: 90% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch HSL looks like it should be perceptually uniform, but it isn't. Yellow at hsl(60 100% 50%) appears far brighter than blue at hsl(240 100% 50%) at the same lightness. oklch uses a model where L actually means the same perceived brightness across all hues. ### How it works HSL was supposed to be human-friendly but its lightness channel is not perceptually uniform. Yellow at hsl(60 100% 50%) looks far brighter than blue at hsl(240 100% 50%) even though both have L: 50%. Building a consistent color palette means manually adjusting each shade by eye until it looks balanced. oklch(L C H) uses a perceptually uniform lightness model. L: 0.55 looks the same perceived brightness whether the hue is green, orange, or purple. To create a lighter shade, increase L. To shift hue, change H. Chroma C controls saturation. All three values are genuinely independent in a way HSL's S and L are not. ### Why use this - Predictable lightness: L: 0.5 looks the same perceived brightness in oklch regardless of the hue. That's not true in HSL. - Easy palette generation: Change only L for lighter or darker shades. Change only H to shift hue. Change only C for saturation. They're independent. - Wide gamut ready: oklch can express P3 colors that hex and sRGB can't. On wide-gamut displays the chroma can go higher than sRGB allows. ### Modern CSS ```css --brand: oklch(0.55 0.2 264); --brand-light: oklch(0.75 0.2 264); --brand-dark: oklch(0.35 0.2 264); ``` --- ## CSS feature detection without JavaScript URL: https://modern-css.com/css-feature-detection-without-javascript/ Category: Workflow Difficulty: Beginner Baseline: widely (2020) Browser support: 96% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/@supports Detecting CSS feature support required JavaScript — either Modernizr, inline CSS.supports() checks, or testing computed styles. @supports lets CSS apply styles conditionally based on whether the browser supports a given property and value. ### How it works Progressive enhancement with JavaScript meant detecting support, adding a class to the root element, then writing separate CSS for that class. This split the feature logic across two files and required JS to run before styles applied correctly — a fragile dependency. @supports is a CSS conditional block. Styles inside only apply when the browser supports the tested declaration. @supports not provides a fallback path. @supports selector(:has()) tests selector support. Because the detection is in CSS, no JavaScript is needed and styles are self-contained. ### Why use this - Detection and fallback in one place: @supports keeps feature detection in CSS where the styles live. No JS, no class toggling, no DOM mutation to apply a CSS branch. - Supports not, and, or: @supports not (), @supports (a) and (b), and @supports (a) or (b) cover all combinatorial cases for progressive enhancement. - Selector detection too: @supports selector(:has()) tests whether a CSS selector is supported — useful for detecting newer pseudo-classes before using them. ### Modern CSS ```css @supports (display: grid) { .layout { display: grid; } } @supports not (display: grid) { .layout { float: left; width: 50%; } } ``` --- ## Frosted glass effect without opacity hacks URL: https://modern-css.com/frosted-glass-effect-without-opacity-hacks/ Category: Color Difficulty: Intermediate Baseline: widely (2022) Browser support: 96% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter Frosted glass used to require a blurred copy of the background behind the element: a pseudo-element with the same background image, a filter: blur, and z-index stacking. backdrop-filter applies effects directly to whatever is rendered behind the element. ### How it works Frosted glass required duplicating the background behind the card: a ::before pseudo-element positioned absolutely with the same background image, a filter: blur() applied, and careful z-index stacking. It only worked with a known static background and broke with scrolling or dynamic content behind it. backdrop-filter applies filter effects to the area behind the element in the stacking context. It blurs, desaturates, or brightens whatever is rendered behind it — including other elements, not just the page background. Pair it with a semi-transparent background to let the blurred content show through. ### Why use this - Real blur: The blur applies to whatever is rendered behind the element, including other elements. Not just a static background image copy. - Stacks filters: Combine blur, brightness, contrast, saturate, and grayscale in a single declaration. - No extra elements: No ::before pseudo-element with a positioned blurred background duplicate. One property on the element. ### Modern CSS ```css .glass { backdrop-filter: blur(12px) saturate(1.5); background: rgba(255, 255, 255, 0.1); } ``` --- ## Exclusive accordions without JavaScript URL: https://modern-css.com/exclusive-accordions-without-javascript/ Category: HTML Difficulty: Beginner Baseline: widely (2025) Browser support: 85% MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details#name Making only one accordion panel open at a time required JavaScript to listen for toggle events and close all other open details elements. The name attribute on details elements creates a mutually exclusive group — the browser handles closing others automatically. ### How it works The
element is a native disclosure widget — clicking the toggles it open and closed. But by default, multiple details elements are independent. Making only one open at a time required attaching toggle event listeners that would close all siblings whenever any one opened. Adding a name attribute groups details elements together. When one opens, the browser automatically closes any other open details with the same name. The behavior is identical to how radio inputs with the same name form an exclusive selection group. It works with keyboard navigation and screen readers without any extra ARIA. ### Why use this - Zero JavaScript: The browser closes other open details in the same name group automatically. No toggle listeners, no forEach loops, no state management. - Works like radio buttons for content: details elements with the same name value form an exclusive group — exactly like radio inputs with the same name. Opening one closes the others. - Accessible by default: details and summary are semantic HTML. They are keyboard navigable and understood by screen readers without extra ARIA attributes. ### Modern CSS ```css
Question 1 Answer 1
Question 2 Answer 2
``` --- ## Custom easing curves without cubic-bezier guessing URL: https://modern-css.com/custom-easing-without-cubic-bezier-guessing/ Category: Animation Difficulty: Intermediate Baseline: newly (2024) Browser support: 87% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/linear Creating non-standard easing like bounce or spring required either a cubic-bezier approximation (which can only produce curves, not bounces) or a JavaScript animation library. linear() defines an easing function by interpolating between a list of keyframe values. ### How it works cubic-bezier() defines an easing curve with two control points. Because it is a cubic function, it cannot cross itself — it can only produce smooth S-curves. Achieving a bounce required either a JavaScript animation library or manually chaining multiple animations, which was complex and hard to maintain. linear() defines an easing function by linearly interpolating between a list of output values. Values above 1 or below 0 overshoot, which creates bounce and spring effects. Optional percentage hints like 1.2 60% place a keyframe at a specific point in the duration. A useful tool for generating linear() curves is linear-easing-generator.netlify.app. ### Why use this - Bounce and spring without a library: cubic-bezier() can only produce S-curves. linear() can overshoot and come back, producing bounces, springs, and elastic effects in pure CSS. - Works with transition and animation: linear() is a standard easing value. Use it anywhere you use ease, ease-in-out, or cubic-bezier — in transitions, animations, and animation-timing-function. - Optional position hints: Each stop can include an optional percentage hint. linear(0, 1.2 60%, 1) puts the overshoot at 60% of the duration rather than evenly spaced. ### Modern CSS ```css .el { transition: transform 0.6s linear(0, 1.2 60%, 0.9, 1.05, 1); } ``` --- ## Preventing layout shift from scrollbar appearance URL: https://modern-css.com/preventing-layout-shift-from-scrollbar/ Category: Layout Difficulty: Beginner Baseline: newly (2024) Browser support: 90% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-gutter When content grows tall enough to scroll, the scrollbar appears and narrows the layout, causing a visible jump. The old fixes were overflow-y: scroll (always shows the bar) or padding-right matching the scrollbar width. scrollbar-gutter: stable reserves the space upfront. ### How it works When a page gains enough content to scroll, the classic scrollbar appears and shrinks the content area — a visible jump. Two common fixes existed: overflow-y: scroll keeps the scrollbar always visible even on short pages, or padding-right: 17px hardcodes the scrollbar width, which varies across OSes and browsers. scrollbar-gutter: stable reserves the scrollbar track space before the scrollbar appears. The layout width stays the same whether the page is scrollable or not. Important: on systems with overlay scrollbars — the default on macOS and iOS — this property has no visible effect since overlay scrollbars float on top of content rather than occupying layout space. To test it on macOS, go to System Settings → Appearance → Show scroll bars → Always. ### Why use this - No layout shift: Space is reserved before the scrollbar appears. The page doesn't jump when content grows past the viewport. - No hardcoded widths: The browser reserves the correct amount automatically. No 17px magic numbers that break across platforms. - both keyword: scrollbar-gutter: stable both reserves space on both sides for symmetric layouts like centered content. ### Modern CSS ```css body { scrollbar-gutter: stable; } ``` --- ## Media query ranges without min-width and max-width URL: https://modern-css.com/media-query-ranges-without-min-max-syntax/ Category: Layout Difficulty: Beginner Baseline: widely (2023) Browser support: 94% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries#syntax_improvements_in_level_4 Defining a range of viewport widths required two separate media features: min-width and max-width. Media Queries Level 4 introduces range syntax using standard comparison operators, making ranges possible in a single expression. ### How it works The old min-width and max-width media features work but read backwards — min-width: 600px means the viewport is at least 600px wide. Defining a range required two features connected with and, which gets verbose for common breakpoint patterns. Media Queries Level 4 range syntax uses standard comparison operators. (600px is a compound range in a single expression and reads in the same direction as the intent. Both (exclusive) and (inclusive) are available for precise boundary control. ### Why use this - Compound ranges in one expression: A between-range like 600px to 1200px takes one expression instead of two media features joined with and. - Familiar comparison operators: Uses <, <=, >, >= instead of min-/max- prefixes. The direction reads naturally: 600px <= width means width is at least 600px. - Works for all range features: Range syntax works for width, height, aspect-ratio, resolution, and any other range-type media feature. ### Modern CSS ```css @media (600px <= width <= 1200px) { .card { grid-template-columns: 1fr 1fr; } } ``` --- ## Preventing scroll chaining without JavaScript URL: https://modern-css.com/preventing-scroll-chaining-without-javascript/ Category: Layout Difficulty: Beginner Baseline: widely (2022) Browser support: 96% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior When a scrollable element inside a modal reached its end, scroll would chain to the page behind it. The fix was a JS wheel event listener calling e.preventDefault(). overscroll-behavior: contain stops that natively. ### How it works Scrollable panels inside modals or dropdowns would chain scroll to the page once the panel reached its end. Stopping this required non-passive wheel and touchmove event listeners calling e.preventDefault(), which also blocked the browser's scroll optimizations and required separate handling for touch. overscroll-behavior: contain stops scroll propagation at the element's boundary. The inner element scrolls normally, but the chain ends there. overscroll-behavior: none additionally prevents the bounce effect and pull-to-refresh on supported platforms. ### Why use this - No JavaScript: No wheel or touchmove event listener, no preventDefault, no passive flag concerns. - Works on touch too: Handles touch scroll chaining on mobile without the complexity of non-passive event listeners. - Pull-to-refresh too: overscroll-behavior: none also prevents pull-to-refresh on Chrome Android for full-screen app-like layouts. ### Modern CSS ```css .modal-content { overflow-y: auto; overscroll-behavior: contain; } ``` --- ## Responsive images without the background-image hack URL: https://modern-css.com/responsive-images-without-background-image-hack/ Category: Layout Difficulty: Beginner Baseline: widely (2022) Browser support: 96% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit Cropped responsive images were done with background-image and background-size: cover on a div. No semantic img element, no alt text, and no native lazy loading. object-fit brings the same cropping behavior to real img elements. ### How it works Cropped images in card layouts used background-image on a div with background-size: cover. Visually it worked, but it meant no element, no alt attribute, no native lazy loading, and no srcset for responsive images. object-fit: cover applies directly to an element. The image fills its container and is cropped to fit, just like background-size: cover. object-position controls which part stays visible, matching background-position. The image stays semantic, accessible, and gets native browser optimization. ### Why use this - Semantic HTML: A real img element with alt text. Screen readers and search engines see the image correctly. - Native lazy loading: img supports loading="lazy" natively. Background images require Intersection Observer to achieve the same. - object-position: Control which part of the image is visible with object-position. Same concept as background-position. ### Modern CSS ```css img { object-fit: cover; object-position: center; width: 100%; height: 200px; } ``` --- ## Scrollbar styling without -webkit- pseudo-elements URL: https://modern-css.com/scrollbar-styling-without-webkit-pseudo-elements/ Category: Layout Difficulty: Beginner Baseline: newly (2025) Browser support: 75% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color Styling scrollbars required ::-webkit-scrollbar, ::-webkit-scrollbar-track, and ::-webkit-scrollbar-thumb — non-standard WebKit-only pseudo-elements that Firefox never supported. scrollbar-color and scrollbar-width are the standard two-property replacement. ### How it works The ::-webkit-scrollbar family of pseudo-elements let Chrome and Safari style scrollbars, but they were never part of the CSS standard and Firefox never implemented them. Styling scrollbars cross-browser required either a polyfill or accepting that Firefox users would see the default system scrollbar. scrollbar-color takes two color values: the thumb and the track. scrollbar-width accepts auto, thin, or none. Both are inherited, so applying them to the root element styles all scrollbars on the page. Safari added support in version 26.2. ### Why use this - Works in Firefox: ::-webkit-scrollbar never worked in Firefox. scrollbar-color and scrollbar-width are supported in Chrome, Firefox, Safari, and Edge. - Two properties instead of six: scrollbar-color takes two values: thumb color and track color. scrollbar-width is thin, auto, or none. No pseudo-elements needed. - Use CSS custom properties: Both properties accept any CSS color value, including custom properties. Scrollbar colors can follow your theme variables. ### Modern CSS ```css * { scrollbar-width: thin; scrollbar-color: #888 transparent; } ``` --- ## Mobile viewport height without the 100vh hack URL: https://modern-css.com/mobile-viewport-height-without-100vh-hack/ Category: Layout Difficulty: Beginner Baseline: widely (2023) Browser support: 93% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/length#viewport-relative_lengths 100vh on mobile includes the browser address bar and navigation controls even when they are visible, causing full-height elements to overflow. Dynamic viewport units (dvh, svh, lvh) adapt to the actual visible area. ### How it works On mobile browsers, 100vh is calculated including the address bar and navigation controls even when they overlay the page. A height: 100vh element ends up taller than the visible screen, pushing content behind the browser chrome. Dynamic viewport units solve this. dvh updates as the browser chrome appears and disappears. svh is the small viewport height — the worst case with chrome fully visible. lvh is the large viewport height — chrome fully hidden. For full-screen sections, replace 100vh with 100dvh. ### Why use this - No mobile overflow: 100dvh accounts for the browser address bar and navigation. It shrinks when the bar is visible and expands when it scrolls away. - Three units for three needs: dvh is dynamic (changes with chrome), svh is small (worst case, chrome fully visible), lvh is large (best case, chrome fully hidden). - Drop-in replacement on desktop: On desktop where viewport height is stable, dvh behaves identically to vh. Switch freely without breaking existing layouts. ### Modern CSS ```css .hero { height: 100dvh; } ``` --- ## Form validation styles without JavaScript URL: https://modern-css.com/form-validation-styles-without-javascript/ Category: Selector Difficulty: Beginner Baseline: newly (2023) Browser support: 85% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/:user-invalid :invalid fires as soon as the page loads, marking empty required fields as errors before anyone types. The workaround was JS adding a .touched class after blur. :user-invalid only activates after the user has interacted with the field. ### How it works :invalid applies the moment the page loads. A required empty field is immediately styled as an error before the user touches it. The fix was JavaScript: listen for blur on each input, add a .touched class, then use .touched:invalid in CSS to defer the error styling until after first interaction. :user-invalid and :user-valid are built-in pseudo-classes that match after the user has interacted with a field. :user-invalid activates when the field is left in an invalid state. :user-valid activates on a valid state. The browser tracks the interaction threshold — no JavaScript, no class management. ### Why use this - No blur listener: No JavaScript event listener, no .touched class, no class toggling on every field. - Interaction-aware: :user-invalid only triggers after the user has interacted with the field. Empty required fields stay neutral on page load. - Pairs with :user-valid: :user-valid shows success state the same way. Both follow the same interaction threshold as :user-invalid. ### Modern CSS ```css input:user-invalid { border-color: red; } input:user-valid { border-color: green; } ``` --- ## Auto-growing textarea without JavaScript URL: https://modern-css.com/auto-growing-textarea-without-javascript/ Category: Layout Difficulty: Beginner Baseline: limited (2024) Browser support: 73% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/field-sizing Making a textarea grow as the user types required a JS event listener that reset height to auto then set it to scrollHeight on every keystroke. field-sizing: content makes the field size itself to its content. ### How it works Auto-growing textareas required JavaScript on every keystroke: first set height to auto so the element could shrink, then immediately set height to scrollHeight to match the content. One listener per textarea, causing a forced reflow on every keypress. field-sizing: content tells the browser to size the field to fit its content instead of a fixed box. Use min-height to set the default empty state and max-height to cap how tall it can grow. No JavaScript, no event listeners, no scrollHeight. ### Why use this - Zero JavaScript: No oninput listener, no scrollHeight, no height reset trick. The browser handles the resize. - Works on inputs too: field-sizing: content works on single-line text inputs as well. The input grows with the typed value. - Min and max still work: Set min-height for the default empty size and max-height to cap growth. CSS handles the range. ### Modern CSS ```css textarea { field-sizing: content; min-height: 3lh; } ``` --- ## Smooth height auto animations without JavaScript URL: https://modern-css.com/smooth-height-auto-animations-without-javascript/ Category: Animation Difficulty: Beginner Baseline: newly (2024) Browser support: 69% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/interpolate-size Animating to height: auto required JS to measure scrollHeight, set a fixed pixel height, then snap back to auto. interpolate-size: allow-keywords lets CSS transition directly to and from intrinsic sizes. ### How it works height: auto isn't animatable because the browser can't interpolate between a fixed length and a keyword. The workaround was JavaScript: read scrollHeight to get the actual pixel height, set it as a fixed value, trigger the transition, then snap back to auto on transitionend. interpolate-size: allow-keywords opts the browser into animating keyword sizes. Set it on :root once and transitions to height: auto, width: fit-content, and other intrinsic sizes work everywhere on the page. No JavaScript, no scrollHeight, no transitionend. ### Why use this - No JS needed: No scrollHeight measurement, no transitionend listener, no pixel-to-auto snap. - Works with any keyword: Transitions to and from auto, min-content, max-content, and fit-content all work with one declaration. - Set it once: Declaring interpolate-size on :root unlocks keyword size transitions everywhere on the page. ### Modern CSS ```css :root { interpolate-size: allow-keywords; } .accordion { height: 0; overflow: hidden; transition: height .3s ease; } .accordion.open { height: auto; } ``` --- ## Range style queries without multiple blocks URL: https://modern-css.com/range-style-queries-without-multiple-blocks/ Category: Workflow Difficulty: Advanced Baseline: limited Browser support: 88% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/@container Testing custom property ranges used to require multiple @container style() blocks or JavaScript comparisons. Range style queries let you write numeric comparisons directly: style(--progress > 50%). ### How it works Style queries (@container style()) landed with equality checks: you can test style(--theme: dark). But for numeric values like progress, temperature, or scores, you had to write separate blocks for each discrete value — or fall back to JavaScript for range-based class toggling. Range style queries add comparison operators to style queries. You write @container style(--progress > 75%) to match any value above 75%. Combine ranges with and: style(--progress > 25%) and style(--progress <= 75%). This enables pure CSS progress bars, data visualizations, and state-dependent styling without any JavaScript. ### Why use this - Numeric ranges: Compare custom property values with >, <, >=, <=. No need to enumerate every possible value. - Progress-based styling: Perfect for progress bars, sliders, and meters. Style changes at thresholds without JavaScript. - Combinable: Use 'and' to create ranges: style(--x > 25%) and style(--x <= 75%). Clean, readable thresholds. ### Modern CSS ```css .progress-container { container-type: style; } @container style(--progress > 75%) { .bar { background: var(--green); } } @container style(--progress > 25%) and style(--progress <= 75%) { .bar { background: var(--yellow); } } ``` --- ## Sticky & snapped element styling without JavaScript URL: https://modern-css.com/sticky-snapped-styling-without-javascript/ Category: Animation Difficulty: Intermediate Baseline: limited Browser support: 50% Styling elements differently when they become stuck (sticky) or snapped used to require JavaScript scroll event listeners. scroll-state() container queries let CSS respond to scroll-related states directly. ### How it works Sticky headers that add a shadow when stuck, or carousels that highlight the active slide — these patterns always required JavaScript. You'd listen to scroll events, calculate positions, and toggle classes. This causes layout thrashing, jank, and race conditions. scroll-state() container queries let CSS detect scroll-related states natively. Set container-type: scroll-state on the scrolling ancestor, then query it: @container scroll-state(stuck: top) matches when a child is stuck to the top. @container scroll-state(snapped: x) matches the currently snapped element. The browser handles all the tracking with zero JavaScript. ### Why use this - No scroll listeners: The browser tracks stuck and snapped states internally. No scroll event handlers, no requestAnimationFrame, no jank. - Multiple states: Query stuck (top, bottom, left, right), snapped (x, y, inline, block), and overflowing states — all with pure CSS. - Container query model: Uses the familiar @container syntax. If you know container queries, you already know how to use scroll-state queries. ### Modern CSS ```css .header-wrap { container-type: scroll-state; } @container scroll-state(stuck: top) { .header { box-shadow: 0 2px 8px rgba(0,0,0,.1); backdrop-filter: blur(12px); } } ``` --- ## Typed attribute values without JavaScript URL: https://modern-css.com/typed-attribute-values-without-javascript/ Category: Workflow Difficulty: Intermediate Baseline: limited Browser support: 42% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/attr Reading data attributes for styling used to require JavaScript to parse dataset values and set inline styles or custom properties. Advanced attr() lets CSS read HTML attributes directly as typed values — numbers, colors, lengths, and more. ### How it works The attr() function has existed in CSS for years, but it could only return strings and was limited to the content property. To use data attributes for layout or color, you had to write JavaScript that reads el.dataset, converts the value, and sets it as an inline style or custom property. Advanced attr() adds type coercion. You write attr(data-pct type()) and the browser reads the HTML attribute, parses it as a percentage, and uses it as a typed CSS value. This works with , , , , and more. You can even provide fallbacks for missing or invalid attributes. ### Why use this - No JavaScript bridge: Read data attributes directly in CSS without JavaScript parsing, type conversion, or custom property bridges. - Type coercion: Specify the expected type: , , , . The browser handles parsing and validation. - Fallback values: Provide a fallback if the attribute is missing or invalid: attr(data-x type(), 1rem). ### Modern CSS ```css .bar { width: attr(data-pct type()); } /* HTML:
*/ ``` --- ## Inline conditional styles without JavaScript URL: https://modern-css.com/inline-conditional-styles-without-javascript/ Category: Workflow Difficulty: Intermediate Baseline: limited Browser support: 35% Conditional styling used to require JavaScript to toggle classes based on state. CSS if() lets you write inline conditions that respond to custom property values, media features, and container queries. ### How it works Component variants have always required separate CSS rules: .btn.primary, .btn.danger, .btn.outlined, each repeating the same properties with different values. JavaScript often manages which classes are applied, adding another layer of complexity. CSS if() lets you write conditional logic inline. A single .btn rule can test style(--variant: primary) and choose values accordingly. It can also test media features like prefers-color-scheme: dark directly inside a property value, eliminating the need for separate @media blocks for simple value swaps. ### Why use this - Inline conditions: Write if/else logic directly in property values. No separate rule blocks for each variant. - Style queries: Test custom property values with style(). Respond to --variant, --size, --state without JavaScript class management. - Media-aware: Can also test media features like prefers-color-scheme or prefers-reduced-motion right inside a property value. ### Modern CSS ```css .btn { background: if( style(--variant: primary): blue; else: gray ); } ``` --- ## Reusable CSS logic without Sass mixins URL: https://modern-css.com/reusable-css-logic-without-sass-mixins/ Category: Workflow Difficulty: Intermediate Baseline: limited Browser support: 67% Reusable calculations and logic in stylesheets used to require Sass or other preprocessors. Native CSS @function lets you define custom functions that return computed values, right in your stylesheet. ### How it works Preprocessors like Sass introduced functions decades ago, allowing developers to encapsulate reusable calculations. But Sass functions compile to static values at build time — they can't react to runtime conditions like viewport size changes or user preferences. CSS @function brings this capability natively to the browser. You define a function with @function --name(--param) { @return ... } and call it anywhere a value is expected: font-size: --fluid(1rem, 2rem). Because it runs at runtime, it can use viewport units, custom properties, and other dynamic values that Sass simply can't access. ### Why use this - No build step: Native CSS functions run in the browser. No Sass compiler, no PostCSS plugin, no build pipeline required. - Runtime computed: Unlike Sass which compiles to static values, CSS @function can use runtime values like viewport units, custom properties, and env(). - Composable: Functions can call other functions and use custom properties. Build complex design token systems with pure CSS. ### Modern CSS ```css @function --fluid(--min, --max) { @return clamp( var(--min), 50vi, var(--max) ); } h1 { font-size: --fluid(1.5rem, 3rem); } ``` --- ## Corner shapes beyond rounded borders URL: https://modern-css.com/corner-shapes-beyond-rounded-borders/ Category: Layout Difficulty: Beginner Baseline: limited Browser support: 67% Non-circular corner shapes like squircles, scoops, and notches used to require clip-path polygon hacks or SVG masks. Now corner-shape provides named corner styles natively. ### How it works Apple popularized the superellipse (squircle) for app icons, and developers have been trying to replicate it in CSS ever since. The border-radius property only creates circular arcs, so achieving smoother, more continuous curves required clip-path with dozens of polygon points or external SVG masks. The corner-shape property changes how border-radius draws its curves. Values include squircle (superellipse), scoop (concave curve), notch (straight cut), and bevel (angled cut). It works with existing border-radius values — just add one line to transform any rounded corner into a different shape. ### Why use this - One property: Replace complex polygon hacks or SVG masks with a single corner-shape declaration. Squircle, scoop, notch, bevel — all built in. - Works with border-radius: corner-shape modifies how border-radius curves are drawn. The radius controls the size, the shape controls the curvature. - iOS-style squircles natively: The superellipse (squircle) shape used by Apple's design system is now a CSS one-liner. No more approximations. ### Modern CSS ```css .card { border-radius: 2em; corner-shape: squircle; } ``` --- ## Responsive clip paths without SVG URL: https://modern-css.com/responsive-clip-paths-without-svg/ Category: Animation Difficulty: Advanced Baseline: limited Browser support: 96% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/basic-shape/shape Complex clip paths used to require SVG path() definitions with fixed coordinates that don't scale responsively. The CSS shape() function uses responsive units like percentages and viewport units for truly flexible shapes. ### How it works The SVG path() function brought complex shapes to CSS clip-path, but it inherited SVG's fixed coordinate system. A path defined with M0 200 L100 0 L200 200 only works for a 200×200 element. Resize it, and the clipping breaks. The CSS shape() function solves this by using standard CSS coordinates and units. You write from 0% 100%, line to 50% 0%, line to 100% 100% — readable, responsive, and animatable. It supports lines, curves (quadratic and cubic), arcs, and smooth joins, all with CSS units that scale with the element. ### Why use this - Responsive by default: Use percentages, em, rem, vw — any CSS unit. The shape scales with the element, unlike fixed-coordinate SVG paths. - CSS-native syntax: No SVG path mini-language to learn. Uses familiar CSS commands: line, curve, arc, smooth, with readable coordinate pairs. - Animatable: Unlike path(), shape() can be animated and transitioned between different shapes smoothly with CSS animations. ### Modern CSS ```css .shape { clip-path: shape( from 0% 100%, line to 50% 0%, line to 100% 100% ); } ``` --- ## Scroll spy without IntersectionObserver URL: https://modern-css.com/scroll-spy-without-intersection-observer/ Category: Selector Difficulty: Intermediate Baseline: limited Browser support: 48% Highlighting navigation links based on scroll position used to require JavaScript IntersectionObserver or scroll event listeners. Now CSS can track which section is in view with scroll-target-group and the :target-current pseudo-class. ### How it works Scroll spy navigation — highlighting the current section's link as the user scrolls — has always required JavaScript. The typical approach uses IntersectionObserver to watch each section, then toggles an 'active' class on the corresponding nav link. This works but requires careful threshold tuning, cleanup on unmount, and can fall out of sync with fast scrolling. The CSS approach uses :target-current, a pseudo-class that matches anchor links pointing to the element currently in the scroll port. Combined with smooth scrolling and scroll-snap, you get a complete scroll-spy navigation with no JavaScript at all. The browser handles the tracking natively, so it's always perfectly in sync. ### Why use this - Zero JavaScript: No IntersectionObserver, no scroll event listeners, no class toggling. The browser tracks which section is in view natively. - Always in sync: The :target-current pseudo-class updates in real-time as the user scrolls. No timing bugs or threshold tuning. - Works with CSS scroll-snap: Pairs naturally with scroll-snap for section-based layouts. The active indicator follows snap points automatically. ### Modern CSS ```css .scroller { overflow-y: auto; } nav a:target-current { color: var(--accent); } ``` --- ## Filling available space without calc workarounds URL: https://modern-css.com/filling-available-space-without-calc-workarounds/ Category: Layout Difficulty: Beginner Baseline: limited Browser support: 90% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/stretch Making an element fill its container while keeping margins meant calc(100% - left - right) or risking overflow with width: 100%. The stretch keyword fills available space while respecting margins automatically. ### How it works When you set width: 100% on an element with margins, the element overflows its container because 100% refers to the content box of the parent, and margins are added on top. The workaround was width: calc(100% - 40px) where you hard-code the exact margin values. Change the margins and you must update the calc too. The stretch keyword resolves to the available space in the containing block, applying the result to the element's margin box instead of the box determined by box-sizing. This means the element fills its container exactly, with margins intact, without any manual math. It works on width, height, min-width, max-height, and all sizing properties. ### Why use this - Margin-aware: stretch applies to the margin box. Margins are respected without manual subtraction. - No overflow: Unlike width: 100%, stretch can never overflow the container when margins or padding are present. - Works on height too: Use height: stretch to fill the block axis. Great for full-height layouts without 100vh quirks. ### Modern CSS ```css .full { width: stretch; } ``` --- ## Staggered animations without nth-child hacks URL: https://modern-css.com/staggered-animations-without-nth-child-hacks/ Category: Animation Difficulty: Intermediate Baseline: limited Browser support: 70% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/sibling-index Staggered list animations used to require manually setting --index on each :nth-child or counting in JavaScript. The sibling-index() function gives every element automatic awareness of its position. ### How it works The classic staggered animation technique required setting a custom property like --i on every :nth-child selector, then using calc(0.1s * var(--i)) for the delay. This was fragile: add a list item and you need another rule. Some developers set the index via inline styles or JavaScript instead, adding complexity. The sibling-index() function returns a 1-based integer representing the element's position among its siblings. Use it directly in calc() for staggered delays, radial positioning, or any layout that depends on element order. Its companion sibling-count() returns the total number of siblings, enabling formulas like distributing items evenly around a circle. ### Why use this - Automatic indexing: sibling-index() returns each element's 1-based position. No manual counting, no matter how many items. - Dynamic safe: Add or remove items and the stagger adapts automatically. The old nth-child approach breaks when the list changes. - Count siblings too: sibling-count() gives the total. Use both for radial layouts, equal distribution, and more. ### Modern CSS ```css li { transition: opacity .25s ease, translate .25s ease; transition-delay: calc(0.1s * (sibling-index() - 1)); } ``` --- ## Carousel navigation without a JavaScript library URL: https://modern-css.com/carousel-navigation-without-a-javascript-library/ Category: Layout Difficulty: Advanced Baseline: limited Browser support: 72% Carousels used to need libraries like Swiper.js or Slick for navigation buttons and dot indicators. CSS scroll-button and scroll-marker pseudo-elements give you native, accessible carousel UI. ### How it works Building a carousel traditionally meant a JavaScript library: Swiper.js, Slick, or Flickity. These libraries create navigation buttons, dot indicators, handle scroll snap, and manage active states. That's a lot of JavaScript and custom CSS for what is fundamentally a scrolling UI. CSS now provides two pseudo-elements for scroll containers. ::scroll-button(direction) creates prev/next buttons that scroll by ~85% of the container's visible area and auto-disable at the ends. ::scroll-marker on each item creates dot indicators grouped in a ::scroll-marker-group. Use the :target-current pseudo-class to style the active dot. Both are fully stylable with CSS. ### Why use this - Native performance: Scroll buttons and markers are browser-generated pseudo-elements. No JS, no DOM manipulation, no resize observers. - Accessible by default: Buttons are focusable and auto-disable at scroll ends. Markers work as anchor links. Keyboard and screen reader friendly. - Drop the library: Swiper.js is ~40 KB. The CSS approach is zero bytes of JavaScript and fully stylable. ### Modern CSS ```css .carousel::scroll-button(left) { content: "⬅" / "Scroll left"; } .carousel::scroll-button(right) { content: "➡" / "Scroll right"; } .carousel { scroll-marker-group: after; } .carousel li::scroll-marker { content: ''; width: 10px; height: 10px; border-radius: 50%; } ``` --- ## Vertical text centering without padding hacks URL: https://modern-css.com/vertical-text-centering-without-padding-hacks/ Category: Typography Difficulty: Beginner Baseline: limited Browser support: 79% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/text-box-trim Text always looked optically off-center because font metrics include extra space for ascenders and descenders. The text-box property trims that invisible space for true visual centering. ### How it works Every font has internal metrics: ascent (space above for accents) and descent (space below for descenders like g and y). When you center text in a button or container, the browser centers the full content box including these invisible metrics. The visual center of the actual letters ends up slightly too low. The text-box shorthand combines text-box-trim and text-box-edge. Using trim-both trims above and below, while cap alphabetic sets the trim edges to the cap height (top of capital letters) and alphabetic baseline. The result: the visible text is truly centered within its container, regardless of font or size. ### Why use this - True centering: Trims invisible font metric space above cap height and below alphabetic baseline. Math and optics finally agree. - Works everywhere: Buttons, badges, tags, headings, pills. Anywhere text looked subtly off-center now looks perfect. - Font-agnostic: The browser reads the font's actual metrics. Switching fonts doesn't break the centering. ### Modern CSS ```css .btn { padding: 10px 20px; text-box: trim-both cap alphabetic; } ``` --- ## Hover tooltips without JavaScript events URL: https://modern-css.com/hover-tooltips-without-javascript-events/ Category: Layout Difficulty: Intermediate Baseline: limited Browser support: 86% MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/popover Tooltips required JavaScript mouseenter/mouseleave listeners, focus handling, and manual positioning. Now popover=hint with the interestfor attribute gives you declarative, accessible hover UI. ### How it works Building accessible tooltips required at least four event listeners (mouseenter, mouseleave, focus, blur), a delay mechanism to prevent flicker, position logic, and careful DOM management. Libraries like Tippy.js exist specifically because this is so tedious to get right. The new popover=hint type paired with the interestfor attribute handles all of this declaratively. The interestfor attribute on the trigger element points to the tooltip's ID. The browser shows the popover when the user 'shows interest' (hover, focus, long-press) and hides it when interest ends. Customize the delay with the interest-delay CSS property. Unlike popover=auto, hint popovers coexist with other open popovers. ### Why use this - All input modes: The browser triggers on hover (mouse), focus (keyboard), and long-press (touch). You write zero event handling. - Non-destructive: Hint popovers don't close other open auto or manual popovers. Layered UI coexists naturally. - Built-in delay: The interest-delay property (default 0.5s) prevents accidental triggers. Configurable with CSS. ### Modern CSS ```css
Tooltip content
``` --- ## Modal controls without onclick handlers URL: https://modern-css.com/modal-controls-without-onclick-handlers/ Category: HTML Difficulty: Beginner Baseline: limited Browser support: 72% MDN: https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement/command Opening a dialog modally required onclick handlers calling showModal(). Invoker Commands let buttons perform actions on other elements declaratively with commandfor and command attributes. ### How it works To open a modally, you needed JavaScript: either an inline onclick that calls showModal(), or an addEventListener in a script. Same for popovers and closing. Every interactive trigger required its own JS wiring. Invoker Commands (Chrome 135+) add two HTML attributes: commandfor takes the ID of the target element, and command specifies the action. Built-in commands mirror their JS counterparts: show-modal, close, show-popover, hide-popover, toggle-popover. Custom commands prefixed with -- fire a command event on the target for extensibility. ### Why use this - Declarative: Link a button to its target with commandfor (like the for attribute on labels). No querySelector, no addEventListener. - Multiple commands: show-modal, close, show-popover, hide-popover, toggle-popover. One pattern for all interactive elements. - Custom commands too: Prefix with -- for custom commands handled by the command event. Extensible without frameworks. ### Modern CSS ```css ... ``` --- ## Dialog light dismiss without click-outside listeners URL: https://modern-css.com/dialog-light-dismiss-without-click-outside-listeners/ Category: HTML Difficulty: Beginner Baseline: limited Browser support: 69% MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog#closedby Closing a dialog when the user clicks outside required a JavaScript click listener on the backdrop. The closedby attribute gives dialogs popover-style light dismiss behavior natively. ### How it works The element didn't have a built-in way to close when clicking outside. Developers had to add a click event listener, check if the click was outside the dialog's bounding rect (since clicking the ::backdrop still fires on the dialog), and then call dialog.close(). This was error-prone and needed extra code for ESC handling too. The closedby attribute, available from Chrome 134, brings the popover's light dismiss pattern to dialogs. Set closedby="any" for full light dismiss (backdrop click + ESC), closedby="closerequest" for ESC-only, or closedby="none" to disable user-triggered closing entirely. The browser handles all the hit-testing and keyboard events natively. ### Why use this - One attribute: Add closedby="any" and the browser handles backdrop clicks and ESC key. No JS event wiring. - Consistent behavior: Works like popover light dismiss. Users get the same close-on-click-outside pattern everywhere. - Three modes: none (default), closerequest (ESC only), or any (ESC + backdrop click). Pick the right behavior per dialog. ### Modern CSS ```css Click outside or press ESC to close ``` --- ## Customizable selects without a JavaScript library URL: https://modern-css.com/customizable-selects-without-a-javascript-library/ Category: HTML Difficulty: Intermediate Baseline: limited Browser support: 96% MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/appearance Styling selects used to require JavaScript libraries like Select2 or Choices.js that replace the native element entirely. Now appearance: base-select unlocks full CSS customization of the native select. ### How it works Custom-styled selects have been one of the biggest pain points in web development. The native