惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
腾讯CDC
Jina AI
Jina AI
I
InfoQ
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
S
SegmentFault 最新的问题
Blog — PlanetScale
Blog — PlanetScale
Stack Overflow Blog
Stack Overflow Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
美团技术团队
MyScale Blog
MyScale Blog
量子位

DEV Community

Terraform with AI: Build AWS Infra (Cursor + MCP) What If AI Didn’t Need the Internet? 750,000 Chips, 140 Trillion Tokens: The Math Behind DeepSeek's Permanent Price Cut You're Renting Someone Else's Compute — And It's Costing You More Than You Think Five Clusters. Five Lessons. One Production System. Synaptic: A Local-First AI Dev Companion That Remembers How You Think Revolutionizing Edge MedTech: Building a Sovereign Sleep Apnea Companion ("XiHan Snore Coach") with Gemma 4 HDD Eksternal Tiba-Tiba Tidak Bisa Diakses di Windows? Ini Tiga Lapis Fix-nya DMARC p=none vs p=quarantine vs p=reject: what to use and when DSA Application in Real Life: How Git Diff Works: LCS Intuition, Myers Algorithm, and Real Code Changes I solo-built a reputation layer for AI agents on NEAR — and here's what I learned I built an AI faceless video generator in 2 months — here's the stack Diffusion Language Models: How NVIDIA Nemotron-Labs Diffusion Shatters the Autoregressive Speed Ceiling llm-nano-vm v0.8.0 — deterministic FSM runtime for LLM pipelines, now with output validation and per-step timeouts From the Renaissance to the Quantum Dawn: AI, Computation, and the Next Paradigm Shift How I Built a Review Site with 800+ Articles Using AI I Built a Smart Kitchen AI with Gemma 4 That Turns Fridge Photos Into Recipes Why your vulnerability dashboard is lying to you (and how to fix it) From Abandoned Prototype to Smart AI System: Reviving Trafiq AI with GitHub Copilot Why Country/State/City Pickers Are Weirdly Hard Node.js 22 LTS — EOL Date, Support Timeline, and What Comes Next The 7-Layer Memory Architecture Behind Modern AI Agents I Imagined Hermes Agent Running an Entire Smart City — And It Changed How I See AI One backend, four products: why we bet on platform-per-brand AI's tech debt is invisible — even to AI. I solved it at the architecture layer. Why ROAS 300% Can Still Mean Losses — Gross Margin in 5 Ecommerce Verticals You Don’t Need to Try Every AI Tool to Keep Up NovelPilot: A Novel Writing Agent Powered by Gemma 4 BoxAgnts is an Out-Of-The-Box Secure AI Agent ToolBox in a WASM SandBox Gemma 4 deep dive: why a 1.5 GB model scores 37.5% on competition mathematics, how the MoE routing actually works, and which model fits your hardware. Full breakdown inside. BeeLlama v0.2.0: 164 tok/s on a 27B model, one RTX 3090 Google Just Declared the Chat-Log Interface Dead. Here's What Neural Expressive Actually Signals for Developers. ARCHITECTURE SPECIFICATION & FORMAL SYSTEM REPORT: k501-AIONARC Notes from a Hammock What's Google Antigravity 2.0 ? Here's What the Agent Harness Actually Changes for Developers. Building an E2EE Chat App in Flask - Part 3: Keeping File Uploads Safe Google's Gemini Spark. Here's What It Actually Does for Developers. Microsoft Just Shipped MCP Governance for .NET. Here's What It Actually Enforces. How I Built a Pakistan Internet Speed Test Platform at 16 How to Build a Supervisor Agent Architecture Without Frameworks I Built My Own Corner of the Internet — Here's What It Looks Like How does VuReact compile Vue 3's defineExpose() to React? Neo-VECTR's Rift Ascent Idempotency Keys: The API Safety Net You Probably Aren't Using Building E-Commerce Sites for Niche Products: Technical Lessons from Specialty Outdoor Retailers Audit Logs: The Silent Guardian of Every Serious System Open-source SDS tooling for Japanese MHLW compliance: the gap nobody filled BetAGracevI I Built a Post-Quantum Cryptographic Identity SDK for AI Agents — Here's Why It Needs to Exist Running Claude Code across multiple repos without losing context There Are Cameras in Every Room of My House. I Put Them There. Why your AI agent loops forever (and how to break the cycle) How does VuReact compile Vue 3's defineSlots() to React? Building a Privacy-First Resume Editor with Typst WASM and React One Soul, Any Model: Portable Memory for Open-Source Agents with .klickd From Pixels to Prescriptions: Building an Autonomous Healthcare Booking Agent with LangGraph MonoGame - A Game Engine for Those Who Love Reinventing the Wheel # Day 24: In Solana, Everything is an Account Mastering Node.js HTTP Module: Build Servers, REST APIs, and Handle Requests Mastering Node.js HTTP Module: Build Servers, REST APIs, and Handle Requests RP2040 Wristwatch Tells Time With a Vintage VU Meter Needle observations about models / 2026, may From Video Transcripts to Source-Grounded AI Notes: A Practical Look at Notesnip AI Agent Dev Environment Guide — Real Experience from an AI Living Inside a Server How I Run 7 AI Models 24/7: Multi-Agent Architecture in Practice What exactly changes with the Claude Max plan? I Revived a Broken MLOps Platform — Now It's Self-Service, Policy-Guarded, and Operationally Credible OpenAI's $2M-tokens-for-equity YC deal, decoded Why DMX Infrastructure is Still Stuck in the 90s Agent Series (2): ReAct — The Most Important Agent Reasoning Paradigm Open Source Project (No.73): Sub2API - All-in-One Claude/OpenAI/Gemini Subscription-to-API Relay I Made the Wrong Bet on Event Streaming in Our Treasure Hunt Engine #ai #productivity #chatgpt #python Symbolic Constant Conundrum From Manual RAG to Real Retrieval — Embedding-Based RAG with NVIDIA NIM Building an outbound-only WebSocket bridge for local AI agents Our System's Sins in Ghana: Why We Had to Rethink Digital Product Sales Execution Governance, AI Drift, and the Security Paradox of Runtime Enforcement Differential Pair Impedance: Why USB and HDMI Routing Is a Geometry Problem Small AI database questions can become big scans Claude Code 2.1 Agent View & /goal: Autonomous Dev Guide 2026 Your AI database agent should not see every column Rust's Low-Latency Conquest: Why We Ditched C++ for a Treasure Hunt Engine Floating-point will quietly corrupt your emissions math, and 0.1 + 0.2 already warned you Autonomous Agents: what breaks first (and why that's the real product) [2026-05-23] Agent payments are the new cloud bill footgun ORA-00069 오류 원인과 해결 방법 완벽 가이드 How I Built a Local, Multimodal Gemma 4 Visual Regression & Patch Agent: Closed-Loop Validation, Canvas Pixel Diffing, and Reproducible Benchmarks Pressure-testing Ota on Supabase: from setup prose to executable repo readiness VPC CNI en EKS: cómo dejar de pagar nodos que no usás The Future of Text Analysis: Introducing TechnoHelps Semantic Engine I built a Chrome Extension that saves product images + context directly to Google Drive & Sheets 95+ browser-based dev tools that never touch a server Running Qwen 2.5 Coder 14B Locally in Cursor with Ollama From a 10,000-line OpenSearch export script to a log analysis tool Ghost Bugs Cost $40K: A Neural Debugging Postmortem SECPAC: A Lightweight CLI Tool to Password-Protect Your Environment Variables 🚀 PasteCheck v1.7 + v1.8 — Hints that tell you what to fix, and a nudge panel that tells you where to start 8 Real Ways Developers Make Money in 2026 (Ranked by Effort) I built a free AI-powered Git CLI that writes your commit messages for you
CSS :has() Selector: The Layout Trick I Wish I Knew 5 Years Ago
Richard Lemo · 2026-05-23 · via DEV Community

