Modern CSS Features You No Longer Need JavaScript For
For years, frontend developers reached for JavaScript to handle interactive UI patterns. Accordions needed click handlers. Tooltips required positioning libraries. Responsive components demanded resize observers. That era is ending.
Modern CSS now handles stateful styling, component-level breakpoints, scroll-based effects, and native popovers—all without a single line of JavaScript. These aren’t experimental features. They’re stable, broadly supported, and ready for production in 2025.
Key Takeaways
- The CSS
:has()selector enables parent styling based on children’s state, eliminating JavaScript for form validation styling and interactive components. - Container queries let components respond to their container’s size rather than the viewport, replacing JavaScript resize observers.
- Scroll-driven animations run on the compositor thread, offering smoother performance than Intersection Observer alternatives.
- The Popover API and
<details name="">attribute provide native, accessible tooltips, menus, and accordions without custom scripts.
The CSS :has() Selector: Parent Selection Finally Arrives
The CSS :has() selector solves a problem developers have complained about for decades: styling parent elements based on their children’s state.
Previously, toggling a card’s appearance when its checkbox was checked required JavaScript event listeners. Now CSS handles it directly:
.card:has(input:checked) {
border-color: blue;
}
This pattern eliminates entire categories of JavaScript:
- Form validation styling: Style containers based on
:validor:invalidinputs - State-driven layouts: Change parent grids when children are empty or present
- Interactive components: Build tabs, accordions, and toggles using hidden inputs and
:has()
The selector works across all major browsers. For older browser support, wrap styles in @supports selector(:has(*)) and provide a baseline experience.
CSS Container Queries: Component-Level Responsiveness
Media queries respond to viewport size. CSS container queries respond to a component’s container size—a fundamental shift in how we build responsive layouts.
.card-wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
This matters because components live in different contexts. A card in a sidebar behaves differently than the same card in a main content area. Container queries let components adapt to their actual available space, not the browser window.
Before container queries, achieving this required JavaScript resize observers and manual class toggling. Now it’s declarative CSS.
Scroll-Driven Animations: No Intersection Observer Required
Scroll-triggered animations traditionally meant importing libraries or writing Intersection Observer code. CSS scroll-driven animations replace both.
Two timeline types handle most use cases:
Scroll timelines tie animation progress to scroll position:
@keyframes grow {
from { width: 0; }
to { width: 100%; }
}
.progress-bar {
animation: grow linear;
animation-timeline: scroll();
}
View timelines trigger when elements enter the viewport:
@keyframes fade-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.reveal {
animation: fade-in linear;
animation-timeline: view();
animation-range: entry 0% cover 30%;
}
These animations run on the compositor thread, delivering smoother performance than JavaScript alternatives. Always respect user preferences with @media (prefers-reduced-motion: reduce) to disable or simplify animations.
Discover how at OpenReplay.com.
The Popover API: Native Tooltips and Menus
Building accessible popovers historically required managing focus traps, click-outside detection, escape key handling, and z-index stacking. The Popover API handles all of this natively.
<button popovertarget="menu">Open Menu</button>
<div id="menu" popover>
<p>Menu content here</p>
</div>
The browser automatically:
- Positions the popover in the top layer (above all other content)
- Closes it when clicking outside or pressing Escape
- Manages focus appropriately
- Handles accessibility announcements
Style popovers with CSS, including entry animations via @starting-style. The popover="auto" default closes when users interact elsewhere, while popover="manual" requires explicit dismissal.
Native Accordions with <details name="">
The <details> element has supported accordions for years. The name attribute adds exclusive behavior—only one panel open at a time:
<details name="faq">
<summary>First question</summary>
<p>Answer content</p>
</details>
<details name="faq">
<summary>Second question</summary>
<p>Answer content</p>
</details>
No JavaScript. Full keyboard accessibility. Screen reader support built in. Style the [open] state and ::marker pseudo-element to match your design system.
Progressive Enhancement Still Matters
These features enjoy broad support, but progressive enhancement remains good practice. Use @supports to provide fallbacks:
@supports not (container-type: inline-size) {
/* Fallback styles using media queries */
}
This approach ensures baseline functionality everywhere while delivering improved experiences where supported.
Conclusion
Modern CSS without JavaScript isn’t about avoiding JavaScript entirely—it’s about choosing the right tool. Declarative CSS solutions are faster to implement, easier to maintain, and often more performant than their scripted equivalents.
Start by auditing your current JavaScript. If it’s toggling classes based on state, positioning tooltips, or watching scroll position, CSS likely handles it now. The browser does the heavy lifting. Let it.
FAQs
The features covered here—:has(), container queries, scroll-driven animations, and the Popover API—are supported in all major browsers including Chrome, Firefox, Safari, and Edge as of 2025. Scroll-driven animations have the narrowest support, so always check caniuse.com for current compatibility data and use @supports for fallbacks.
The :has() selector handles many state-based styling scenarios but has limits. It works well for styling based on form states, child presence, or sibling conditions. For complex multi-step logic, conditional rendering, or data fetching, JavaScript remains necessary. Use :has() for visual state changes, not application logic.
CSS scroll-driven animations typically outperform JavaScript alternatives because they run on the compositor thread, separate from the main thread. This prevents layout thrashing and jank. However, animating properties that trigger layout recalculations like width or height can still cause performance issues. Stick to transform and opacity for best results.
Use the @supports rule to detect feature availability and provide alternative styles. For example, @supports not (container-type: inline-size) lets you define media query fallbacks. For JavaScript-dependent fallbacks, check for feature support in your scripts before initializing polyfills or alternative implementations.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.