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 > .childmatches only.childelements 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 > poutlines only the direct child paragraph. -
.box poutlines 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 > .childmeans “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 1Nested item 2Card 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-childmeans “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-childtargets the first direct child element of.menu(the firstli). -
.menu > li:first-childis just a more explicit version (useful for readability). -
.menu :first-child(descendant) can accidentally hit nested first children too (like thespan).
Learn more in the CSS First Child Interactive Tutorial.
Other direct child variants you’ll love
.parent > :last-childfor 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;
}
FirstSecondThird
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-childcan match nested “only children” deep inside (sometimes surprising). -
.panel > :only-childrestricts 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.parentelements 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.
FeaturedCard with badge
The badge is a direct child.
![]()
Card with image
The image is nested inside .thumb.
What to notice:
-
.card:has(> .badge)matches only the card where.badgeis 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 theimgis 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 ThreeItem B
Another item in the stack.
Alpha BetaItem 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.
Pattern: style only immediate links in a nav
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.
