1. Smooth scrolling

The line: scroll-behavior: smooth;

Before this existed, implementing smooth scroll-to-anchor required JavaScript: an event listener on every anchor, `getBoundingClientRect()`, `requestAnimationFrame()`, and careful easing math. The whole thing was 30–50 lines of code that needed testing across browsers.

Now you add one declaration to html and every in-page anchor link animates smoothly — including the browser's back/forward buttons.

❌ Before — JavaScript
document.querySelectorAll('a[href^="#"]')
  .forEach(anchor => {
    anchor.addEventListener('click', e => {
      e.preventDefault();
      const target = document.querySelector(
        anchor.getAttribute('href')
      );
      target.scrollIntoView({ behavior: 'smooth' });
    });
  });
✅ After — CSS
html {
  scroll-behavior: smooth;
}
💡 Accessibility note

Respect user preferences with @media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } } — users who have opted into reduced motion won't get the animation.

2. Sticky positioning

The line: position: sticky;

Sticky headers, table headers that stay visible, sidebars that follow you down the page — all of these used to require a scroll event listener that calculated offsets and toggled a fixed class. It caused layout jank, required cleanup on resize, and was fragile.

position: sticky does all of this natively in the browser's compositor thread, meaning zero layout thrashing and no JavaScript at all.

❌ Before — JavaScript
const header = document.querySelector('header');
const offset = header.offsetTop;

window.addEventListener('scroll', () => {
  if (window.pageYOffset >= offset) {
    header.classList.add('sticky');
  } else {
    header.classList.remove('sticky');
  }
});
✅ After — CSS
header {
  position: sticky;
  top: 0;
  z-index: 100;
}

The key thing most people miss: position: sticky only works if the element's parent isn't overflow: hidden or overflow: auto. If sticky isn't working, check your ancestor elements.

3. Aspect ratio

The line: aspect-ratio: 16 / 9;

The old "padding-top hack" for responsive aspect ratios was one of CSS's most infamous workarounds — a 0-height div with percentage padding-top. Developers had to write JavaScript resize observers to handle dynamic content. The aspect-ratio property makes this a single clean declaration.

❌ Before — The padding hack
.video-wrapper {
  position: relative;
  padding-top: 56.25%; /* 9/16 */
  height: 0;
}
.video-wrapper iframe {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
}
✅ After — CSS
.video-wrapper {
  aspect-ratio: 16 / 9;
  width: 100%;
}

/* That's it. */

Works on any element — images, videos, divs, iframes. You can even use aspect-ratio: 1 to make perfect squares without knowing the dimensions in advance.

CSS
/* Perfect squares for avatar thumbnails */
.avatar { aspect-ratio: 1; border-radius: 50%; }

/* Cinema widescreen */
.cinema { aspect-ratio: 21 / 9; }

/* Classic photo */
.photo { aspect-ratio: 4 / 3; }

/* Let the browser figure it out from the image's natural dimensions */
img { aspect-ratio: auto; }

4. Text truncation with ellipsis

The line: text-overflow: ellipsis;

Truncating text to fit a container — especially card titles or table cells — used to involve JavaScript that measured element widths, sliced strings character by character, and re-ran on every resize. It was brittle, expensive, and often wrong by a character or two.

❌ Before — JavaScript
function truncate(el, maxWidth) {
  const text = el.textContent;
  el.textContent = text;
  while (el.scrollWidth > maxWidth) {
    el.textContent = el.textContent
      .slice(0, -1);
  }
  el.textContent += '…';
}
window.addEventListener('resize',
  () => truncate(el, 200));
✅ After — CSS
.card-title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

Three properties work together here. white-space: nowrap keeps the text on one line, overflow: hidden clips it at the container boundary, and text-overflow: ellipsis adds the "…" character at the cut point. The container just needs a defined width (or max-width).

For multi-line clamping (e.g. show only 2 lines), use the WebKit-prefixed line-clamp, which now has broad support:

CSS
.card-excerpt {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Modern browsers also accept: */
.card-excerpt {
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  line-clamp: 2;
  -webkit-line-clamp: 2;
}

5. Disabling interactions

The line: pointer-events: none;

Disabling an entire section of the UI — overlay during loading, a disabled form, a preview mode — used to mean adding a JS event listener that called e.preventDefault() on every click, or wrapping everything in a transparent div that captured events. Both approaches required cleanup logic.

❌ Before — JavaScript
const overlay = document.createElement('div');
overlay.style.cssText = `
  position: absolute; inset: 0;
  z-index: 999;
`;
form.style.position = 'relative';
form.appendChild(overlay);

// And remember to remove it later...
overlay.remove();
✅ After — CSS
.form--loading {
  pointer-events: none;
  opacity: 0.6;
  cursor: not-allowed;
}

Toggle the class with one line of JavaScript — el.classList.toggle('form--loading') — and CSS handles the rest. No event interception, no overlay divs, no cleanup. The element still exists in the DOM and is still accessible to screen readers, which is often the desired behaviour.

⚠️ Accessibility reminder

pointer-events: none only prevents mouse and touch interactions. Keyboard navigation still works. If you need to fully disable an element from all interactions including keyboard, use the HTML disabled attribute on form controls or aria-disabled="true" combined with JS for custom elements.

Bonus: accent-color

The line: accent-color: #7c6fff;

This one's newer but deserves a mention. Styling native checkboxes, radio buttons, range inputs, and progress bars used to require hiding the native element and building a custom one entirely in JavaScript-managed DOM. Now a single property applies your brand color to all native form controls consistently across browsers.

CSS
/* Apply brand color to all native form controls at once */
:root {
  accent-color: #7c6fff;
}

/* Or target specific elements */
input[type="checkbox"] { accent-color: #22c55e; }
input[type="range"]    { accent-color: #7c6fff; }
progress               { accent-color: #ff6fb0; }

Why prefer CSS over JS for these?

There are three concrete reasons beyond code conciseness:

  • Performance. CSS properties like position: sticky and scroll-behavior run on the browser's compositor thread — they don't block the main thread, don't cause layout recalculations, and don't interfere with other JavaScript execution.
  • Resilience. CSS still applies if your JavaScript fails to load, is blocked by an ad blocker, or errors before running. A sticky header that requires a scroll listener breaks on that error. A CSS sticky header never will.
  • Less to maintain. Every line of JavaScript is a test to write, a dependency to update, and a potential runtime error. CSS declarations just work.

The rule of thumb: if you're reaching for JavaScript to change how something looks or behaves based on layout, check if CSS already handles it natively. Modern CSS in 2026 probably does.

Try these properties live

Use CSSTools.io to experiment with CSS transforms, shadows, gradients, and more — all with live previews and copy-ready code.

Open CSS Tools →