CSS Direct Child Selector

The CSS direct child selector (also called the child combinator) is one of those small tools that quietly saves your CSS from becoming a tangled mess. It lets you say: “Only style elements that are immediate children of this parent… not deeper descendants.”

In this tutorial, we’ll learn the child combinator (>), compare it to the descendant selector (space), and then level up with patterns like > *, > :first-child, :only-child, and even the modern parent selector power move: :has(> selector).

What is the CSS direct child selector

The direct child selector uses the > symbol between two selectors:

  • .parent > .child matches only .child elements that are immediate children of .parent.
  • It does not match grandchildren, great-grandchildren, or anything deeper.

If you remember one sentence, make it this: > matches direct children only.

.box > p {
  outline: 3px solid #111;
}
.box p {
  outline: 3px solid #111;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
}

.box {
  border: 3px solid #111;
  padding: 14px;
  border-radius: 14px;
  background: #f5f5f5;
}

.inner {
  border: 2px dashed #111;
  padding: 12px;
  border-radius: 12px;
  background: #fff;
}

p {
  margin: 0;
  padding: 10px 12px;
  border-radius: 10px;
  background: #eaeaea;
}

p + p {
  margin-top: 10px;
}

I am a direct child paragraph.

I am a nested paragraph (NOT a direct child of .box).

Click the snippets:

  • .box > p outlines only the direct child paragraph.
  • .box p outlines both paragraphs (direct child + nested descendant).

Direct child vs descendant

The difference is one character:

  • Descendant selector: .parent .child (space) means “anywhere inside” (child, grandchild, etc.).
  • Direct child selector: .parent > .child means “immediate child only”.

The child combinator is often more precise (and helps prevent styling stuff you didn’t mean to touch).

Why this matters in real projects

Real HTML grows extra wrappers:

  • CMS output
  • component wrappers
  • utility divs
  • “I’ll clean this later” containers (we’ve all been there)

Using > lets you style structure without accidentally styling things deeper in the tree.

CSS all direct child selector: > *

Sometimes you don’t care what the children are—you just want to style every direct child:

.parent > * matches all direct children of .parent.

.grid > * {
  border: 2px solid #111;
  padding: 12px;
  border-radius: 12px;
  background: #fff;
}
.grid * {
  border: 2px solid #111;
  padding: 12px;
  border-radius: 12px;
  background: #fff;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  border: 3px solid #111;
  border-radius: 16px;
  padding: 12px;
  background: #f2f2f2;
}

.card {
  display: grid;
  gap: 10px;
}

.card h4 {
  margin: 0;
  font-size: 16px;
}

.card p {
  margin: 0;
  opacity: 0.8;
}

.nested {
  display: grid;
  gap: 8px;
  border: 2px dashed #111;
  padding: 10px;
  border-radius: 12px;
  background: #fafafa;
}

Card A

Direct child of .grid

Card B

Direct child of .grid

Nested item 1
Nested item 2

Card C

Direct child of .grid

Try both snippets:

  • .grid > * styles only the three cards (direct children).
  • .grid * styles the cards and everything inside them (often too much).

Learn more about * in the CSS Universal Selector (*) Interactive Tutorial.

CSS first direct child selector: > :first-child

Want to style only the first direct child? Combine the child combinator with :first-child:

  • .parent > :first-child means “the first element child of .parent”.

The :first-child pseudo-class matches the first element among siblings.

.menu > :first-child {
  background: #111;
  color: #fff;
  transform: translateY(-2px);
}
.menu > li:first-child {
  background: #111;
  color: #fff;
  transform: translateY(-2px);
}
.menu :first-child {
  background: #111;
  color: #fff;
  transform: translateY(-2px);
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
}

.menu {
  list-style: none;
  margin: 0;
  padding: 10px;
  display: flex;
  gap: 10px;
  border: 3px solid #111;
  border-radius: 999px;
  background: #f2f2f2;
}

.menu li {
  padding: 10px 14px;
  border-radius: 999px;
  background: #fff;
  border: 2px solid #111;
}

.menu li span {
  display: inline-block;
  padding: 6px 10px;
  border-radius: 999px;
  border: 2px dashed #111;
  background: #fafafa;
}

What to notice:

  • .menu > :first-child targets the first direct child element of .menu (the first li).
  • .menu > li:first-child is just a more explicit version (useful for readability).
  • .menu :first-child (descendant) can accidentally hit nested first children too (like the span).

Learn more in the CSS First Child Interactive Tutorial.

Other direct child variants you’ll love

  • .parent > :last-child for the last direct child
  • .parent > :nth-child(2) for the second direct child
  • .parent > :not(:first-child) for “every direct child except the first”
