CSS Structural Pseudo-Classes: :first-child and :nth-child()

Introduction

Structural pseudo-classes select elements according to relationships recorded in the document tree. Instead of requiring a special class on the first list item, every second table row, or the first three cards, CSS can calculate position from sibling order. Two foundational selectors are :first-child and :nth-child(). The first matches an element when it is the first element child of its parent; the second matches positions described by an index or formula.

These selectors are compact and expressive, but they are also frequently misunderstood. Child positions are one-based, all element siblings normally participate in counting, hidden elements still count, and CSS layout order does not rewrite DOM order. A reliable tutorial must therefore explain not only syntax but also the tree model behind each match.

What Are Structural Pseudo-Classes?

A structural pseudo-class describes where an element sits relative to its parent and siblings. Examples include :first-child, :last-child, :only-child, :nth-child(), :nth-last-child(), :first-of-type, and :nth-of-type(). Browsers recalculate these conditions when the DOM changes, so inserted, removed, or reordered siblings can change which elements match.

Structural selectors reduce markup added solely for presentation and work well when position itself carries stable meaning. They become fragile when the position is incidental, content is frequently reordered, or optional elements alter counting. In those cases, a role or state class may communicate intent more clearly.

Basic Syntax

li:first-child {
  font-weight: 700;
}

li:nth-child(3) {
  color: #0b63a3;
}

li:nth-child(odd) {
  background: #f4f7fa;
}

The first rule selects an li only if it is the first element child under its parent. The second selects an li that occupies child position three. The third selects li elements in odd child positions. In every case, the type selector and pseudo-class form one compound selector, so both conditions must be true.

How Browsers Determine Child Position

For a selector such as li:nth-child(3), the browser considers li candidates and determines each candidate's one-based position among all element children of its parent. If the li is the third element child, it matches. A heading or button before it changes the number even though that sibling is not an li.

Text nodes, indentation whitespace, and comments do not occupy element positions. Generated ::before and ::after boxes are not DOM element children either. This distinction explains why formatted HTML whitespace does not disrupt first-child, while a real heading element does.

What Is :first-child?

The :first-child pseudo-class matches an element that is the first element child of its parent. It does not mean the first occurrence of that element type. In p:first-child, a paragraph matches only when no earlier element sibling exists. If an h2 appears first, the first paragraph does not match because its child position is two.

This selector is useful for removing the leading margin from the first item, styling the first navigation entry, or treating the first cell in each row. Its meaning is always local to each parent, so one page may contain many independent first-child matches.

A Basic :first-child Example

<ul class="steps">
  <li>Create an account</li>
  <li>Choose a course</li>
  <li>Start learning</li>
</ul>

.steps li:first-child {
  color: #0f5f8f;
  font-weight: 700;
}

Only “Create an account” matches because it is both an li and the first element child of the ul. If a heading were inserted as the first child inside the same ul, the markup would be invalid for a normal list and the li would lose its first-child match. Valid, semantic structure makes positional selectors easier to reason about.

The Most Common :first-child Mistake

Consider a div containing an h2 followed by two paragraphs. The selector p:first-child matches nothing because the h2 is the first element child. Developers often read it as “the first paragraph child,” but that behavior belongs to p:first-of-type. First-child evaluates absolute child position before the p condition can succeed.

When a rule fails, inspect every element sibling under the immediate parent. Do not rely on visual layout or search only for earlier elements of the same type. Browser developer tools make the child sequence explicit.

:first-child Versus :first-of-type

p:first-child requires the paragraph to be child position one. p:first-of-type requires it to be the first p among its siblings, even if headings, images, or other element types come first. These selectors can match the same node, but their conditions differ.

Use first-child when the first position is meaningful regardless of type. Use first-of-type when the first occurrence of a particular element name matters. Neither means “the first element with this class”; class-based type groups need different techniques, such as :nth-child(1 of .item) where supported or an explicit role class.

Equivalent Selectors

:first-child is functionally equivalent to :nth-child(1). The named form is usually clearer because it directly communicates intent. Likewise, :last-child corresponds to the final child position, while :nth-last-child(1) expresses the same result by counting backward.

