jQuery Selectors Explained: ID, Class, Attribute, and Pseudo-Selectors

How I pick jQuery selectors on real pages: ID, class, attribute and pseudo-selectors, form filters, the zero-based :even trap, and escaping dots in IDs, with output from jQuery 1.8.3.

Executive Summary: Most selector bugs I fix come from five rules. An empty result is still truthy, jQuery-only filters run slower than plain CSS, :even counts from zero, :checked also matches the selected option, and a dot in an ID needs escaping. I show each rule with real output from jQuery 1.8.3, then share how I keep selectors fast on large pages.
jQuery selectors illustration: a magnifying glass over a web page wireframe picks out teal, indigo and amber elements linked to small tag markers

I hear the same question every few weeks: “I wrote $('.something') and nothing happened. Is my selector wrong?” Usually the selector is fine and the page is not what the developer expects. Sometimes, though, it really is a rule of jQuery selectors that nobody told them about.

jQuery selectors are CSS selectors plus a set of jQuery-only extensions. The Sizzle engine inside jQuery evaluates them. The official selector reference lists every one. Here I stick to the questions that come up when I build real pages. How do I find an element? How do I narrow the result? And why did I get zero matches?

The test page behind these jQuery selectors examples

I ran every example against jQuery 1.8.3, the current release as I write this. The fixture is a small page with a menu, two headings, a sign-up form, a table and a few paragraphs. Each output block is the real result of that run:

<div id="wrapper" class="page">
  <ul id="menu" class="nav main">
    <li class="item active"><a href="/home" title="Home page">Home</a></li>
    <li class="item"><a href="/blog/jquery-tips" rel="nofollow" target="_blank">Blog</a></li>
    <li class="item disabled"><a href="http://example.com/about.pdf" data-type="pdf">About</a></li>
    <li class="item"><a href="mailto:me@example.com">Contact</a></li>
    <li class="item"><a href="#top">Top</a></li>
  </ul>
  <h1>Title</h1><h2>Sub</h2>
  <form id="signup" action="/signup">
    <input type="text" name="user[name]" value="Ana">
    <input type="text" name="email" value="">
    <input type="password" name="pwd">
    <input type="checkbox" name="agree" checked>
    <input type="checkbox" name="news">
    <input type="radio" name="plan" value="a" checked><input type="radio" name="plan" value="b">
    <select id="country"><option value="in" selected>India</option><option value="us">USA</option></select>
    <textarea name="note">Hi</textarea>
    <input type="text" name="locked" disabled>
    <button type="submit">Send</button>
  </form>
  <table id="grid"><tr><td>1</td><td>2</td></tr><tr><td>3</td><td>4</td></tr><tr><td>5</td><td></td></tr></table>
  <p class="note">First <em>note</em></p><p class="note empty-one"></p><p class="note">Third note</p>
  <div id="box" style="display:none"><span class="note">hidden note</span></div>
</div>

My position is simple. Learn the standard CSS selectors well, and reach for the jQuery-only ones on purpose. The reason is speed. jQuery can hand a standard selector to the browser’s fast querySelectorAll. It cannot do that for :first, :contains or :eq(), so it falls back to its own slower matching. Know which side of that line each of your jQuery selectors sits on. Your pages get faster and your bugs get easier to find. Many of the jQuery-only filters look like the CSS pseudo-classes you already use, which is exactly why they confuse people.

Basic jQuery selectors: tag, ID, class and combinations

console.log('#menu          ', $('#menu').length);
console.log('.item          ', $('.item').length);
console.log('li.item.active ', $('li.item.active').length);
console.log('ul#menu .item  ', $('ul#menu .item').length);
console.log('a              ', $('a').length);
console.log('li, h1, h2     ', $('li, h1, h2').length);
console.log('#menu > li     ', $('#menu > li').length);
console.log('h1 + h2        ', $('h1 + h2').length);
console.log('h1 ~ p         ', $('h1 ~ p').length);
console.log('* (all)        ', $('*').length);
console.log('#missing       ', $('#missing').length, '| truthy?', !!$('#missing'));
#menu           1
.item           5
li.item.active  1
ul#menu .item   5
a               5
li, h1, h2      7
#menu > li      5
h1 + h2         1
h1 ~ p          3
* (all)         48
#missing        0 | truthy? true