.stack > :not(:first-child) {
  margin-top: 10px;
}
.stack > :nth-child(2) {
  outline: 3px solid #111;
}
.stack > :last-child {
  background: #111;
  color: #fff;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
}

.stack {
  border: 3px solid #111;
  border-radius: 16px;
  padding: 12px;
  background: #f2f2f2;
  max-width: 420px;
}

.block {
  padding: 12px;
  border-radius: 12px;
  border: 2px solid #111;
  background: #fff;
}
First
Second
Third

Learn more in the CSS Nth Child Interactive Tutorial and the CSS :not Interactive Tutorial.

CSS only direct child selector: :only-child

:only-child matches an element that has no element siblings. In other words: it’s the one-and-only child element inside its parent.

Fun fact: :only-child is equivalent to :first-child:last-child (but with lower specificity).

:only-child is about siblings, not depth

:only-child doesn’t mean “only child in the whole subtree”. It means “only child among siblings under the same parent”.

.panel :only-child {
  outline: 3px solid #111;
  background: #fff;
}
.panel > :only-child {
  outline: 3px solid #111;
  background: #fff;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
  display: grid;
  gap: 16px;
}

.panel {
  border: 3px solid #111;
  border-radius: 16px;
  padding: 12px;
  background: #f2f2f2;
  display: grid;
  gap: 10px;
}

.card {
  border: 2px solid #111;
  border-radius: 12px;
  padding: 12px;
  background: #eaeaea;
}

.inner {
  border: 2px dashed #111;
  border-radius: 12px;
  padding: 10px;
  background: #fafafa;
}
I am the only direct child.
I have a sibling.
I am that sibling.
I am nested and I am an only-child inside .card.
Sibling card

Why the two snippets matter:

  • .panel :only-child can match nested “only children” deep inside (sometimes surprising).
  • .panel > :only-child restricts it to only direct children of .panel.

CSS :has direct child selector: :has(> selector)

Normally, CSS selectors flow “down” the tree (parent → child). :has() flips that: it lets you select an element based on what it contains.

The direct-child version looks like this:

  • .parent:has(> .badge) selects .parent elements that have a direct child matching .badge.

Why :has() is a big deal

It’s a way to do “parent styling” without JavaScript for lots of common UI patterns (cards with a certain child, form groups with an error element, nav items that contain a submenu, etc.).

Important compatibility note: if a browser doesn’t support :has(), the whole selector is invalid and won’t apply, unless you use it inside a forgiving selector list like :is() / :where().

Learn more in the CSS :is() and :where() Pseudo-Classes Interactive Tutorial.