CSS :has() is not just a fancy :parent

When :has() started popping up in specs and tweets, I mentally filed it under “cool, but not for shipping work.” I was wrong.

Now it is in Chrome, Safari, Edge, and Firefox. I use it in real projects. It has removed entire JavaScript files and a pile of .is-active classes that I was embarrassed to maintain.

If you are a working frontend dev, the shorthand is this: :has() turns CSS from “style what is there” into “style this thing if it contains that thing”. That one capability changes layout, state, and validation flows.

I will walk through three places where it made a real difference for me:

  • Parent styling without JS
  • Sibling state UIs without wiring events
  • Form validation UI that reacts to the DOM, not a framework

All of this shipped with zero additional JavaScript.

Quick mental model of :has()

The syntax looks like a pseudo class on a selector:

.card:has(img.hero) {
  /* styles here */
}

Read it as: “select .card elements that have a descendant img.hero somewhere inside.” It is a conditional filter on the left side of the selector.

You can also scope it more tightly:

.tabs:has(> .tab.is-active) {
  /* direct children only */
}

Or use it with relational selectors like siblings:

.field:has(+ .field--error) {
  /* this .field is followed by an error field */
}

Once that clicks, you start seeing places to remove JS.

1. Parent styling in a content-heavy project

First real use: a content-heavy marketing site for a biohacking brand I work with. Editors can drop components in any order with a CMS. Sometimes a card has an image, sometimes it is text-only. The layout should adapt.