Equivalent matching does not always mean equal readability. Prefer the simplest named pseudo-class for first, last, or only-child conditions. Use nth formulas when a repeated sequence, bounded range, or calculated position is truly needed.

What Is :nth-child()?

The :nth-child() pseudo-class matches elements whose sibling position satisfies its argument. The argument may be a positive integer, the keywords odd or even, or an An+B formula. Positions begin at one; there is no child at position zero.

In .item:nth-child(4), the candidate must have the item class and occupy the fourth element-child position. The class does not filter the siblings being counted under the traditional syntax. If unrelated element siblings appear before or between items, they still affect item positions.

Selecting a Specific Position

.lesson-list li:nth-child(2) {
  border-inline-start: 4px solid #0b63a3;
}

This selects an li only when it is the second element child of lesson-list. It does not select the second li if a non-li element occupies an earlier position. When a parent is semantically a ul or ol, direct children should normally be li elements, so the distinction rarely causes trouble. In mixed-content containers, it matters immediately.

Use a direct-child combinator when ownership should be explicit: .lesson-list > li:nth-child(2). This prevents nested list items from matching through a descendant relationship.

Odd and Even Keywords

The keyword odd selects positions 1, 3, 5, and so on. It is equivalent to 2n+1. The keyword even selects positions 2, 4, 6, and so on and is equivalent to 2n. These keywords improve readability for alternating rows, cards, or repeated content.

Alternation follows DOM position, not whether an element is visibly rendered. A hidden row still occupies a child position and can shift the apparent stripe pattern. Filtering through the modern of syntax or applying classes during data rendering may be preferable when only visible or eligible items should determine alternation.

Understanding An+B

The general formula is An+B. The browser substitutes integer values of n starting at zero and keeps positive results that correspond to child positions. A defines the repeating interval, while B defines the offset. For 3n+1, the sequence is 1, 4, 7, 10, and so on. For 4n, it is 4, 8, 12, and onward.

Spaces around plus and minus must follow valid CSS grammar, but common compact forms are easy to read. Negative or zero results do not correspond to elements and are ignored. Thinking in generated sequences is often more reliable than memorizing patterns.

Every Second, Third, or Fourth Element

:nth-child(2n) selects every second position, :nth-child(3n) every third, and :nth-child(4n) every fourth. Adding an offset changes where the cycle begins: 3n+2 produces 2, 5, 8, and 11. This is useful for grid rhythm, repeated borders, or clearing visual groups.

Modern grid and flex layouts usually manage wrapping without old clearfix techniques, but nth-child remains valuable for patterned styling. Be careful when a responsive layout changes column count; a formula designed for four columns may create the wrong edge treatment at two columns.

Selecting the First N Children

A negative coefficient can define a finite range. :nth-child(-n+3) produces positions 3, 2, and 1 as n increases from zero until results become nonpositive, so it selects the first three children. Similarly, :nth-child(-n+5) selects the first five.

This pattern can emphasize an initial group or limit a presentation rule. If the group has semantic meaning such as “featured,” an explicit class or server-provided state may be more robust than assuming the first three items are always featured. Position should be the actual source of truth.

Selecting From a Starting Position

:nth-child(n+4) selects position four and every later position. When n begins at zero, the sequence is 4, 5, 6, and onward. This can hide or de-emphasize overflow items in a compact preview, although essential content should not be made inaccessible without an operable expansion control.

Combining ranges can isolate a middle segment. For example, an element matching both :nth-child(n+3) and :nth-child(-n+6) falls between positions three and six inclusive. Keep combined formulas documented because their intent may not be obvious during maintenance.

Counting Backward with :nth-last-child()

:nth-last-child() uses the same An+B grammar but counts from the end of the sibling list. :nth-last-child(2) selects the second-to-last child, and :nth-last-child(-n+3) selects the final three children. The element type or class outside the pseudo-class still does not filter the count.

Backward counting is useful for trailing groups, removing borders near the end of a layout, or adapting controls when only a few items remain. Dynamic insertion at the end changes which elements match, just as insertion at the beginning affects forward counting.

The of Selector List

Modern :nth-child() syntax can include of followed by a selector list. The form :nth-child(2 of .item) selects the second sibling that matches .item, rather than requiring a .item to occupy overall child position two. Unmatched siblings are excluded from the counting set.