.card:has(> .badge) {
  border-width: 5px;
  transform: translateY(-2px);
}
.card:has(.badge) {
  border-width: 5px;
  transform: translateY(-2px);
}
.card:has(> img) {
  border-style: dashed;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.card {
  border: 3px solid #111;
  border-radius: 16px;
  padding: 12px;
  background: #f2f2f2;
  display: grid;
  gap: 10px;
  transition: transform 150ms ease;
}

.card h4 {
  margin: 0;
  font-size: 16px;
}

.card p {
  margin: 0;
  opacity: 0.8;
}

.badge {
  justify-self: start;
  font-size: 12px;
  border: 2px solid #111;
  border-radius: 999px;
  padding: 4px 10px;
  background: #fff;
}

.thumb {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 12px;
  border: 2px solid #111;
  overflow: hidden;
  background: #fff;
}

.thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

Plain card

No badge. No image.

Featured

Card with badge

The badge is a direct child.

Random

Card with image

The image is nested inside .thumb.

What to notice:

  • .card:has(> .badge) matches only the card where .badge is a direct child.
  • .card:has(.badge) matches cards that contain a badge anywhere inside (direct or nested).
  • .card:has(> img) does not match the image card, because the img is not a direct child of .card.

Browser support for :has()

:has() is supported in modern Chrome/Edge (105+) and Safari (15.4+), but not supported in older browser versions. Always check current support before relying on it for critical UI.

Helpful link: Can I use: :has()

Learn more in the CSS :has Pseudo-Class Interactive Tutorial.

Practical patterns you’ll use everywhere

Pattern: safe spacing between direct children

A classic UI problem: “Add spacing between items, but don’t affect nested layout inside items.” This is where direct-child selectors shine.

.stack > * + * {
  margin-top: 12px;
}
.stack * + * {
  margin-top: 12px;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
}

.stack {
  border: 3px solid #111;
  border-radius: 16px;
  padding: 12px;
  background: #f2f2f2;
  max-width: 520px;
}

.item {
  border: 2px solid #111;
  border-radius: 12px;
  padding: 12px;
  background: #fff;
  display: grid;
  gap: 10px;
}

.item h4 {
  margin: 0;
  font-size: 16px;
}

.item p {
  margin: 0;
  opacity: 0.8;
}

.pills {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.pills span {
  border: 2px dashed #111;
  border-radius: 999px;
  padding: 4px 10px;
  background: #fafafa;
}

Item A

Has nested pill elements.

One Two Three

Item B

Another item in the stack.

Alpha Beta

Item C

Last one.

The difference:

  • .stack > * + * adds spacing only between direct children of .stack (the items).
  • .stack * + * adds spacing between everything inside (including pills), which is usually… chaos.

If you have nested menus, this is a clean way to style the top-level items only:

.nav > a {
  background: #111;
  color: #fff;
}
.nav a {
  background: #111;
  color: #fff;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
}

.nav {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  border: 3px solid #111;
  border-radius: 16px;
  padding: 12px;
  background: #f2f2f2;
}

.nav a {
  text-decoration: none;
  border: 2px solid #111;
  border-radius: 999px;
  padding: 10px 14px;
  background: #fff;
  color: #111;
  display: inline-block;
}

.dropdown {
  border: 2px dashed #111;
  border-radius: 16px;
  padding: 10px;
  background: #fafafa;
  display: grid;
  gap: 10px;
}

CSS direct child selector not working

If > “does nothing”, it’s almost always one of these:

1) You’re targeting a grandchild, not a child

This is the #1 mistake: you wrote .parent > .thing, but .thing is actually nested one level deeper.

.panel > .title {
  outline: 3px solid #111;
}
.panel .title {
  outline: 3px solid #111;
}
.panel > .header > .title {
  outline: 3px solid #111;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
}

.panel {
  border: 3px solid #111;
  border-radius: 16px;
  padding: 12px;
  background: #f2f2f2;
  max-width: 520px;
}

.header {
  border: 2px dashed #111;
  border-radius: 12px;
  padding: 10px;
  background: #fafafa;
}

.title {
  margin: 0;
  padding: 10px 12px;
  border-radius: 12px;
  background: #fff;
  border: 2px solid #111;
  font-size: 16px;
}

Hello from inside .header

Fixes:

  • If you truly want “any depth”, use the descendant selector: .panel .title.
  • If you want a specific structure, include the intermediate child: .panel > .header > .title.

2) There is a wrapper element you forgot about

Frameworks and CMS templates often insert wrappers. Your “expected child” is no longer direct.

  • Quick check: open DevTools, inspect the element, and look at its parent. Is it really the parent you’re selecting?

3) You’re matching text nodes in your head

CSS selectors match elements. Newlines and spaces in HTML create text nodes, but they don’t affect :first-child / :only-child in the way beginners fear. What matters is: element siblings.

4) Specificity or order is overriding you

Sometimes the selector matches, but another rule wins. Try temporarily adding outline: 3px solid red; to confirm matching, then resolve conflicts by:

  • making your selector slightly more specific (carefully)
  • moving the rule later in the stylesheet
  • removing conflicting rules

Learn more in the CSS Specificity Interactive Tutorial.

5) Your :has() selector isn’t supported in that browser

If you’re using :has() and nothing happens, check support and test in multiple browsers. Also remember: unsupported :has() makes the whole selector invalid unless used in a forgiving selector list.

Direct child selector cheat sheet

  • Direct child: .parent > .child
  • All direct children: .parent > *
  • First direct child: .parent > :first-child
  • Last direct child: .parent > :last-child
  • Nth direct child: .parent > :nth-child(3)
  • All but first direct child: .parent > :not(:first-child)
  • Only child (optionally direct): .parent > :only-child
  • Parent that has a direct child: .parent:has(> .thing)

Best practices

  • Use > when you want styles to apply to a component’s immediate layout children (cards, list items, columns).
  • Prefer .stack > * + * for spacing between items. It’s simple, readable, and doesn’t mess with nested content.
  • Avoid overusing * deep in the tree (like .component * { ... }) unless you truly want to style everything inside.
  • Treat :has() as a powerful modern feature: awesome when supported, but worth a quick compatibility check for production-critical UI.

CSS Direct Child Selector Conclusion

The child combinator (>) is a simple but essential tool for writing precise CSS. It helps you target only the elements you intend, without accidentally styling deeper descendants. Whether you’re spacing out items in a list, styling top-level links in a nav, or using :has() to conditionally style parents, understanding direct child selectors will make your CSS cleaner and more maintainable.