CSS Just Got a Parent Selector. Your Forms Will Never Look the Same

2026年8月13日2 次浏览来源:Dev.to阅读原文

For as long as I've been writing CSS, there's been one direction it refused to look: up.

You could style a child based on its parent all day long, but the second you wanted a parent to react to something happening inside it — a checked checkbox, an invalid field, a filled-in input — you were reaching for JavaScript.

Every time.

It didn't matter how small the interaction was. breaks that rule on purpose, and it's been safe to use in production for a while now — it's supported across Chrome, Edge, Firefox, Safari, and Opera, no polyfill required.

I didn't fully appreciate what that meant until I rebuilt a form I'd been maintaining for two years and deleted most of the JavaScript in it.

Not all of it — I'll get to where it still earns its place — but most.

The rule CSS used to have reads as "select this element, if it contains a match for whatever's inside the parentheses." Once that clicks, a huge category of things people were writing calls for turns into a single selector.

Styling a label when its input is focused This used to mean a and listener on the input, toggling a class on the label.

Now: Wrap the label and input in a container, and the whole field lights up the moment the input inside it gets focus — no listener, no class toggle, and it can never drift out of sync with the actual focus state, because it is the actual focus state.

Required-field indicators that can't go stale I've fixed this bug more times than I want to admit: a form gets a field added, and someone forgets to also add the little red asterisk that's supposed to mark it required.

With , the asterisk isn't a separate thing you remember to add — it's derived directly from the attribute: Add to the input, the asterisk appears.

Remove it, the asterisk disappears.

There's no second place to update, so there's no way for the two to disagree.

Validation styling without a single event listener This is the one that actually made me sit up.

Native HTML validation states — , — have existed forever, but styling the field wrapper based on the input's validity used to be impossible without JS, because the wrapper isn't the thing that's valid or invalid, the input is.

The part matters more than it looks like it should — without it, every empty required field shows as an error the instant the page loads, before the person has even had a chance to type anything.

Pairing it with means the error only shows once someone's actually interacted with the field and left it wrong.

A submit button that enables itself The button disables itself as long as anything in the form is invalid, and re-enables the instant everything passes native validation.

I want to flag something here though, because it's the kind of detail that's easy to skip past: blocks clicks, but it doesn't stop a screen reader user from tabbing to the button and trying to activate it, and it gives no explanation for why nothing happened.

If accessibility matters for your form — and it should — pair this with the actual attribute set via a tiny bit of JS, or at minimum make sure your fields' own validation messages are what's actually communicating the problem, not the button's disabled-looking state.

Counting checked items and styling accordingly A "Clear filters" button that only appears once at least one filter checkbox is actually checked.

I used to wire this up with a listener that recounted checked boxes on every click.

Now it's two selectors.

Where I'd still reach for JavaScript I don't want to oversell this. covers presentation — how things look based on state that already exists in the DOM.

It doesn't cover behavior that needs to happen at a specific moment: submitting data to a server, showing a toast on success, redirecting after login, debouncing a search-as-you-type field.

Those are still JavaScript's job, and no amount of clever selector nesting changes that.

What actually replaced in my form wasn't the logic — it was the busywork.

All the little / calls that existed purely to mirror state the browser already knew, just so CSS could react to it.

That's the part that's gone, and it's also the part that used to drift out of sync and cause bugs nobody could reproduce reliably.

One real performance caveat has to check the subtree underneath the element it's evaluating, which means a selector like forces the browser to search a huge chunk of the page for every state change.

Keep the element you're calling on as narrow as possible — a wrapper around one input, not the whole form, and definitely not the whole page — and keep what's inside the parentheses specific. is cheap. on a content-heavy page is the kind of thing that can genuinely show up in a performance profile.

Try it on something small first If you haven't used yet, don't start by rewriting an entire form system.

Pick one spot where you're currently toggling a class purely to mirror state the DOM already has — a focused input, a checked box, a filled field — and swap it for a selector.

It's a small change, but it's the kind that quietly removes a whole category of "why did this get out of sync" bugs from your codebase.

We're ArtClick, a web development agency based in Kyoto.

We build company websites, WordPress sites, and custom systems — with a focus on sites that are fast, well-designed, and easy to maintain long-term.

Learn more at artclickdev

分享