Each line follows a CSS rule you already know. #menu matches by ID and finds one element. It is the fastest selector there is, because jQuery turns it into a document.getElementById call. .item matches by class, and li.item.active requires all three conditions on the same element. ul#menu .item is a descendant selector at any depth. In contrast, #menu > li only matches direct children. h1 + h2 is the adjacent sibling, and h1 ~ p is every following sibling.

The last line is the one that costs people an afternoon. $('#missing') matched nothing, yet it is truthy. jQuery always returns an object, even an empty one. So if ($('#missing')) is always true. The correct test is if ($('#missing').length). jQuery also stays quiet about misses. Calling .hide() or .addClass() on an empty set does nothing. That is a feature until you need to know it did nothing.

jQuery attribute selectors

console.log('[href]                ', $('a[href]').length);
console.log('[href="/home"]        ', $('a[href="/home"]').length);
console.log('[href^="http"]  begins', $('a[href^="http"]').length);
console.log('[href$=".pdf"]  ends  ', $('a[href$=".pdf"]').length);
console.log('[href*="jquery"] has  ', $('a[href*="jquery"]').length);
console.log('[rel~="nofollow"]     ', $('a[rel~="nofollow"]').length);
console.log('[href!="/home"]       ', $('a[href!="/home"]').length);
console.log('[data-type="pdf"]     ', $('a[data-type="pdf"]').length);
console.log('[target][rel]         ', $('a[target][rel]').length);
console.log('[name="user[name]"]   ', $('input[name="user[name]"]').length);
try { console.log($('input[name=user[name]]').length); } catch (e) { console.log('unquoted brackets ->', e.message); }
[href]                 5
[href="/home"]         1
[href^="http"]  begins 1
[href$=".pdf"]  ends   1
[href*="jquery"] has   1
[rel~="nofollow"]      1
[href!="/home"]        4
[data-type="pdf"]      1
[target][rel]          1
[name="user[name]"]    1
unquoted brackets -> Syntax error, unrecognized expression: input[name=user[name]]
Selector Matches when the attribute Typical use
[href] exists Links that have a destination
[href="/home"] equals the value exactly One specific link
[href^="http"] starts with the value External links
[href$=".pdf"] ends with the value Mark PDF downloads
[href*="jquery"] contains the value anywhere Rough URL matching
[rel~="nofollow"] contains the value as a whole word Space-separated lists such as rel
[href!="/home"] does not equal it (jQuery only) Same as :not([href="/home"])
[target][rel] both exist Several attribute conditions at once

Two details in the output deserve attention. First, [href!="/home"] is a jQuery extension. It also matches elements that have no href at all. The portable spelling is :not([href="/home"]), which behaves the same and stays inside standard CSS. Second, the last line is a real error. When the attribute value contains brackets, you must quote it. So input[name="user[name]"] works, while input[name=user[name]] throws a syntax error. PHP-style field names such as user[name] are the usual victim.

Pseudo-selectors: the position filters that surprise people