.result:nth-child(odd of :not([hidden])) {
  background: #f4f7fa;
}

This pattern stripes visible, non-hidden result elements even when hidden siblings remain in the DOM. Verify target-browser support and provide a suitable fallback when the project supports older engines or embedded webviews.

Traditional Filtering Versus of Syntax

.item:nth-child(2) means “an item that is child number two.” :nth-child(2 of .item) means “the second child among siblings that match item.” These are identical only when every counted sibling before the target is also an item. Mixed siblings expose the difference.

This distinction solves many historical nth-child frustrations. Before of syntax, authors often used nth-of-type, changed markup, added helper classes, or accepted incorrect alternation. The of list supports classes, attributes, and more expressive selector groups rather than only element names.

Specificity Rules

Structural pseudo-classes such as first-child and nth-child contribute one class-category count. Therefore, li:first-child and li:nth-child(2) each have specificity 0-1-1: one pseudo-class and one type selector. The numeric formula does not add weight.

When nth-child includes an of selector list, its specificity includes the pseudo-class plus the most specific selector in that list. For example, :nth-child(2 of .item, #featured) takes ID specificity from the strongest argument in addition to the pseudo-class count. Use :where() inside an of list when zero argument specificity is desired.

Table Row Striping

tbody tr:nth-child(even) {
  background-color: #f3f6f9;
}

tbody tr:hover {
  background-color: #e7f1f8;
}

Scoping the rule to tbody avoids counting header rows under a separate thead. Each tbody creates its own sibling group, so multiple tbody elements restart the pattern. Row and column headers still need semantic th markup, and striping should maintain sufficient text contrast.

Color alone should not carry critical row meaning. Selected, error, or warning rows need text, icons, or accessible state as appropriate. Hover styling should not erase zebra contrast so completely that users lose their reading position.

First Cells and Column Styling

td:first-child selects a td only when it is the first element child of its row. In well-formed data tables, the first cell may instead be a th row header, so :first-child without a td qualifier or a role class may better match the intended first column. The right selector depends on table semantics.

tr > :nth-child(3) selects the third cell in each row, whether it is th or td. Colspan can make visual column positions differ from child indexes, so complex tables may require col elements, classes, or data-driven markup rather than positional assumptions.

Navigation and Breadcrumbs

First-child and last-child can remove separators or outer spacing in navigation lists. A breadcrumb might add a separator before every item except the first using .breadcrumb li:not(:first-child)::before. An adjacent sibling selector, .breadcrumb li + li, can express the same “every item after the first” relationship more directly.

Choose the selector that communicates intent. Positional styling must not replace current-page semantics such as aria-current="page". The first link is not automatically the homepage, and the final breadcrumb requires semantic current-state information rather than only a visual difference.

Card Grids

Nth-child can apply repeating accents or remove outer-edge borders in fixed-column card grids. For a three-column layout, .card:nth-child(3n) identifies every third DOM item. This is safe only while three columns consistently represent the visual row.

Responsive grids can change track count based on available space, content, or auto-fit behavior. CSS does not offer a general selector for “last item in the current visual row.” Gap usually handles spacing more reliably than nth-child margins. Use positional formulas for patterns that intentionally follow source sequence, not inferred layout geometry.

Form Groups

Structural selectors can remove the top margin from the first field group or the bottom divider from the last. A rule such as .form-section > .field:first-child works only if the field is the first child overall. A preceding heading or description prevents the match.

If the goal is the first field among mixed content, use :nth-child(1 of .field) where supported or wrap fields in a dedicated container. Wrappers can improve semantics and component ownership; avoiding all markup is not automatically better than using clear structure.

Lists with Optional or Hidden Items

An element with display: none, the hidden attribute, or visibility hidden remains an element child and normally participates in nth-child counting. As a result, visible rows can appear with two consecutive stripe colors or an unexpected “first visible” style. CSS layout visibility does not remove the node from the DOM tree.

The of syntax can filter known states, such as :nth-child(odd of :not([hidden])). If items are filtered through classes, virtual rendering, or server data, align the selector with the actual hidden-state mechanism. Test updates after filters change.

DOM Order Versus Visual Order

Flexbox order, grid placement, writing mode, and absolute positioning can change where an element appears without changing its sibling index. Structural pseudo-classes continue using DOM order. The visually first card may not match first-child if CSS moves it ahead of an earlier DOM sibling.

Source order should normally remain meaningful for keyboard navigation and assistive technology. Avoid rearranging content solely to make a positional style appear correct. When visual order must differ, ensure interaction and reading sequences still make sense and do not describe nth-child results as visual positions.

Dynamic Content

When JavaScript inserts, removes, or moves an element, browsers recalculate structural matches. Adding a new first item causes the previous first-child to lose its match and shifts every later nth-child position. This automatic update is useful for live lists, sortable interfaces, and data tables.

It also means positional styling can move unexpectedly during streaming updates or advertisements inserted among content. If a badge, priority, or business status must stay attached to a specific record, store that meaning in data and expose it through a class or attribute rather than deriving it from current position.

Direct Children and Scope

Selectors such as .list li:nth-child(odd) can match nested li elements anywhere below list because the space is a descendant combinator. If only top-level items should be striped, use .list > li:nth-child(odd). The child combinator clarifies ownership and prevents nested structures from inheriting a pattern unintentionally.

Component scoping through classes, CSS Modules, shadow DOM, or @scope further limits accidental matches. Positional selectors are easiest to maintain when their parent collection is explicit and stable.

Generated Content Does Not Count

Pseudo-elements such as ::before and ::after create generated boxes but do not become element children in the DOM. They do not shift nth-child positions and cannot themselves match first-child. Similarly, list markers are generated formatting constructs rather than siblings.

This separation is useful when adding decorative separators or icons. The structure remains unchanged, so existing positional formulas continue to match the same elements. Inspect the DOM tree rather than the rendered box tree when predicting structural selectors.

Shadow DOM and Slots

Structural selectors operate within the tree and style scope available to the stylesheet. Light-DOM children assigned to slots do not simply become ordinary DOM siblings of internal shadow elements for every selector. Shadow styles, slotted selectors, and host relationships follow component-specific rules.

When working with web components, use documented parts, slots, attributes, and custom properties instead of assuming a page-level nth-child rule can reach private internals. Within a repeated light-DOM collection, positional matching still follows that collection's actual parentage.

Accessibility Considerations

Position-based presentation should not be the only communication of meaning. Highlighting the first item does not announce “recommended,” and coloring every third row does not provide a label. If position represents priority, status, or sequence, expose that information in text, semantics, or accessible attributes.

Maintain contrast in alternating backgrounds, hover states, selected states, and forced-colors mode. Ensure reordering does not create a mismatch between visual sequence and keyboard or screen-reader order. Structural selectors should enhance a semantic document rather than compensate for unclear structure.

Common Mistake: Counting Only Matching Types

The selector p:nth-child(2) does not mean the second paragraph. It means a paragraph that is the second element child. A preceding h2 occupies position one, making the first p position two and therefore a match; another paragraph later may be the second p but not child two.

Use nth-of-type when grouping by element name, or nth-child with an of selector list when grouping by a class, attribute, or richer condition. State the counting set in plain language before writing the selector.

Common Mistake: Starting at Zero

Child indexes begin at one. :nth-child(0) matches nothing because there is no zero-position child. In formulas, n begins at zero, but the generated result must be a positive child position. Thus 2n first produces zero, which is ignored, then 2, 4, 6, and onward.

This difference between n's initial value and child indexing causes confusion. Write out the first several substitutions when validating a formula. A short sequence reveals most errors immediately.

Common Mistake: Assuming Hidden Items Are Removed

Hiding an element with CSS does not remove it from sibling counting. A filtered data table can therefore retain stripes based on original positions. Developers may attempt to fix this with more color overrides, but the counting model remains unchanged.

Use the of syntax to exclude a reliable hidden-state selector, remove filtered nodes when appropriate, or assign stripe classes based on the rendered data. Choose according to application architecture and browser support.

Common Mistake: Encoding Business Meaning in Position

A rule such as .product:nth-child(-n+3) can visually mark the first three products, but it does not know whether they are genuinely featured. Sorting, personalization, advertising, or asynchronous loading can change positions and therefore change the implied meaning.

When the business system determines featured status, expose it through data and a class or attribute. Use nth-child for presentation derived from order, not domain facts that merely happen to align with order today.

Debugging Structural Selectors

Inspect the target and list all immediate element siblings in DOM order. Confirm the parent, position, type, classes, hidden state, and whether a nested wrapper changes the relationship. Browser developer tools often display matched pseudo-classes and let you test selectors in the Elements panel or console.

Simplify a complex formula to a fixed position, then restore it after confirming scope. For An+B, write the generated positions. Check whether a framework inserts comments, fragments, wrappers, or portals; comments do not count, but wrapper elements do. Inspect rendered output rather than relying only on template source.

Performance Considerations

Structural pseudo-classes are normal, optimized selector features and are suitable for everyday interfaces. Dynamic insertion can require style recalculation for affected siblings because positions change, but component-sized collections rarely present a concern. Large, rapidly mutating lists should be measured with browser performance tools rather than optimized based on assumptions.

Virtualization, smaller DOM trees, and efficient rendering usually matter more than replacing a clear nth-child rule. Choose selectors for correctness and maintainability first, then profile demonstrated bottlenecks.

Related :last-child and :only-child Selectors

The :last-child pseudo-class matches an element when it is the final element child of its parent. It is useful for removing a trailing divider, margin, or border. Like first-child, it tests absolute position rather than element type. A selector such as p:last-child fails when another element follows the paragraph, even if that paragraph is the last p.

The :only-child pseudo-class matches when an element has no element siblings, making it both first-child and last-child. A single result card can receive a centered layout, or a lone navigation action can avoid group separators. These named selectors communicate intent more clearly than combining first-child and last-child manually. They still respond dynamically when siblings are inserted or removed, and text nodes do not prevent an element from being an only child.

Best Practices

Use structural pseudo-classes when position is a stable part of presentation. Scope them to a clear parent, prefer direct-child combinators for owned collections, and use named forms such as first-child when they express intent better than formulas. Write out An+B sequences during review and document non-obvious ranges.

Use first-of-type or nth-of-type only when element type defines the group. Use nth-child of a selector list when classes or states should define the counting set. Do not infer business status from position, and test hidden items, dynamic insertion, responsive reordering, nested lists, and accessibility modes.

Interview Questions and Answers

What does first-child select? An element that is the first element child of its parent. Does p:first-child mean the first paragraph? Only if that paragraph is child position one. Where does nth-child counting begin? At one. Do text nodes count? No, only element siblings. Do hidden elements count? Yes, unless the of selector list explicitly excludes them.

What does 2n+1 match? Odd positions. How do you select the first three children? Use :nth-child(-n+3). What is the difference between nth-child and nth-of-type? Nth-child counts all element siblings, while nth-of-type counts siblings sharing the same element type. Does visual flex order affect counting? No; DOM order determines structural position.

Practical Review Checklist

  • Identify the immediate parent and list its element children in DOM order.
  • Remember that child positions are one-based.
  • Count all element siblings unless an of selector list filters them.
  • Ignore whitespace, comments, text nodes, and generated pseudo-elements when counting.
  • Distinguish first-child from first-of-type and nth-child from nth-of-type.
  • Write the first several results of every An+B formula.
  • Account for hidden elements, inserted wrappers, and dynamic updates.
  • Do not confuse DOM order with flex or grid visual order.
  • Keep business meaning in classes, attributes, and data rather than incidental position.
  • Test nested collections, responsive layouts, contrast, and keyboard reading order.

Key Takeaway

First-child and nth-child select according to an element's position among siblings under one parent. First-child requires absolute position one. Nth-child accepts a fixed index, odd or even keyword, or an An+B sequence, while the modern of syntax can restrict which siblings participate in counting. Text and generated content do not count, but hidden elements and unrelated element types normally do.

Reliable use begins with the DOM tree, not the visual layout. Scope selectors to stable collections, choose type-based variants only when element names define the group, and avoid attaching domain meaning to accidental order. With those rules, structural pseudo-classes provide concise styling without extra presentation classes while remaining understandable as content changes.