CSS selectors are the patterns you write to target HTML elements and apply styles to them. Knowing your selectors well means writing less CSS, avoiding unnecessary classes, and targeting elements with surgical precision. This guide covers every selector type, from the basics to combinators, attribute selectors, pseudo-classes, and pseudo-elements, with practical examples for each.
Types of CSS selectors at a glance
CSS selectors fall into five broad categories. Every selector you'll ever write belongs to one of these, or combines two or more of them together.
| Category | Example | Selects |
|---|---|---|
| Basic (simple) | p, .card, #nav, * | Elements by tag, class, ID, or all elements |
| Combinators | .nav a, ul > li, h2 + p, h2 ~ p | Elements by their relationship in the HTML |
| Attribute | [disabled], [type="text"], [href^="https"] | Elements by presence or value of an attribute |
| Pseudo-classes | :hover, :nth-child(), :not(), :has() | Elements by state, position, or user interaction |
| Pseudo-elements | ::before, ::first-letter, ::placeholder | A specific part of an element or generated content |
Type, class, ID, universal, and attribute selectors are collectively called simple selectors in the CSS specification. Chain two or more simple selectors together with no space between them (like a.btn.btn-primary or #featured.card) and you get a compound selector, which still targets a single element but narrows the match. Add a combinator (space, >, +, ~) to link compound selectors together and you get a complex selector, which matches based on relationships between elements.
/* Compound selector: element must be both an anchor AND have both classes */ a.btn.btn-primary { font-weight: 600; } /* Compound selector: element with ID "featured" that also has class "card" */ #featured.card { border-color: #7c6af7; } /* Complex selector: a compound selector combined via a combinator */ .sidebar > .card.featured { box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
The basic selectors
Type selector
Targets every element of a given HTML tag. The broadest selector: use it for base styles and resets.
/* Targets every paragraph */ p { line-height: 1.6; } /* Targets every heading */ h2 { font-size: 1.5rem; }
Class selector
Targets elements with a specific class attribute. The most commonly used selector in day-to-day CSS, reusable across multiple elements.
.card { border-radius: 8px; padding: 1.5rem; } /* Multiple classes on one element */ .btn.btn-primary { background: #3b82f6; }
ID selector
Targets a single element with a specific ID. IDs must be unique per page. The ID selector has very high specificity: it overrides class and type selectors. Avoid using ID selectors for styling in most cases; reserve IDs for JavaScript hooks and anchor links.
#main-nav { position: sticky; top: 0; }
Universal selector
Targets every element on the page. Written as an asterisk (*) and sometimes called the wildcard selector. Most commonly used in CSS resets and for applying box-sizing globally.
/* Apply box-sizing to every element */ *, *::before, *::after { box-sizing: border-box; }
Combinators: selecting by relationship
Combinators define the relationship between two selectors. They let you target elements based on where they sit in the HTML structure.
Descendant combinator (space)
Targets an element that is anywhere inside another element, not necessarily a direct child.
/* Any <a> inside .nav, at any depth */ .nav a { text-decoration: none; color: inherit; }
Child combinator (>)
Targets an element that is a direct child of another, one level deep only.
/* Only direct <li> children of .menu, not nested ones */ .menu > li { display: inline-block; }
Adjacent sibling combinator (+)
Targets an element that immediately follows a specified element, sharing the same parent.
/* <p> that comes immediately after an <h2> */ h2 + p { margin-top: 0.5rem; font-size: 1.1rem; } /* Remove top margin from label immediately after a checkbox */ input[type="checkbox"] + label { margin-top: 0; }
General sibling combinator (~)
Targets all matching elements that follow a specified element, sharing the same parent, not just the immediately adjacent one.
/* All <p> elements that follow an <h2>, not just the first */ h2 ~ p { color: #475569; }
Attribute selectors
Attribute selectors target elements based on the presence or value of their HTML attributes. They are extremely powerful for styling form inputs, links, and custom data attributes.
/* Has the attribute (any value) */ [disabled] { opacity: 0.5; cursor: not-allowed; } /* Exact value match */ [type="text"] { border: 1px solid #cbd5e1; } /* Value starts with */ [href^="https"] { color: green; } /* Value ends with */ [href$=".pdf"] { padding-right: 1.25rem; } /* Value contains */ [class*="btn"] { cursor: pointer; border-radius: 4px; } /* Value is a space-separated word */ [class~="active"] { font-weight: bold; } /* Value equals or starts with (useful for language codes) */ [lang|="en"] { font-family: Georgia, serif; }
/* Case-insensitive match */ [type="text" i] { } /* Case-sensitive match */ [data-status="Active" s] { }
Selecting form inputs by type
The <input> element uses the same tag for text fields, checkboxes, radios, and more, so styling "just the checkboxes" or "just the email fields" means using an attribute selector on type, not a different tag or class.
/* Style every checkbox input */ input[type="checkbox"] { accent-color: #7c6af7; } /* Style every radio button */ input[type="radio"] { width: 18px; height: 18px; } /* Style email and number inputs the same way */ input[type="email"], input[type="number"] { font-variant-numeric: tabular-nums; }
Data attribute selectors
JavaScript frameworks and testing tools commonly attach custom data-* attributes to elements to track state or provide hooks that don't affect styling by default. Because they're plain attributes, they work with every attribute selector operator covered above.
/* Style based on a data-* state attribute */ [data-theme="dark"] { background: #0f111a; } /* Toggle-style component driven entirely by a data attribute */ [data-state="open"] .panel { display: block; }
Pseudo-classes: selecting by state
Pseudo-classes target elements based on their current state, position, or user interaction, information that is not visible in the HTML source.
User interaction states
/* Mouse hover */ .btn:hover { background: #1d4ed8; } /* Keyboard focus: never remove, essential for accessibility */ .btn:focus { outline: 2px solid #3b82f6; outline-offset: 2px; } /* Focus only via keyboard, not mouse */ .btn:focus-visible { outline: 2px solid #3b82f6; } /* Active (being clicked) */ .btn:active { transform: scale(0.97); } /* Visited links */ a:visited { color: #7c3aed; }
Form states
/* Valid and invalid input */ input:valid { border-color: #22c55e; } input:invalid { border-color: #ef4444; } /* Checked checkbox or radio */ input:checked + label { font-weight: bold; } /* Disabled input */ input:disabled { background: #f1f5f9; cursor: not-allowed; } /* Required field */ input:required { border-left: 3px solid #f59e0b; } /* Placeholder shown (input is empty) */ input:placeholder-shown { border-color: #cbd5e1; }
Structural pseudo-classes
These target elements based on their position within their parent. They are the foundation of pattern-based styling without adding extra classes.
/* First and last child */ li:first-child { border-top: none; } li:last-child { border-bottom: none; } /* First and last of a specific type */ p:first-of-type { font-size: 1.1rem; } p:last-of-type { margin-bottom: 0; } /* Every other item (zebra striping) */ tr:nth-child(even) { background: #f8fafc; } /* Every third item starting from the first */ li:nth-child(3n+1) { color: #3b82f6; } /* Only child */ p:only-child { font-style: italic; } /* Empty element */ div:empty { display: none; }
Other useful pseudo-classes
/* Negation: everything except .active */ li:not(.active) { opacity: 0.6; } /* Multiple negations (modern CSS) */ li:not(.active):not(.disabled) { cursor: pointer; } /* :is(): groups selectors with shared styles */ :is(h1, h2, h3, h4) { font-family: Georgia, serif; line-height: 1.2; } /* :where(): like :is(), but zero specificity */ :where(h1, h2, h3) { margin-top: 1.5rem; } /* :has(): parent selector (select parent based on child) */ .card:has(img) { padding-top: 0; } label:has(+ input:required)::after { content: " *"; color: #ef4444; }
Pseudo-elements: selecting virtual parts
Pseudo-elements target a specific part of an element, or insert generated content. They use double-colon syntax (::) to distinguish them from pseudo-classes.
/* First line of a paragraph */ p::first-line { font-variant: small-caps; } /* First letter of a paragraph (drop cap) */ p::first-letter { font-size: 3em; float: left; line-height: 1; margin-right: 0.1em; } /* Insert content before and after */ .quote::before { content: "\201C"; } .quote::after { content: "\201D"; } /* Style selected text */ ::selection { background: #bfdbfe; color: #1e3a5f; } /* Style placeholder text */ input::placeholder { color: #94a3b8; font-style: italic; }
Selector specificity
When two selectors target the same element, the browser uses specificity to decide which rule wins. Specificity is calculated as a three-part score: (ID, Class/Attribute/Pseudo-class, Type/Pseudo-element).
/* Specificity: (0, 0, 1), type selector */ p { color: black; } /* Specificity: (0, 1, 0), class selector */ .text { color: blue; } /* Specificity: (0, 1, 1), class + type */ p.text { color: green; } /* Specificity: (1, 0, 0), ID selector */ #intro { color: red; }
- ID selectors always beat class selectors, regardless of how many classes are chained
!importantoverrides everything: avoid using it except in utility classes or resets:is(),:not(), and:has()take the specificity of their most specific argument:where()always has zero specificity: useful for base styles you want to be easily overridden- When specificity is equal, the rule that appears last in the stylesheet wins
Selector performance tips
Browsers read selectors from right to left: the rightmost part is matched first, then the browser walks up the DOM to verify the rest. This means the rightmost selector (the key selector) should be as specific as possible to limit the number of elements the browser needs to check.
- Avoid overly broad key selectors:
* { }anddiv { }force the browser to check every element. - Avoid deep descendant chains:
.nav ul li a spanmakes the browser walk five levels up the DOM for every<span>on the page. - Prefer class selectors: They are fast, reusable, and have predictable specificity.
- Avoid
*as a key selector in combinations:.parent *checks every element in the document to see if it is inside.parent.
In practice, selector performance is rarely the bottleneck on modern browsers: layout, paint, and JavaScript execution are far more impactful. But clean, focused selectors make your CSS easier to maintain.
CSS selectors in React and component-based apps
The selector rules above still apply in React, Vue, or any component framework, but two habits matter more once your markup is generated by components instead of hand-written HTML.
- Avoid ID selectors for styling entirely: components can render more than once on a page (a modal, a list item, a repeated widget), and a hard-coded ID will collide the second time that component mounts. Classes, CSS Modules, or scoped styles (styled-components, CSS-in-JS) don't have this problem.
- Reserve an attribute for test hooks: a dedicated attribute like
data-testidkeeps testing selectors separate from styling selectors, so a designer renaming a class doesn't silently break a test suite, and a developer refactoring tests doesn't touch CSS.
/* Styling selector: safe to rename, only affects appearance */ .card-title { font-weight: 600; } /* Test selector: kept separate from styling, safe for automated tests */ [data-testid="card-title"] { }
CSS selectors beyond stylesheets
CSS selector syntax isn't only for stylesheets. The same patterns you've just learned, classes, IDs, attributes, and combinators, are reused as the query language in browser DevTools, the DOM's own JavaScript API, browser automation tools like Selenium and Playwright, and web scraping libraries.
Selecting elements in JavaScript
The DOM API accepts any valid CSS selector string, so everything covered in this guide, including combinators, attribute selectors, and most pseudo-classes, works directly in querySelector and querySelectorAll.
/* Returns the first match, or null */ document.querySelector('.card:has(img)'); /* Returns a static NodeList of every match */ document.querySelectorAll('input[type="checkbox"]:not(:checked)');
CSS selectors in Selenium
Selenium's By.CSS_SELECTOR locator strategy uses the exact same syntax as a stylesheet, in Java, Python, C#, and every other language binding. It's a common alternative to XPath for locating elements in automated tests.
// Java driver.findElement(By.cssSelector("button.submit-btn")); # Python driver.find_element(By.CSS_SELECTOR, "button.submit-btn") // C# driver.FindElement(By.CssSelector("button.submit-btn"));
CSS selectors versus XPath is a common question for anyone writing Selenium tests. CSS selectors are generally faster to execute and easier to read for class, ID, and attribute-based matching. XPath's advantage is traversing upward to a parent or matching by an element's visible text, both of which the :has() pseudo-class now covers for the parent case directly in CSS. Playwright, WebdriverIO, and most modern browser automation tools accept CSS selectors the same way.
Web scraping and other tools
Python's BeautifulSoup exposes a .select() method that takes a CSS selector string, and Scrapy responses expose the same thing through response.css(). Google Tag Manager's "Element Matches CSS Selector" trigger condition also evaluates a CSS selector against the page to decide when to fire a tag.
# BeautifulSoup soup.select('.product-card[data-in-stock="true"]') # Scrapy response.css('a.product-link::attr(href)').getall()
For finding the right selector on a live page, your browser's built-in DevTools (right-click an element, Inspect, then right-click the highlighted markup and copy its selector) covers most cases without installing anything extra, and is the same tool used to verify a selector before dropping it into a test script or a GTM trigger.
Frequently asked questions
How many types of CSS selectors are there?
CSS selectors fall into five broad categories: basic (type, class, ID, universal), attribute, combinators, pseudo-classes, and pseudo-elements. Within those categories there are dozens of individual selectors, and combining simple selectors into compound and complex selectors means the number of possible selectors you can write is effectively unlimited.
What is the difference between CSS selectors and XPath in Selenium?
Both locate elements in automated tests, but CSS selectors are generally faster to execute and more readable for matching by class, ID, or attribute. XPath can traverse upward to a parent element and match by an element's visible text, which CSS selectors traditionally couldn't do. The :has() pseudo-class now closes most of that gap for the parent-selection case.
What is the difference between :nth-child and :nth-of-type?
:nth-child(n) counts all sibling elements regardless of type. :nth-of-type(n) counts only siblings of the same element type. If your list contains mixed element types, :nth-of-type gives more predictable results. For a consistent list of the same element, both work the same.
What is the difference between :is() and :where()?
Both group selectors to share a set of styles. The difference is specificity. :is() takes the specificity of its most specific argument, so :is(#id, .class) has ID-level specificity. :where() always contributes zero specificity, making it ideal for base and reset styles that should be easy to override.
Can I select a parent element based on its children in CSS?
Yes, using the :has() pseudo-class, which is now supported in all modern browsers. .card:has(img) selects any .card that contains an <img>. This was impossible in CSS for years and required JavaScript. :has() is one of the most powerful additions to modern CSS.
What is the difference between the child combinator and the descendant combinator?
The descendant combinator (a space) targets an element anywhere inside another, at any depth. The child combinator (>) targets only direct children, one level deep. Use > when you want to avoid unintentionally styling deeply nested elements of the same type.
How do I select every element except one?
Use the :not() pseudo-class. For example, li:not(.active) selects all list items except those with the active class. Modern CSS also allows multiple arguments: li:not(.active, .disabled) excludes both classes in one rule.
Why should I avoid ID selectors for styling?
ID selectors have very high specificity: higher than any number of chained class selectors. This makes them hard to override cleanly and leads to specificity wars where you end up using !important or doubling selectors just to win. IDs are also unique per page, so they are not reusable. Class selectors give you the same targeting ability with manageable, predictable specificity.
Test CSS nth-child patterns interactively
Use our free nth-child Tester to preview selector patterns against a real element list in real time.
Open nth-child Tester →