console.log('li:first           ', $('li:first').text());
console.log('li:last            ', $('li:last').text());
console.log('li:eq(2)           ', $('li:eq(2)').text());
console.log('li:gt(2)           ', $('li:gt(2)').length, ' li:lt(2)', $('li:lt(2)').length);
console.log('tr:even / tr:odd   ', $('tr:even').length, '/', $('tr:odd').length);
console.log('li:first-child     ', $('li:first-child').text());
console.log('li:nth-child(2)    ', $('li:nth-child(2)').text());
console.log('li:nth-child(odd)  ', $('li:nth-child(odd)').length);
console.log('li:not(.active)    ', $('li:not(.active)').length);
console.log('ul:has(.disabled)  ', $('ul:has(.disabled)').length);
console.log('p:contains(note)   ', $('p:contains("note")').length);
console.log('p:contains(Note) capital N', $('p:contains("Note")').length);
console.log('p:empty            ', $('p:empty').length, ' td:empty', $('td:empty').length);
console.log('p:parent           ', $('p:parent').length);
console.log(':header            ', $(':header').length);
li:first            Home
li:last             Top
li:eq(2)            About
li:gt(2)            2  li:lt(2) 2
tr:even / tr:odd    2 / 1
li:first-child      Home
li:nth-child(2)     Blog
li:nth-child(odd)   3
li:not(.active)     4
ul:has(.disabled)   1
p:contains(note)    2
p:contains(Note) capital N 0
p:empty             1  td:empty 1
p:parent            2
:header             2

Read the output against the fixture, because three of these lines contradict intuition:

  • tr:even returned 2 and tr:odd returned 1. The table has three rows. :even, :odd, :eq(), :gt() and :lt() all count from zero. So :even picks the first and third rows. However, :nth-child(odd) counts from one, as CSS does, and the two disagree on the same table. If your zebra stripes come out backwards, this is why.
  • li:first versus li:first-child. They return the same item here by coincidence. :first means “the first element in the whole result set”. :first-child means “every element that is the first child of its parent”. On a page with three lists, the first returns one element and the second returns three.
  • p:contains("Note") returned 0 while p:contains("note") returned 2. :contains is case-sensitive. It also looks at text anywhere inside the element, including child elements. That is why the paragraph with <em>note</em> matched.

Watch :empty as well. It means no children and no text, so a paragraph holding a single space is not empty. :parent is its opposite. Finally, :has(.disabled) asks “does this element contain a match?” It reverses a descendant selector, and I use it often.

Form selectors

console.log(':input    ', $('#signup :input').length, '(input, select, textarea, button)');
console.log('input     ', $('#signup input').length);
console.log(':text     ', $('#signup :text').length);
console.log(':checkbox ', $('#signup :checkbox').length);
console.log(':checked  ', $('#signup :checked').length, '(checkbox + radio + the selected option)');
console.log(':selected ', $('#signup :selected').val());
console.log(':disabled ', $('#signup :disabled').attr('name'));
console.log(':enabled  ', $('#signup input:enabled').length);
console.log(':submit   ', $('#signup :submit').length);
console.log('radio checked value', $('input[name="plan"]:checked').val());
console.log('select val ', $('#country').val());
:input     11 (input, select, textarea, button)
input      8
:text      3
:checkbox  2
:checked   3 (checkbox + radio + the selected option)
:selected  in
:disabled  locked
:enabled   7
:submit    1
radio checked value a
select val  in

The form filters save typing, but each one has a catch. :input is broader than input. It also matches select, textarea and button, so it returned 11 against 8. Next, :checked returned 3 from a form with one checked checkbox and one checked radio. It also counts the selected <option>. If you want only checkboxes, write :checkbox:checked. For a radio group, I use $('input[name="plan"]:checked').val(), which returned a above. For a drop-down, skip :selected entirely, because $('#country').val() returns the chosen value directly.

Narrowing and traversing instead of writing longer selectors

