:empty - Hide Containers That Have Nothing to Say
Server-rendered templates leak empty elements: the errors div that's always in the markup, the badge span waiting for a count, the "related posts" section when there are none. Each renders as a ghost - a bordered nothing, a padded gap, a red dot with no number. CSS can see emptiness:
.badge:empty,
.alert:empty,
.tag-list:empty {
display: none;
}
The badge is my favorite version: render <span class="badge"><?= $count ?: '' ?></span> unconditionally, and CSS hides it when the count is empty. The template loses its if-statement; presence logic becomes presentation logic, where this particular decision belongs.
The rule that trips everyone: whitespace counts
<div class="alert"></div> <!-- :empty matches -->
<div class="alert"> </div> <!-- does NOT match - a text node of one space -->
<div class="alert">
</div> <!-- does NOT match - newline + indentation -->
<div class="alert"><!-- note --></div> <!-- matches - comments don't count -->
An element is :empty only with zero child nodes (comments excepted). Template engines love emitting whitespace inside "empty" tags, which is why this selector has a reputation for not working. Fixes: emit truly empty tags (<div class="alert"><?php ... ?></div> with no gaps), or trim in the template. There's no :blank selector shipping to save you - control your whitespace.
Empty-state messaging with ::before
.comment-list:empty::before {
content: 'No comments yet - be the first!';
display: block;
padding: 2rem;
text-align: center;
color: #94a3b8;
}
An empty container announces itself. For AJAX-driven lists this is delightfully robust: JS clears the list, the message appears; JS appends rows, it vanishes - no state flags. (Screen-reader note: generated content is announced by modern AT, but for important empty states a real element toggled by :has() is more dependable.)
The grown-up sibling: :has() empty-state layouts
/* Style the whole panel differently when its list is empty */
.sidebar:has(.notification-list:empty) {
display: none;
}
/* Or reserve the ghost only when content exists */
.results-panel:not(:has(.result)) { border: 0; padding: 0; }
:has() lets emptiness propagate upward - hide the section heading and wrapper when the list inside has nothing, which pure :empty (matching only the element itself) can't reach.
Support: :empty since the dawn of CSS3; the :has() combos since 2023. Small selector, but it moves a whole category of "should this render?" checks out of templates - and templates with fewer conditionals are templates with fewer bugs.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.