Skip to content

CSS selectors

docs.scrimba.com

You have a page full of elements and a set of styles you want to apply, but a colour or a font is no use until you can say which elements it belongs to. A selector is how you point at the right ones: this paragraph, every button, only the links inside the nav. Get comfortable with selectors and the rest of CSS becomes a matter of choosing what to change, because you already know how to reach it.

Type, class, and id selectors

A CSS rule has two parts: a selector that picks which elements to style, and a block of declarations that says how. The simplest selector is the element's name, called a type selector. Write p and every paragraph on the page is styled.

That is often too broad. You rarely want all paragraphs to look the same. So you give elements a label called a class, using the HTML class attribute, and target that label from CSS with a dot in front of it. Think of a class like a name tag: you pin the same tag on every element you want to treat as a group, then style the tag once.

There are three basic ways to target an element. A type selector matches by tag name (p matches every paragraph). A class selector matches by the class attribute and is written with a leading dot (.button). An id selector matches by the id attribute and is written with a leading hash (#header).

Reach for classes as your default styling hook. A class can go on as many elements as you like and an element can carry several classes, so classes describe reusable groups: everything with .card shares a look. Type selectors are useful for broad base styling (setting the body font, for instance) but they are blunt, since they hit every element of that kind. Ids are meant to be unique on a page, so styling through them locks a rule to a single element and, as you will see under specificity, they carry extra weight that makes them awkward to override.

Three selector types cover the basics, and the distinction between them is partly about matching and partly about weight. A type selector (also called a tag or element selector) matches by element name: p, section, input. A class selector, written .card, matches any element whose class attribute contains that token, and the attribute is space separated so one element can hold many classes. An id selector, written #header, matches the single element whose id attribute equals that value; an id is required to be unique within a document.

The practical rule is to style through classes almost exclusively. Classes are the only one of the three that is both reusable and low in specificity (the priority score the browser uses to resolve conflicts, covered at the end of this chapter), which keeps your styles flat and overridable. Type selectors belong to a small set of base rules: a reset, the default font on body, sensible defaults on a or input. Ids are legitimate as JavaScript hooks and as fragment link targets (#pricing in a URL), but as a styling hook they are a trap: they are unique so they cannot describe a group, and their high specificity means a later class rule cannot override them without escalation. Style with classes, reserve ids for anchors and scripts.

html
<button class="btn-primary">Sign up</button>
<button class="btn-primary">Buy now</button>
css
/* type selector: every button on the page */
button {
  font-family: inherit;
}

/* class selector: only elements tagged btn-primary */
.btn-primary {
  background: rebeccapurple;
  color: white;
}

Both buttons pick up the .btn-primary styling from one rule, because they share the class. That reuse is the whole reason classes are the tool you reach for. This builds directly on how CSS works, where each rule pairs a selector with a set of declarations.

JunoType, class, and id selectors A type selector like p hits every element of that kind, which is usually more than you want. A class is a name tag you pin on the elements you care about and style with a dot, like .btn-primary. Start with classes for almost everything and you will not go far wrong.
JunoType, class, and id selectors Classes are your default hook: reusable, and one element can wear several. Keep type selectors for broad base styles like the body font, and treat ids as too specific and too single-use for styling. Save yourself the future headache and reach for a class first.
JunoType, class, and id selectors Classes are the only basic selector that is both reusable and low specificity, which is exactly why you style through them and keep your rules flat. Type selectors earn their place in a small base layer, and ids belong to scripts and link anchors, not to styling. The day an id rule refuses to be overridden by a later class, you will be glad you kept them out.

Combinators: selecting by relationship

Sometimes you want to style an element only when it sits inside another one. The links in your navigation should look different from links in a paragraph, even though both are <a> elements. You describe that relationship with a combinator: put two selectors side by side with a space, and CSS reads it as "the second one, but only inside the first".

So .nav a means "any link inside an element with the class nav". The space is doing real work here. It is not decoration, it is the instruction to look inside.

A combinator joins two selectors and matches based on how the elements relate in the HTML. The most common is the descendant combinator, written as a space: .nav a matches every <a> anywhere inside a .nav, no matter how deeply nested.

When you want only direct children and not deeper descendants, use the child combinator >. .menu > li matches list items that are immediate children of .menu, skipping any nested lists. There are also two sibling combinators for elements that share a parent: the adjacent sibling + matches the element immediately after another (h2 + p styles the paragraph right after a heading), and the general sibling ~ matches any following sibling. Descendant and child cover most real work; the sibling combinators are handy for spacing and for form patterns.

A combinator is the character between two selectors that defines the structural relationship they must satisfy. The descendant combinator (a space) matches the right-hand selector when it is nested anywhere inside the left, at any depth. The child combinator (>) restricts that to direct children only: .menu > li ignores list items buried in a nested .submenu. The adjacent sibling combinator (+) matches an element that immediately follows another with the same parent, and the general sibling combinator (~) matches any later sibling with the same parent.

Two things are worth holding onto in production. First, a compound selector reads right to left in the browser's matcher: for .nav a the engine finds the <a> elements first, then walks up to check each has a .nav ancestor. This is why very long descendant chains cost more and why a tight .nav > a can be cheaper than a loose .nav a. Second, descendant selectors couple your CSS to your markup structure: .card .title breaks the moment someone wraps the title in another element. Favouring a direct class on the element (.card-title) keeps styles resilient to markup changes, and you drop to combinators when a relationship is the thing you actually want to express.

css
/* descendant: any link inside the nav */
.nav a {
  text-decoration: none;
}

/* child: only list items that are direct children of .menu */
.menu > li {
  display: inline-block;
}

The first rule reaches every link nested inside the nav; the second reaches only the top-level items of the menu and leaves any nested list alone. Choosing between them is choosing how far down you want the rule to travel.

JunoCombinators A space between two selectors means "inside": .nav a styles links that sit inside the nav. That space is an instruction, not spacing for looks, so do not drop it by accident. This one pattern covers a lot of everyday styling.
JunoCombinators The space is the descendant combinator and matches at any depth; > narrows it to direct children only. Siblings + and ~ are there when you need to style an element based on what comes before it. Descendant and child will handle most of what you write day to day.
JunoCombinators Combinators are useful, and they also tie your CSS to your HTML structure, so a long descendant chain like .card .title is one refactor away from breaking. Prefer a direct class on the element and drop to combinators when the relationship is the actual thing you want to express. Matching runs right to left too, so a tight > often beats a loose descendant chain.

Pseudo-classes: styling by state

Elements can be in different states. A link can be sitting still or have the mouse hovering over it. A pseudo-class lets you style an element based on its state, and you write it with a single colon after the selector.

The one you will use first is :hover. Write .btn-primary:hover and those styles apply only while the pointer is over the button, which is how buttons and links give that little visual response when you point at them.

A pseudo-class targets an element in a particular state or position, written with a single colon: :hover while the pointer is over it, :focus when it is selected for keyboard input, :first-child when it is the first element inside its parent.

:focus deserves attention because it is an accessibility concern, not a nicety. People who navigate with a keyboard tab from one control to the next, and the :focus style is the only signal telling them where they are. Never remove focus outlines without replacing them with a visible alternative. There are also structural pseudo-classes like :nth-child(), which takes a pattern in the parentheses: :nth-child(odd) matches every other element, useful for striping table rows. :first-child and :last-child grab the ends of a group without needing an extra class.

A pseudo-class (a keyword after a single colon) matches an element that is in a given state or structural position, without you adding any markup for it. State pseudo-classes include :hover, :active, and :focus; structural ones include :first-child, :last-child, and the parameterised :nth-child(), whose argument is an an+b pattern (:nth-child(2n) is every second element, :nth-child(3n+1) is the first of each group of three).

Two production notes. :focus is non-negotiable for keyboard accessibility: keyboard users rely on the focus ring to know their position, so stripping the outline without a visible replacement makes an interface unusable for them. Prefer :focus-visible, which shows the ring for keyboard interaction but suppresses it on mouse click, so you get both a clean pointer experience and an accessible keyboard one. On the structural side, remember :nth-child(n) counts all siblings regardless of type, while :nth-of-type(n) counts only siblings of the same element, which is the difference between a rule that works and one that silently miscounts when a stray element sits in the list.

css
.btn-primary:hover {
  background: purple;
}

/* keyboard users need a visible focus indicator */
.btn-primary:focus-visible {
  outline: 2px solid rebeccapurple;
  outline-offset: 2px;
}

The first rule fires while the pointer is over the button; the second draws a clear ring when the button is selected by keyboard, which is what keeps the page usable without a mouse. Colour and outline choices here connect to how CSS works and the values you set on any property.

JunoPseudo-classes A pseudo-class styles an element in a certain state, written with one colon, like :hover for when the pointer is over it. That is where a button's hover effect comes from. You attach it right onto the selector, no space in between.
JunoPseudo-classes Pseudo-classes match state and position: :hover, :focus, :nth-child(), :first-child. Treat :focus as an accessibility requirement, because keyboard users need to see where they are, so never strip the outline without a visible replacement. That habit will keep your pages usable for everyone.
JunoPseudo-classes Reach for :focus-visible over bare :focus so the ring shows for keyboard users but not on mouse click, and never ship an interface that removes the focus indicator outright. Watch the :nth-child versus :nth-of-type trap: one counts every sibling, the other only same-type siblings, and the wrong choice miscounts the moment a stray element lands in the list. It is the kind of bug that looks fine until it does not.

Pseudo-elements: styling a part of an element

A pseudo-class styles a whole element in a certain state. A pseudo-element is different: it lets you style a part of an element, or add a small piece of content that is not in your HTML at all.

The two you will meet first are ::before and ::after. They insert content at the start or end of an element, and you tell them what to insert with the content property. Notice the two colons: that double colon is how CSS marks a pseudo-element apart from a pseudo-class.

A pseudo-element styles a specific part of an element rather than the whole thing, and it uses a double colon (::) to distinguish it from a single-colon pseudo-class. The most used are ::before and ::after, which create a generated child at the very start or end of an element. They require a content property to render, even if it is empty (content: ""), and that generated content is decorative: screen readers may skip it, so never put meaning there.

Another common one is ::placeholder, which styles the faint hint text inside an empty input. Because pseudo-elements target a slice of the element, they let you build small visual pieces (an icon before a label, a decorative line after a heading) without adding extra tags to your markup.

A pseudo-element exposes a part of an element that has no tag of its own, styled through a double-colon keyword. ::before and ::after generate an inline box as the element's first or last child; they render only when content is set (use content: "" for a purely decorative box), and that generated content sits outside the accessibility tree, so it must stay decorative and never carry meaning a user needs. ::placeholder targets an input's hint text, ::selection targets highlighted text, ::first-line and ::first-letter target typographic slices.

The single-versus-double colon story is worth knowing. Modern pseudo-elements use :: (two colons) to separate them from pseudo-classes, a distinction introduced in CSS3. For the four original pseudo-elements (:before, :after, :first-line, :first-letter), the single-colon form still parses for legacy reasons, but write the double-colon form; the single colon is a compatibility relic, not a style you should carry into new code. In production, ::before and ::after do a lot of quiet work: tooltips, custom list markers, clearfix in older layouts, and decorative flourishes that would otherwise clutter the HTML.

css
/* add a decorative arrow after every nav link */
.nav a::after {
  content: " →";
  color: rebeccapurple;
}

/* style the hint text inside a search box */
.search-input::placeholder {
  color: grey;
  font-style: italic;
}

The ::after rule inserts an arrow that lives only in the styling, not in the HTML, while ::placeholder reaches inside the input to style text the browser draws for you. Both style something you could not otherwise reach with a plain selector. If you are styling hint text and labels, the typography chapter goes deeper on the text side.

JunoPseudo-elements A pseudo-element styles a part of an element or adds a little content, and it uses two colons, like ::before and ::after. Those two need a content property to show up. The double colon is the quick way to tell a pseudo-element from a pseudo-class.
JunoPseudo-elements Pseudo-elements use a double colon and style a slice of an element: ::before, ::after, ::placeholder. The first two need a content value, even an empty "", and that content is decorative, so keep real meaning in your HTML. They save you from adding tags only to hang a decoration on.
JunoPseudo-elements Write pseudo-elements with two colons; the single-colon form only survives for the four originals as a legacy quirk. Content from ::before and ::after lives outside the accessibility tree, so it stays decorative and never carries anything a user needs to read. They quietly handle tooltips, custom markers, and flourishes that would otherwise bloat your markup.

Specificity: which rule wins

Sooner or later two rules will try to style the same element in different ways. One says the text is blue, another says it is red. The browser has to pick one, and the way it decides is called specificity: the more specific selector wins.

The short version is an order of strength. An id selector beats a class selector, and a class selector beats a type selector. So if #header and .title both set a colour on the same element, the id wins. When two rules are equally specific, the one written later in the file wins.

When several rules target the same element and set the same property, the browser resolves the conflict with specificity, a score based on the kinds of selectors involved. The order from strongest to weakest is: inline styles (a style attribute on the element), then ids, then classes (and pseudo-classes and attribute selectors), then type selectors (and pseudo-elements). A more specific selector wins regardless of source order; only when specificity ties does the later rule win.

This is the practical reason to lean on classes and avoid ids for styling. If you style with ids, a later class rule cannot override them, so you end up escalating. And !important, which you can append to a declaration to force it to win over everything, should be treated as a last resort: it breaks the normal cascade and the only way to beat one !important is another !important, which is a spiral you do not want to start.

Specificity is the algorithm the browser uses to choose between conflicting declarations. It is computed as a three-part count, often written (a, b, c): a is the number of id selectors, b is the number of class, attribute, and pseudo-class selectors, and c is the number of type and pseudo-element selectors. The parts compare left to right, so any id beats any number of classes, and any class beats any number of type selectors. #header .title a scores (1, 1, 1); .nav .menu .item a scores (0, 3, 1); the first wins on the id alone. Inline style attributes sit above all selector specificity, and !important (a flag you append to a value to force it) overrides even that; the universal selector * and combinators add nothing to the score.

Two modern tools change how you manage this. :is() takes a list of selectors and matches if any of them do, but it takes the specificity of its most specific argument, so :is(#header, .title) inherits the id's weight. :where() does the same matching but has zero specificity always, which makes it the tool for writing low-weight base styles that anything can override. :has(), the relational pseudo-class, matches an element that contains something ("a .card that has an image", .card:has(img)) and takes its argument's specificity. The maintainability practice underneath all this is to keep specificity low and flat: prefer single-class selectors, avoid ids and !important, and use :where() to zero out weight where you want defaults to yield. Flat specificity means a later rule can always win by being later, which is the cascade working for you instead of against you.

css
/* type selector, specificity (0, 0, 1) */
p {
  color: grey;
}

/* class selector, specificity (0, 1, 0): this one wins */
.intro {
  color: black;
}
html
<p class="intro">This paragraph is black, because the class rule outranks the type rule.</p>

Both rules target the paragraph and both set color, but the class selector is more specific than the type selector, so the paragraph is black. Nudging one selector up or down the specificity ladder is often all it takes to settle a conflict without resorting to heavier tools. The box model chapter runs into the same cascade when spacing rules collide.

JunoSpecificity When two rules fight over the same element, the more specific one wins: an id beats a class, and a class beats a type selector. If they are equally specific, whichever comes last in the file wins. That is most conflicts explained.
JunoSpecificity Strength runs inline, then id, then class, then type, and a more specific rule wins no matter where it sits in the file. This is why styling with classes and avoiding ids keeps your CSS overridable. Keep !important as a last resort, because the only thing that beats one is another one.
JunoSpecificity Specificity counts as (a, b, c) for ids, classes, and types, compared left to right, so one id outranks any pile of classes. Keep it low and flat: single classes, no ids for styling, no !important, and :where() to zero out weight when you want defaults to yield. Flat specificity means a later rule can always win by being later, and that is the cascade working with you.