var $menu = $('#menu');
console.log('$(".item", $menu)       ', $('.item', $menu).length);
console.log('$menu.find(".item")      ', $menu.find('.item').length);
console.log('$menu.children()         ', $menu.children().length);
console.log('$menu.children(".active")', $menu.children('.active').length);
console.log('.active.siblings()       ', $('.active').siblings().length);
console.log('.active.next().text()    ', $('.active').next().text());
console.log('a:first.parent()         ', $('a:first').parent().attr('class'));
console.log('a:first.closest("ul")    ', $('a:first').closest('ul').attr('id'));
console.log('.item.filter(".disabled")', $('.item').filter('.disabled').length);
console.log('.item.not(".disabled")   ', $('.item').not('.disabled').length);
console.log('.item.has("a[rel]")      ', $('.item').has('a[rel]').length);
console.log('$("p.note").eq(1).is(":empty")', $('p.note').eq(1).is(':empty'));
console.log('.note total vs inside #box', $('.note').length, $('#box .note').length);
$(".item", $menu)        5
$menu.find(".item")       5
$menu.children()          5
$menu.children(".active") 1
.active.siblings()        4
.active.next().text()     Blog
a:first.parent()          item active
a:first.closest("ul")     menu
.item.filter(".disabled") 1
.item.not(".disabled")    4
.item.has("a[rel]")       1
$("p.note").eq(1).is(":empty") true
.note total vs inside #box 4 1

The first two lines return the same five elements. They are two spellings of one operation, because jQuery turns $('.item', $menu) into $menu.find('.item'). I prefer the explicit .find(), since it reads left to right. The traversal methods then replace pseudo-selectors with something faster and clearer:

  • .filter() keeps matches, and .not() removes them.
  • .has() keeps elements that contain a match.
  • .eq(n) picks by position.
  • .closest() walks up the tree to the nearest matching ancestor.

The last line shows why scoping matters. .note matched four elements on the page but only one inside #box.

Special characters in IDs and classes

$('<div id="a.b" class="x:y"></div>').appendTo('body');
console.log('$("#a.b")            ', $('#a.b').length, '(looks for id="a" with class "b")');
console.log('$("#a\\\\.b")          ', $('#a\\.b').length);
console.log('$("[id=\\"a.b\\"]")     ', $('[id="a.b"]').length);
console.log('$(".x\\\\:y")          ', $('.x\\:y').length);
console.log('document.getElementById', $(document.getElementById('a.b')).length);
$("#a.b")             0 (looks for id="a" with class "b")
$("#a\\.b")           1
$("[id=\"a.b\"]")      1
$(".x\\:y")           1
document.getElementById 1

An element with id="a.b" is valid HTML. However, $('#a.b') means “the element with ID a and class b“, so it found nothing. You have three fixes. Escape the dot with a double backslash: one for the JavaScript string, one for the selector. Use the attribute form [id="a.b"]. Or skip the selector and call document.getElementById. The same rule applies to colons, brackets and other punctuation. ASP.NET and JSF both generate IDs with colons or dollar signs, so I meet this on real projects. It trips up jQuery selectors more than any other rule.

Visibility: :visible and :hidden

In jQuery 1.x an element counts as hidden when it takes up no space on the page. That covers display: none, zero width and height, a hidden ancestor, and type="hidden". It does not cover visibility: hidden, because that element still occupies space. These two filters need real layout data from the browser. As a result, they run slower than other selectors, and my headless test page cannot show them. Check them in a real browser. For a single element, call .is(':visible') instead of selecting a whole page of them.

Troubleshooting jQuery selectors

Symptom Likely cause Fix
$('.x').length is 0 but the element is in the HTML Script runs in the <head> before the element exists Wrap in $(document).ready(function () { … }) or move the script to the end of the body
Selector works in the console, not in the page Element is added later by AJAX or script Select after it is inserted, or use event delegation for events
“Syntax error, unrecognized expression” Unquoted attribute value with brackets, or an unescaped special character Quote the value; escape with \\
Stripes are the wrong way round :even counts from zero Use :odd, or :nth-child(odd) for CSS-style counting
if ($('#x')) is always true jQuery returns an object even when empty Test .length
Same ID appears twice and only the first is found Duplicate IDs are invalid HTML Fix the markup, or use a class

