w3tweaks.com · CSS Tutorial

CSS Selectors You're Not Using

Playground, :has() real patterns, and hidden gems that replace JavaScript.

Tab 1

Click a Selector — See What It Matches

Each selector highlights the exact elements it matches in a live HTML tree. These are the selectors most developers know exist but never actually use.

li:nth-child(odd)
Zebra striping without classes
li:nth-last-child(2)
Second from the end
li:only-child
Only when no siblings exist
p + p
Adjacent sibling combinator
li ~ li
All siblings after first
div.card > p
Direct children only
[ data-status ]
Any element with this attribute
[ href^="https" ]
Attribute starts with value
[ href$=".pdf" ]
Attribute ends with value
li:not(.disabled)
Everything except this
:is(h1, h2, h3)
Any of these selectors
li:empty
Completely empty elements
Matched by li:nth-child(odd)
Selects every odd-numbered list item — 1st, 3rd, 5th. Great for zebra-striped tables and lists without adding classes.
Tab 2

:has() — 6 Real Patterns That Replace JavaScript

:has() is the CSS parent selector. These are the real-world patterns you've been writing JavaScript for — now done in pure CSS. Interact with each demo.

Style card that has an image
.card:has(img)
Card with image — highlighted ✓
Card without image — default style

The left card has an image — CSS detects this with :has(img) and applies a teal border and bolder text. No JavaScript class toggling needed.

Style label when input is focused
.group:has(input:focus)

Click an input — the label turns cyan. The parent .demo-form-group detects its child's focus via :has(input:focus) and changes the label color.

Toggle row style with checkbox
label:has(input:checked)

Checked rows turn green automatically. The label detects its own checkbox state via :has(input:checked) — replacing a JavaScript event listener.

Auto arrow on nav with children
li:has(.dropdown)::after
Home
Products
About
Resources
Contact

Nav items that have a .dropdown child automatically get a ▾ arrow via :has(.dropdown)::after. No JavaScript class detection needed.

Show empty state with :empty
ul:empty::after

    When the list has no children, :empty::after inserts the "No items" message. Add items to see it disappear. Pure CSS state — zero JavaScript for the UI.

    Quantity query — change layout by count
    .grid:has(:nth-child(3))
    1
    2

    Add a 3rd item — the grid switches to 3-column automatically via :has(:nth-child(3)). Layout changes based on content count with zero JavaScript.

    Tab 3

    Hidden Gems — Selectors Nobody Teaches

    These selectors exist in all modern browsers. Most developers have never used them. Interact with each demo.

    :focus-within
    🔥 Underused

    Style a parent when any of its children has focus — no JavaScript needed for form group highlighting.

    .form-group:focus-within {
      border-color: cyan;
    }
    .form-group:focus-within label {
      color: cyan;
    }
    :placeholder-shown
    🔥 Underused

    True when the placeholder is visible — i.e., the field is empty. Powers floating labels with zero JavaScript.

    /* Float label up when field has value */
    input:not(:placeholder-shown) ~ label {
      top: 8px;
      font-size: 10px;
    }
    :user-valid / :user-invalid
    ✨ New 2024

    Like :valid/:invalid but only activates after the user interacts. Avoids showing red errors on an untouched empty form.

    input:user-valid { border-color: green; }
    input:user-invalid { border-color: red; }
    /* :valid fires immediately — :user-valid waits */
    :nth-child(n of selector)
    ✨ New syntax

    Count only elements matching a selector. :nth-child(2 of .featured) selects the 2nd featured item — not the 2nd child that happens to be featured.

    Item 1
    Item 3
    Item 5
    .item:nth-child(2 of .featured) {
      outline: 2px solid cyan;
    }
    Attribute Selector Tricks
    🔥 Underused

    Auto-decorate links by type — no extra classes or JavaScript:

    a[href^="https"]::before { content: "🔒 "; }
    a[href$=".pdf"]::after { content: " 📄"; }
    a[href$=".zip"]::after { content: " 📦"; }
    a[target="_blank"]::after{ content: " ↗"; }
    ::marker & :is() DRY
    🔥 Underused

    Custom list bullets without images or list-style: none hacks. And use :is() to write DRY heading rules.

    • Custom marker color
    • Custom marker content
    • No list-style none needed
    li::marker {
      color: cyan;
      content: "▸ ";
    }
    /* DRY headings with :is() */
    :is(h1,h2,h3,h4) { line-height: 1.2; }
    Read the tutorial