Six CSS Features That Replace Whole JavaScript Libraries in 2026
Hello everyone😁.
If in 2026, you are still using JavaScript to listen to scroll events for parallax animations, still using ResizeObserver for component-level responsiveness, and still importing Popper.js to position a tiny tooltip...
Then you owe the browser an apology😀.
Over the past two years, CSS has undergone an extremely violent capability leap at the bottom layer. A large number of interactive and layout logics that previously had to rely on JavaScript can now be achieved with a single line of CSS declaration. Moreover, because these capabilities run directly at the bottom layer of the browser's rendering engine, their performance is unmatched by any JavaScript solution.
Below are 6 pure CSS new features that have gained full support from mainstream browsers by 2026, but are still severely underestimated by many domestic front-end developers👇.
:has() Selector — Let Parent Elements Perceive Child Elements
In the 20-plus-year history of CSS, there has been a heart-wrenching flaw for all front-end developers: You can never reverse-control the style of a parent element based on the state of a child element.
For example, a very common form scenario: when an input field gains focus, highlight its outer container. In the past, you had to use JavaScript to listen to focus and blur events, and manually toggle a class on the parent element.
JavaScript Manual Listening
// To make the parent container perceive the focus state of a child element, forced to write a bunch of event listeners🤷♂️
const input = document.querySelector('.form-input');
const wrapper = document.querySelector('.form-wrapper');
input.addEventListener('focus', () => {
wrapper.classList.add('is-focused');
});
input.addEventListener('blur', () => {
wrapper.classList.remove('is-focused');
});
.form-wrapper.is-focused {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
Now it's a one-line CSS instant kill⚡
/* Parent element directly perceives the focus state of the inner input, zero JavaScript */
.form-wrapper:has(input:focus) {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
The power of the :has() selector goes far beyond this. You can use .card:has(img) to distinguish layouts for cards with and without images; use form:has(:invalid) to directly disable the visual style of the submit button when there are validation errors in the form; even use body:has(.modal-open) to lock background scrolling when a modal is open.
This single selector eliminates countless state-switching JavaScript from the past.
@container Container Queries
Traditional @media media queries respond to the width of the entire viewport. But in the era of component-based development, the same card component might be placed in a sidebar with a width of 300px, or in a main content area with a width of 800px. The viewport width simply cannot reflect the actual space of the component itself.
In the past, to achieve component-level responsiveness, you had to introduce ResizeObserver, write a large chunk of JavaScript to monitor container size changes, and then manually switch layouts.
ResizeObserver Manual Listening
// To make the card component switch layout based on its own container width, forced to introduce a JS observer
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const width = entry.contentRect.width;
if (width < 400) {
entry.target.classList.add('compact');
entry.target.classList.remove('wide');
} else {
entry.target.classList.add('wide');
entry.target.classList.remove('compact');
}
}
});
observer.observe(document.querySelector('.card-container'));
Pure CSS Container Queries
/* Declare container */
.card-container {
container-type: inline-size;
}
/* Component automatically switches layout based on its own container width, zero JavaScript */
.card {
display: grid;
grid-template-columns: 1fr;
}
@container (min-width: 400px) {
.card {
grid-template-columns: 200px 1fr;
}
}
Components no longer need to care about where they are placed on the page; they automatically adapt their layout based on their own physical space. This is the capability leap closest to intelligent components in the history of CSS.
Scroll-Driven Animations — Completely Eliminate scroll Event Listening
In the past, any animation effect related to scrolling (progress bars, parallax, fade-in/fade-out) required listening to the scroll event. And the scroll event is one of the highest-frequency events triggered in the browser; if not handled properly, it easily leads to main thread jank and dropped frames.
JavaScript Listening to Scroll
// To implement a top reading progress bar, forced to frantically listen to the scroll event
window.addEventListener('scroll', () => {
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = (scrollTop / scrollHeight) * 100;
document.querySelector('.progress-bar').style.width = `${progress}%`;
});
This code triggers dozens or even hundreds of callbacks per second when the user scrolls quickly, directly draining the main thread's computing power.
Now it's pure CSS scroll-driven animation
/* Top reading progress bar, completely driven by the browser rendering engine on the compositor layer, zero JS, zero jank */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: #3b82f6;
width: 0%;
/* Bind to the page's scroll progress, browser bottom layer automatically calculates */
animation: grow-progress linear;
animation-timeline: scroll();
}
@keyframes grow-progress {
from { width: 0%; }
to { width: 100%; }
}
The entire animation is completely handed over to the browser's bottom-layer Compositor Thread to drive, not occupying a single bit of the main thread's computing power. This performance gap cannot be bridged by optimizing a few lines of JavaScript; it's a dimensionality reduction strike at the architectural level.
@starting-style — Natively Implement Entry Animations for display: none
This feature solves a deep-water pain point that has plagued front-end development for over a decade: You cannot transition animate the switch from display: none to display: block.
In the past, to achieve a fade-in effect for a modal, you either had to import an animation library like Framer Motion, or use JavaScript to first set the element to display: block, then switch opacity in the next frame, which was extremely ugly.
JavaScript Double-Frame Hack
// To make a modal fade in from display:none, forced to write this disgusting double-frame hack
function showModal(el) {
el.style.display = 'block';
// Force browser reflow, otherwise transition won't trigger
el.offsetHeight; // This line seems meaningless, but deleting it breaks the animation
el.classList.add('visible');
}
Pure CSS Native Entry Animation
.modal {
display: none;
opacity: 0;
transition: opacity 0.3s ease, display 0.3s ease allow-discrete;
}
.modal.open {
display: block;
opacity: 1;
/* Tell the browser: when switching from display:none, start opacity from 0 */
@starting-style {
opacity: 0;
}
}
The browser natively supports transition animations for discrete properties (like display). No JavaScript needed, no animation library needed; a modal's fade-in and fade-out is perfectly solved with a few lines of CSS🤷♂️.
field-sizing: content — Auto-expanding textarea
In the past, to make a multi-line input box automatically adjust its height based on content (instead of having a fixed, rigid height), you needed to use JavaScript to listen to the input event, dynamically calculate scrollHeight, and assign it to style.height.
JavaScript Manual Height Expansion
const textarea = document.querySelector('textarea');
textarea.addEventListener('input', () => {
textarea.style.height = 'auto'; // Reset first, otherwise it only gets taller
textarea.style.height = textarea.scrollHeight + 'px';
});
Now it's one line of CSS
textarea {
field-sizing: content; /* Browser automatically expands height based on content, zero JavaScript */
}
One line of CSS eliminates a classic JavaScript hack that has circulated in the front-end circle for over a decade 🖐️.
CSS Anchor Positioning — Native Tooltip Positioning
In the past, whenever you needed to implement an effect where clicking a button pops up a tooltip, and the tooltip automatically sticks next to the button, you almost had to import a positioning library like Popper.js or Floating UI. Because manually calculating positions and handling boundary overflow flipping is extremely complex logic.
Using CSS Native Anchor Positioning
/* Declare anchor */
.trigger-button {
anchor-name: --my-anchor;
}
/* Tooltip automatically positions below the anchor, automatically flips on overflow */
.tooltip {
position: fixed;
position-anchor: --my-anchor;
top: anchor(bottom);
left: anchor(center);
/* Native overflow flipping logic, browser automatically handles it */
position-try-fallbacks: flip-block, flip-inline;
}
No JavaScript library needed, no need to calculate getBoundingClientRect, no need to handle scroll offsets. The browser's rendering engine handles everything directly at the layout stage, including boundary detection and automatic flipping. This means you can delete an entire positioning library from your project's dependencies🙌.
Know When Not to Write JavaScript
This round of capability explosion in CSS is essentially browser vendors sinking more and more interaction decision-making power from the JavaScript main thread down to the bottom layer of the rendering engine.
This is not just about writing a few fewer lines of code. Every line of JavaScript replaced by CSS means one less burden on the main thread, a higher rendering frame rate for the page, and a few milliseconds faster user interaction response.
That's all for today, likes and saves appreciated🙏
Top 2 of 3 from juejin.cn, machine-translated. The original thread is authoritative.
Checked caniuse, basically compatible with mainstream PC browsers, except IE. Mobile compatibility is average, need to consider the use case before using.
Exactly 👍!
Good stuff