How I keep jQuery selectors fast and maintainable

  • Start from an ID, then search inside. $('#menu').find('.item') gives jQuery a fast anchor and a small search area. A bare $('.item') scans the whole document. The same scoping keeps your inserts cheap when you later append, prepend or replace content with jQuery.
  • Cache the result. Using $('#menu') three times in one function? Store it once in a variable with a leading $ and reuse it.
  • Select by a hook class, not by structure. A selector like div > ul > li > a breaks the moment a designer wraps something in another div. A dedicated class such as js-toggle survives redesigns.
  • Keep jQuery-only selectors out of the first selector. Write $('#menu li').first() rather than $('#menu li:first'). The browser does the heavy work, and jQuery applies the extra step to a small set. Most slow jQuery selectors I review break this rule.

The slow filter panels I get asked to fix in old admin screens usually share one cause. It is a selector like $('tr:contains("pending")'), running on every keystroke across a table of several hundred rows. In Internet Explorer 8 that feels like typing through mud. The fix is to move the status into a class on each row and select $('#orders').find('tr.is-pending') instead. I keep text matching out of anything that runs often. And once I need to find a row twice, I give it a hook class.

How I choose a selector

  1. Does the element have an ID? Use it. Nothing is faster or less ambiguous.
  2. Can I express it as plain CSS? Use the CSS form, even when a jQuery shortcut exists.
  3. Do I need a jQuery-only filter? Apply it as a method (.first(), .eq(), .filter()) after a CSS selector.
  4. Am I searching the whole page? Scope it with .find() from a cached parent.
  5. Could the selector match zero elements? Decide whether that is fine, and test .length if it is not.

When NOT to reach for a selector

  • You already hold the element. Inside an event handler, $(this) is the element. Re-selecting it by class is slower and may match the wrong one. My notes on jQuery event handling and delegation cover this in detail.
  • You only need a form value. document.getElementById('country').value works in every browser and needs no library.
  • The page has thousands of rows. Selecting each cell one by one crawls in older browsers. Work on a detached fragment, or let the server do it.

Common jQuery selectors mistakes

  • Testing the result for truthiness. Use .length.
  • Using :eq() or :even and expecting CSS counting. They start at zero.
  • Selecting before the DOM is ready. The script found nothing, since nothing existed yet.
  • Writing $('#a.b') for an ID containing a dot. Escape it.
  • Using :input when you meant input. The first includes buttons and drop-downs.
  • Re-running the same selector in a loop. Cache it.

FAQ

What are jQuery selectors?

jQuery selectors are strings you pass to $() to describe which elements to find. They support the common CSS selectors, such as tag, ID, class, attribute, child and sibling. They also add jQuery extensions such as :first, :contains(), :has() and :checked. The result is a jQuery object that wraps the matches.

How do I select an element by ID or class in jQuery?

Use $('#myId') for an ID and $('.myClass') for a class. Combine them like CSS. $('li.item.active') requires all classes on one element, and $('#menu .item') finds items inside the menu. IDs are the fastest, because jQuery maps them to getElementById.

How do I check whether a jQuery selector found anything?

Test the length property: if ($('#x').length) { … }. Do not test the object itself, because an empty jQuery object is still truthy and the condition would always pass.

Why does :even select the first row in jQuery?

jQuery’s :even and :odd filters count from zero, like :eq(). So the first row is index 0 and counts as even. The CSS-style :nth-child(odd) counts from one, and the two give opposite results on the same table.

How do I select an ID that contains a dot or a colon?

Escape the character with two backslashes inside a JavaScript string, for example $('#a\\.b'). You can also use an attribute selector such as $('[id="a.b"]'). Without the escape, jQuery reads #a.b as ID a with class b.

jQuery selectors are the first thing I write on any page and often the first thing that goes wrong. A few minutes spent on the places where jQuery differs from CSS pays back on every later page. My habit is simple: anchor on an ID, scope with .find(), and keep jQuery-only filters out of the first selector.

Originally posted on 20 November 2012 and post last modified on 2 November 2026

Front-End Development JavaScript jQuery jQuery Selectors

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *