jQuery / Filter Parents by Child Count

Filter candidate parents with .filter(function () { ... }) and compare $(this).children(".item").length inside each callback. Use === 2 for exactly two or >= 2 for at least two. Replace children() with find() only when you intend to count all nested descendants.

Define what each parent counts

.filter() retains members of the original selection that pass a test. this is the parent DOM element under examination; returning true keeps that parent. The result is a parent collection, not a child collection, and does not remove other parents from the document.

.children(".item") counts matching elements one level below. Without the selector, headings and other direct element children count too. Neither form includes text or comment nodes.

.find(".item") includes deeper descendants. Beta has one direct item containing two more, so its descendant count is three. Hidden elements still exist in the DOM and count; visible-only requirements need an additional explicit rule.

Complete exact and minimum-count example

Save everything as UTF-8 child-count-demo.html and open it in a browser. Internet loads the official jQuery file 4.0.0. No server or installation is needed. Conditions, reset and dynamic additions were executed in Windows Edge with 3.7.1 and 4.0.0.

Direct counts are Alpha 2, Beta 1, Gamma 0 and Delta 3. Delta’s hidden D3 counts. Headings and plain text are not .item. Candidates are limited to #board’s direct .group children so nested objects are not accidentally treated as parents.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Filter parents by child count</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; }
    label { display: block; margin: 8px 0; }
    select, input, button { font: inherit; max-width: 100%; }
    .group { border: 2px solid #aaa; padding: 12px; margin: 12px 0; }
    .group.chosen { border-color: teal; background: #e5f5f1; }
    .item { display: inline-block; padding: 4px; border: 1px solid #777; margin: 3px; }
    .item[hidden] { display: none; }
    .group h2 { font-size: 20px; margin: 0; }
    #result { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <label>Count scope <select id="scope"><option value="direct">Direct children</option><option value="descendant">All descendants</option></select></label>
  <label>Rule <select id="rule"><option value="exact">Exactly</option><option value="minimum">At least</option></select></label>
  <label>Required count <input id="count" type="number" min="0" step="1" value="2"></label>
  <button type="button" id="add">Add direct item to Alpha</button>
  <button type="button" id="reset">Reset</button>
  <div id="board">
    <section class="group" id="alpha"><h2>Alpha</h2><span class="item">A1</span><span class="item">A2</span> plain text <!-- comment --></section>
    <section class="group" id="beta"><h2>Beta</h2><div class="item">B1 <span class="item">B2</span><span class="item">B3</span></div></section>
    <section class="group" id="gamma"><h2>Gamma</h2>Text without an item</section>
    <section class="group" id="delta"><h2>Delta</h2><span class="item">D1</span><span class="item">D2</span><span class="item" hidden>D3 hidden</span></section>
  </div>
  <pre id="result" aria-live="polite"></pre>
  <script>
    $(function () {
      const initial = $("#board").html();
      let added = 0;
      function apply() {
        const $groups = $("#board > .group");
        const scope = $("#scope").val(), rule = $("#rule").val();
        const field = document.getElementById("count"), target = Number(field.value);
        $groups.removeClass("chosen");
        if (field.value === "" || !field.checkValidity() || !Number.isInteger(target) || target < 0) {
          $("#result").text("Enter a nonnegative integer."); return;
        }
        function count(group) {
          return scope === "direct" ? $(group).children(".item").length : $(group).find(".item").length;
        }
        const $selected = $groups.filter(function () {
          const n = count(this);
          return rule === "exact" ? n === target : n >= target;
        });
        $selected.addClass("chosen");
        const ids = $selected.map(function () { return this.id; }).get();
        const counts = $groups.map(function () { return this.id + "=" + count(this); }).get();
        $("#result").text(`scope=${scope}; rule=${rule}; count=${target}\nselected=${ids.join(",") || "(none)"}\n${counts.join("; ")}`);
      }
      $("#scope, #rule, #count").on("change input", apply);
      $("#add").on("click", function () {
        added++; $("<span>").addClass("item").text("Added " + added).appendTo("#alpha"); apply();
      });
      $("#reset").on("click", function () {
        $("#board").html(initial); added = 0;
        $("#scope").val("direct"); $("#rule").val("exact"); $("#count").val(2); apply();
      });
      apply();
    });
  </script>
</body>
</html>

Initially only Alpha, with exactly two direct items, is highlighted.

scope=direct; rule=exact; count=2
selected=alpha
alpha=2; beta=1; gamma=0; delta=3
  1. Choose At least: Alpha and Delta match. Exactly two and at least two are different conditions.
  2. Choose All descendants: Beta also matches because its count changes from one to three.
  3. Exactly 3 selects Beta and Delta under descendants; switching to Direct children leaves only Delta.
  4. Exactly 0 selects Gamma. At least 0 permits zero and selects all four candidates.
  5. After Reset, Add direct item to Alpha makes Alpha three; exactly two then gives (none). No parent was deleted.
  6. Blank, negative and fractional inputs show an error and clear highlights. Reset removes additions and restores the original structure and conditions.

Combined total versus per-parent test

Reset first, then run this in developer tools to compare a combined child collection with individual filtering:

const $groups = $("#board > .group");
console.log($groups.children(".item").length);
console.log($groups.filter(function () {
  return $(this).children(".item").length === 0;
}).map(function () { return this.id; }).get().join(","));

Six is the combined 2+1+0+3, not information about which parent has two. The second line tests each parent and returns Gamma, which has no direct .item.

6
gamma

A normal function supplies this as the current parent. Replacing it with an arrow while keeping the same this expression breaks that assumption. With an arrow, receive the callback’s second element argument and inspect it explicitly.

A stored filtered collection does not continually reevaluate its condition. After adding to Alpha, an old result may still contain Alpha. The example calls apply() after adding and queries current candidates and counts again. .filter() is not a DOM-change observer.

Scope and common mistakes

Decide whether children means every element or a specific card, li or control. Here only .item counts, so Gamma is zero despite its h2 and text. It is not an empty document container. Explicitly add rules for visibility, availability or data state if the task requires them.

If only the existence of any matching descendant matters, .has() may express it more simply. Exact or minimum quantities are clear as numeric comparisons. Do not confuse nth-child sibling position with a parent’s total child count.

With thousands of parents and deep repeated searches, narrow candidates and avoid unnecessary recomputation. Conversely, do not reuse an outdated early result after structural changes. Check both the initial markup and the post-update state.