jQuery / Select Parents Without a Specific Child
To select parents without a matching descendant, exclude the candidates returned by .has(".badge") using .not(). To inspect only direct children, test $(this).children(".badge").length === 0 per parent. Lacking a particular child is different from being completely empty.
Define the absent target and depth
.has(".badge") retains candidate parents containing at least one matching descendant at any depth. Removing that collection with .not() leaves candidates without a badge anywhere inside.
.children(".badge") checks only one level. A nested badge passes “no direct badge” but fails “no badge descendant.” Hidden elements still exist in the DOM, Document Object Model, and count as present.
Parent here means a candidate container, not a call to .parent() moving upward. Start with a limited candidate set so unrelated ancestors and nested objects do not enter the result.
Complete depth and content example
Save everything as UTF-8 missing-child-demo.html and open it in a browser. Internet loads the official jQuery file 4.0.0. No server or installation is needed. Initial results, addition, movement and reset were executed in Windows Edge with 3.7.1 and 4.0.0.
a has a direct badge, b a nested badge, c another child type and d only text. e is empty, f comment-only, g one space and h a hidden badge. Names such as a: are CSS-generated, not DOM content. Changing internal whitespace in e, f or g can change :empty results.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Select containers without a badge</title>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<style>
body { font: 16px/1.6 sans-serif; margin: 20px; max-width: 760px; }
button { font: inherit; margin: 4px; }
.card { border: 2px solid #aaa; margin: 10px 0; padding: 10px; min-height: 28px; }
.card::before { content: attr(id) ": "; font-weight: bold; }
.card.chosen { border-color: teal; background: #e4f6f0; }
.badge { border: 1px solid #777; padding: 2px; }
.badge[hidden] { display: none; }
#result { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<p>Green cards have no .badge descendant. Names are CSS-generated labels.</p>
<button type="button" id="add">Add badge to c</button>
<button type="button" id="move">Move b badge to direct child</button>
<button type="button" id="reset">Reset</button>
<div id="cards">
<div class="card" id="a">Direct <span class="badge">A</span></div>
<div class="card" id="b"><div class="wrapper">Nested <span class="badge">B</span></div></div>
<div class="card" id="c"><span class="note">Other element</span></div>
<div class="card" id="d">Text only</div>
<div class="card" id="e"></div>
<div class="card" id="f"><!-- comment only --></div>
<div class="card" id="g"> </div>
<div class="card" id="h"><span class="badge" hidden>Hidden</span></div>
</div>
<pre id="result" aria-live="polite"></pre>
<script>
$(function () {
const initial = $("#cards").html();
function ids($set) { return $set.map(function () { return this.id; }).get().join(",") || "(none)"; }
function report() {
const $cards = $("#cards > .card");
const $withoutDescendant = $cards.not($cards.has(".badge"));
const $withoutDirect = $cards.filter(function () { return $(this).children(".badge").length === 0; });
const $empty = $cards.filter(":empty");
const $withoutElements = $cards.filter(function () { return $(this).children().length === 0; });
$cards.removeClass("chosen"); $withoutDescendant.addClass("chosen");
$("#result").text([
"no descendant badge=" + ids($withoutDescendant),
"no direct badge=" + ids($withoutDirect),
"empty=" + ids($empty),
"no element children=" + ids($withoutElements)
].join("\n"));
}
$("#add").on("click", function () {
$("<span>").addClass("badge").text("New").appendTo("#c");
$(this).prop("disabled", true); report();
});
$("#move").on("click", function () {
$("#b .badge").appendTo("#b"); $(this).prop("disabled", true); report();
});
$("#reset").on("click", function () {
$("#cards").html(initial); $("#add, #move").prop("disabled", false); report();
});
report();
});
</script>
</body>
</html>
Initial output follows. Green highlights apply only to the first set, no badge descendant. The other lines compare different conditions.
no descendant badge=c,d,e,f,g
no direct badge=b,c,d,e,f,g
empty=e,f
no element children=d,e,f,g
- c,d,e,f,g have no badge at any depth. c qualifies despite its note element.
- The second line includes b because its badge is inside a wrapper, not direct.
- Add badge to c changes the first line to d,e,f,g and the second to b,d,e,f,g.
- Move b badge to direct child makes the second line d,e,f,g too. The same node moves; descendant presence stays, direct relationship changes.
- Reset restores eight containers and their structure. h’s hidden badge still counts as present.
Do not confuse candidate exclusion and empty
Reset, then run this in developer tools. The first expression tests the candidates themselves; the second inspects their contents.
const $cards = $("#cards > .card");
console.log($cards.not(".badge").map(function () { return this.id; }).get().join(","));
console.log($cards.filter(function () {
return $(this).find(".badge").length === 0;
}).map(function () { return this.id; }).get().join(","));
None of the eight cards is itself a .badge, so the first result includes all. The second returns the desired c,d,e,f,g.
a,b,c,d,e,f,g,h
c,d,e,f,g
:empty does not mean absence of a specific type. It selects elements without element or text children. Here e and comment-only f qualify. Comments do not prevent the match, but g’s whitespace text does. d’s text also prevents it.
.children().length === 0 instead means no direct element children, giving d,e,f,g because text and whitespace do not count. CSS ::before labels are not DOM children and do not change e or f’s :empty state. Visible appearance and node structure are separate.
Updates and practical use
A stored result does not update itself after a badge is added. The example calls report() after addition and movement and queries current candidates. Connect reevaluation after other code changes the DOM too.
.not() and .filter() build selections; they do not delete other cards from the document. Apply a separate class or empty-state message as needed. This example removes old highlights before marking current matches.
Define the target tag or class, direct-child versus descendant depth, and whether hidden or unavailable elements count. Compare initial and updated structures rather than substituting similar-looking selectors blindly. Limit candidates to avoid unnecessary deep repeated searches.