Previously I solved this with modifier classes from the CMS, or a hydration script that scans the DOM and adds classes like .card--with-media. Boring, fragile, and slightly gross.

With :has() I deleted that script.

Image-aware cards

The card markup is boring on purpose:

<article class="card">
  <img class="card__media" src="hero.jpg" alt="">
  <div class="card__body">
    <h2>Title</h2>
    <p>Some text...</p>
  </div>
</article>

<article class="card">
  <div class="card__body">
    <h2>Another Card</h2>
    <p>Text-only card.</p>
  </div>
</article>

Now the CSS decides layout based on presence of media.

.card {
  display: grid;
  gap: 1rem;
}

.card:has(.card__media) {
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  align-items: center;
}

.card:not(:has(.card__media)) {
  padding: 2rem;
  background: #111;
  color: #eee;
}

Result: if marketing drops in an image, the card becomes a two-column layout. If not, it becomes a full-width text block. No new class. No CMS configuration. No JS.

I like this because the markup stays semantic and dumb. The layout is a true function of the content, which is what CSS was always supposed to do but rarely could at the parent level.

Auto-promoting “hero” sections

Same project. Editors could add a .section stack: some had a prominent CTA, some were just copy. If a section had a primary CTA, design wanted extra padding and a gradient background.

<section class="section">
  <h2>Get early access</h2>
  <p>Short description.</p>
  <a class="btn btn--primary" href="#">Join the beta</a>
</section>

<section class="section">
  <h2>What you get</h2>
  <p>More text...</p>
</section>

With :has() I treat any section with a primary button as a pseudo hero.

.section {
  padding: 2rem 1.5rem;
  background: #050505;
}

