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

推荐订阅源

爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
G
GRAHAM CLULEY
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V2EX - 技术
V2EX - 技术
The Last Watchdog
The Last Watchdog
S
Secure Thoughts
Webroot Blog
Webroot Blog
PCI Perspectives
PCI Perspectives
L
LINUX DO - 最新话题
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
H
Heimdal Security Blog
H
Help Net Security
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
The GitHub Blog
The GitHub Blog
Jina AI
Jina AI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
F
Full Disclosure
小众软件
小众软件
S
Securelist
罗磊的独立博客
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
云风的 BLOG
云风的 BLOG
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
Know Your Adversary
Know Your Adversary
S
Schneier on Security
D
DataBreaches.Net
M
MIT News - Artificial intelligence
V
Vulnerabilities – Threatpost
N
News and Events Feed by Topic
有赞技术团队
有赞技术团队
F
Fortinet All Blogs
T
Tenable Blog
The Register - Security
The Register - Security
C
Check Point Blog
AWS News Blog
AWS News Blog
Cloudbric
Cloudbric
C
CXSECURITY Database RSS Feed - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Google Online Security Blog
Google Online Security Blog
博客园 - 叶小钗
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 司徒正美

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
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.