.section:has(.btn--primary) {
  padding: 4rem 1.5rem;
  background: radial-gradient(circle at top, #2f80ed, #050505);
  color: #fff;
}

.section:has(.btn--primary) h2 {
  font-size: 2.25rem;
}

That tiny selector replaced a custom “hero” block type in the CMS that content editors kept misusing. I stopped explaining “use the hero component for this” and just let the CSS infer intent from presence of a primary CTA.

You can do similar things with :has(video), :has(.badge--new), etc. It is a good fit for messy CMS content where you want layout to respond to what your editors actually do, not what the schema designer hoped they would do.

2. Sibling state UIs without event listeners

Second use case: stateful UIs that I used to wire up with click handlers. Tabs, disclosure panels, navigation highlights, that stuff.

Yes, you can still do it in JS. But if the state is already visible in the DOM, :has() lets CSS own more of the behavior. That means less code, fewer states to sync, and fewer bugs.

Tabs powered by :target and :has()

On a little side project for baseball drills, I built a tabbed interface where each tab is actually a link to an anchor. I wanted a sticky tab bar that changes style when any tab content is active.

<div class="tabs">
  <nav class="tabs__nav">
    <a href="#hitting">Hitting</a>
    <a href="#pitching">Pitching</a>
    <a href="#fielding">Fielding</a>
  </nav>

  <section id="hitting" class="tabs__panel">...</section>
  <section id="pitching" class="tabs__panel">...</section>
  <section id="fielding" class="tabs__panel">...</section>
</div>

The panels show / hide with a regular :target trick.

.tabs__panel {
  display: none;
}

.tabs__panel:target {
  display: block;
}

Old me would now add JS to toggle classes on the nav. Instead I lean on :has().

.tabs {
  border-bottom: 1px solid #333;
}

.tabs__nav a {
  padding: .5rem 1rem;
  text-decoration: none;
  color: #888;
}

.tabs__nav a:is(:hover, :focus-visible) {
  color: #fff;
}

/* highlight the active tab label */
.tabs__nav a[href^="#"] {
  position: relative;
}

.tabs:has(#hitting:target) .tabs__nav a[href="#hitting"],
.tabs:has(#pitching:target) .tabs__nav a[href="#pitching"],
.tabs:has(#fielding:target) .tabs__nav a[href="#fielding"] {
  color: #fff;
  font-weight: 600;
}

/* make the whole tabs block look active if any panel is targeted */
.tabs:has(.tabs__panel:target) {
  border-color: #2f80ed;
}

I am not pretending this scales to 50 tabs. For most content UIs, 3 to 5 tabs is realistic. Writing those few selectors is still cheaper than adding a tab manager, handling history state, and worrying about hydration.

The key pattern is: some child panel already has state via :target or [aria-selected="true"]. Let :has() bubble that state up to parents and siblings.

Accordion with native <details> and :has()

I use <details> a lot. It is surprisingly powerful with :has(). On a settings panel I wanted the container to visually compress when no section was open, then expand once any accordion entry was open.

<section class="settings">
  <details class="settings__item">
    <summary>Profile</summary>
    <div>...</div>
  </details>
  <details class="settings__item">
    <summary>Privacy</summary>
    <div>...</div>
  </details>
</section>

CSS:

.settings {
  padding: 1rem;
  border-radius: .75rem;
  border: 1px solid #333;
  max-height: 60vh;
  overflow: auto;
  transition: box-shadow .2s ease, border-color .2s ease;
}

.settings:has(.settings__item[open]) {
  border-color: #2f80ed;
  box-shadow: 0 16px 40px rgba(0, 0, 0, .55);
}

.settings__item + .settings__item {
  border-top: 1px solid #222;
}

.settings__item summary {
  cursor: pointer;
}

Once any <details> is open, the whole settings block feels “in focus”. No JS to listen for the toggle event, no syncing of .is-active classes. The HTML already has [open]. CSS reacts.

3. Form validation UI with zero JavaScript

The biggest win for me: form UI that uses :has() with built-in browser validation. No client-side validation library. No “touched” state juggling.

On my own site I revamped a contact form and a simple experiment log form. I wanted:

  • Parent field wrappers that highlight error or success
  • Inline messages that only show when actually invalid
  • Submit button that changes state based on form validity

Browser validation already tracks validity. The DOM knows. :has() lets CSS hook into that.

Field states from input validity

Markup:

<form class="form" novalidate>
  <div class="field">
    <label>
      Email
      <input type="email" name="email" required>
    </label>
    <p class="field__error">Please enter a valid email.</p>
  </div>

  <div class="field">
    <label>
      Message
      <textarea name="message" minlength="10" required></textarea>
    </label>
    <p class="field__error">Write at least 10 characters.</p>
  </div>

  <button type="submit">Send</button>
</form>

You can bind field styling to the input inside, purely with CSS.

.field {
  margin-bottom: 1.5rem;
}

.field input,
.field textarea {
  width: 100%;
  padding: .6rem .75rem;
  border-radius: .4rem;
  border: 1px solid #444;
  background: #050505;
  color: #eee;
}

.field__error {
  display: none;
  margin-top: .35rem;
  font-size: .8rem;
  color: #ff6b6b;
}

/* highlight when invalid and touched (using :user-invalid where supported) */
.field:has(input:user-invalid),
.field:has(textarea:user-invalid) {
  color: #ff6b6b;
}

.field:has(input:user-invalid) input,
.field:has(textarea:user-invalid) textarea {
  border-color: #ff6b6b;
  box-shadow: 0 0 0 1px rgba(255, 107, 107, .6);
}

.field:has(input:user-invalid) .field__error,
.field:has(textarea:user-invalid) .field__error {
  display: block;
}

/* success state */
.field:has(input:user-valid),
.field:has(textarea:user-valid) {
  color: #4caf50;
}

.field:has(input:user-valid) input,
.field:has(textarea:user-valid) textarea {
  border-color: #4caf50;
}

No custom event handlers. The browser decides when the input is valid or invalid. CSS uses :has() to move that state to the wrapper and the message.

If you want broader support than :user-invalid, you can fall back to :invalid and accept that some browsers show the state earlier.

Form-level feedback and submit button state

Now zoom out one level. The entire <form> element also exposes validity via :valid and :invalid. Combine that with :has() and your submit button can react.

.form button[type="submit"] {
  padding: .7rem 1.25rem;
  border-radius: .4rem;
  border: none;
  background: #333;
  color: #aaa;
  cursor: not-allowed;
  transition: background .15s ease, color .15s ease, transform .05s;
}

/* any invalid field keeps button in "disabled" style */
.form:has(:invalid) button[type="submit"] {
  background: #333;
  color: #777;
}

/* all fields valid, button goes live */
.form:has(:valid) button[type="submit"] {
  background: #2f80ed;
  color: #fff;
  cursor: pointer;
}

.form:has(:valid) button[type="submit"]:active {
  transform: translateY(1px);
}

If you want to actually disable the button, you still need a tiny bit of JS to toggle the disabled attribute. I usually do not bother for simple forms; the button just looks inactive until the browser considers the form valid.

The nice part is that the logic lives where it belongs. The browser enforces constraints. CSS reads that state. JS, if present at all, sends the request and displays a toast.

4. Layout tweaks based on children, not breakpoints

One more pattern that has crept into my “default toolkit”: adjusting layout based on how many items a container has.

On my baseball drills page, each drill has one or more tags. I wanted single-tag drills to show the tag inline next to the title, and multi-tag drills to move them into a separate row. Doing that in JS felt silly.

<article class="drill">
  <header class="drill__header">
    <h3 class="drill__title">Front toss</h3>
    <div class="drill__tags">
      <span class="tag">Hitting</span>
    </div>
  </header>
</article>

<article class="drill">
  <header class="drill__header">
    <h3 class="drill__title">Relay race</h3>
    <div class="drill__tags">
      <span class="tag">Fielding</span>
      <span class="tag">Conditioning</span>
    </div>
  </header>
</article>

With :has() and the :nth-child() selector you can treat the two cases differently.

.drill__header {
  display: flex;
  gap: .5rem;
  align-items: baseline;
  flex-wrap: wrap;
}

/* one tag only: keep inline */
.drill__tags:has(.tag:nth-child(1):last-child) {
  order: 0;
}

/* more than one tag: push tags to next line */
.drill__tags:has(.tag:nth-child(2)) {
  flex-basis: 100%;
  order: 1;
}

No JavaScript counting nodes. No data attributes. Just “if there is at least a second tag, change layout”. If product decides to add a third or fourth tag, the CSS keeps working.

Reality check: performance and support

I am not going to pretend :has() is free. The browser has to do more work, because selectors now depend on what is inside elements and how that changes.

My take after profiling a few real pages: do not go wild with global *:has(...) selectors. Scope them. Prefer direct children or close relationships.

/* Bad idea */
*:has(.error) { ... }

/* Reasonable */
.form:has(.field__error) { ... }

/* Even better */
.form:has(.field > .field__error) { ... }

Support is good now. Chrome, Edge, Safari, Firefox all ship :has(). Old Safari versions are the main risk. If you work on something critical for a weird enterprise fleet, check caniuse and add progressive enhancement.

Most of my patterns above fail gracefully. You lose a highlight or a layout tweak, not core functionality. That is a good bar to aim for.

How I think about :has() now

I used to reach for JavaScript whenever a parent needed to know about a child, or a sibling needed to react to state. That felt normal. It also created a lot of glue code that did not age well.

Now my filter is simple:

  • Is the state already visible in the DOM? (attribute, pseudo class, anchor, etc.)
  • Can that state reasonably drive styling only?

If the answer is yes, I try :has() first. JS comes later, if at all.

Five years ago I was writing tab managers and form validators by hand. I would not go back. :has() is the layout trick that finally lets CSS act on the structure we already have, instead of the utility classes we wish we had planned better.

If you have a component that keeps growing event listeners and state flags, look at the HTML for five minutes. There is a decent chance :has() can take some of that weight off.