CSS-First UI Patterns · Content · 16

Content Filter Without JavaScript

Filter chips that show only the matching cards — built with radio buttons and the CSS :has() selector. It works with keyboard and touch, counts the results with a CSS counter, and keeps every item in the HTML.

JavaScript
0 KB
Built with
:has(), :checked, [data-topic~=…], CSS counters
Browsers
All current browsers (:has() since 2023)
Updated
Can you filter content without JavaScript?

Yes. Radio buttons store the chosen category, and a rule like .filter:has(#web:checked) li:not([data-topic~="web"]) { display: none } hides everything that doesn't match. For a short list on one page, that is all it takes. For large catalogues that people search for by category, give each filter its own URL instead.

Live demo

Ten of our free tools, filtered by topic. Click a chip, or Tab to the chips and use the arrow keys. Some tools belong to two topics. The count below the list updates by itself — with a CSS counter.

Live demo 0 KB JavaScript Click · Tab · ← →

How the CSS filter works

  1. Radio buttons hold the choice

    All four share name="ui-topic", so exactly one is checked. Their labels are styled as chips; the radios are visually hidden but stay reachable by keyboard.

  2. Topics in a data attribute

    Each item lists its topics: data-topic="baidu content". The ~= selector matches one word in that list, so an item can belong to several topics.

  3. :has() connects them

    .ui-filter:has(#ui-f-web:checked) is true while “Websites” is chosen. Combined with li:not([data-topic~="web"]), it hides every other item.

  4. One line per topic

    Adding a topic means one more radio, one more label and one more selector line. “All” needs no rule — nothing is hidden.

  5. A count from a CSS counter

    Each list item increments a counter. Hidden items (display: none) don't count, so the number after “Showing” is always right.

  6. A fieldset and legend

    They group the chips and name the group: screen readers announce “Show tools for, Websites, radio button, 2 of 4”.

Copy the HTML and CSS

Exactly the code running in the demo. All class names start with ui-. The legend and count are styled for the dark demo background — adjust their colours for yours.

filter.html
<div class="ui-filter">
  <fieldset class="ui-filter__chips">
    <legend>Show tools for</legend>
    <input type="radio" name="ui-topic" id="ui-f-all" checked>
    <label for="ui-f-all">All</label>
    <input type="radio" name="ui-topic" id="ui-f-web">
    <label for="ui-f-web">Websites</label>
    <input type="radio" name="ui-topic" id="ui-f-baidu">
    <label for="ui-f-baidu">Baidu</label>
    <input type="radio" name="ui-topic" id="ui-f-content">
    <label for="ui-f-content">Content</label>
  </fieldset>

  <ul class="ui-filter__list">
    <li data-topic="web"><a href="/tools/chinese-filler-text/">Chinese Dummy Text</a></li>
    <li data-topic="web"><a href="/tools/chinese-font-stack-generator/">Chinese Font Stack</a></li>
    <li data-topic="web"><a href="/tools/chinese-font-subset/">Chinese Font Subset</a></li>
    <li data-topic="web baidu"><a href="/tools/pinyin-slug-generator/">Pinyin URL Slugs</a></li>
    <li data-topic="baidu"><a href="/tools/baidu-serp-preview/">Baidu SERP Preview</a></li>
    <li data-topic="baidu content"><a href="/tools/chinese-seo-editor/">Chinese SEO Editor</a></li>
    <li data-topic="baidu content"><a href="/tools/chinese-word-segmentation/">Word Segmentation</a></li>
    <li data-topic="baidu"><a href="/tools/baidu-keyword-combiner/">Baidu Ads Keywords</a></li>
    <li data-topic="content"><a href="/tools/detect-chinese/">Simplified or Traditional?</a></li>
    <li data-topic="content web"><a href="/tools/chinese-typography-checker/">Typography Checker</a></li>
  </ul>

  <p class="ui-filter__count">Showing <span class="ui-filter__number"></span> of 10 tools</p>
</div>
filter.css
/* Filter — no JavaScript.
   Radio buttons hold the chosen topic; :has() hides the items
   that don't match. All items stay in the HTML. */
.ui-filter {
  --ui-text: #1b1f3b;
  --ui-muted: #5d6280;
  --ui-accent: #4f46e5;
  --ui-chip: #e6e8f5;
  font: 500 0.9375rem/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
  color: var(--ui-text);
}

/* Chips */
.ui-filter__chips {
  position: relative; /* keeps the hidden radios inside */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 1rem;
  padding: 0;
  border: 0;
}
.ui-filter__chips legend {
  float: left; /* puts the legend in the row of chips */
  margin-right: 0.5rem;
  padding: 0;
  font-weight: 600;
  color: #fff;
}
.ui-filter__chips input {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.ui-filter__chips label {
  padding: 0.45rem 0.9rem;
  border-radius: 999px;
  background: var(--ui-chip);
  cursor: pointer;
}
.ui-filter__chips input:checked + label {
  background: var(--ui-accent);
  color: #fff;
}
.ui-filter__chips input:focus-visible + label {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* Items */
.ui-filter__list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: 0.6rem;
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: ui-shown;
}
.ui-filter__list li {
  counter-increment: ui-shown; /* only visible items are counted */
}
.ui-filter__list a {
  display: block;
  padding: 0.8rem 1rem;
  border-radius: 10px;
  background: #fff;
  color: var(--ui-text);
  text-decoration: none;
}
.ui-filter__list a:hover {
  color: var(--ui-accent);
  text-decoration: underline;
}
.ui-filter__list a:focus-visible {
  outline: 2px solid var(--ui-accent);
  outline-offset: 2px;
}

/* The filter rules: one line per topic */
.ui-filter:has(#ui-f-web:checked) li:not([data-topic~="web"]),
.ui-filter:has(#ui-f-baidu:checked) li:not([data-topic~="baidu"]),
.ui-filter:has(#ui-f-content:checked) li:not([data-topic~="content"]) {
  display: none;
}

/* "Showing 4 of 10": the counter only counts visible items */
.ui-filter__count {
  margin: 0.9rem 0 0;
  color: rgb(255 255 255 / 0.75);
  font-size: 0.875rem;
}
.ui-filter__number::after {
  content: counter(ui-shown);
}

Filters and SEO: CSS filter or filter URLs?

A CSS filter keeps everything on one page. All items are in the HTML, so search engines see and follow every link — however the visitor filters. That is ideal for short lists: a team page, a resource list, ten tools.

But a filter on one URL has nothing of its own to rank. If people search for a category — “industrial pumps”, “Baidu tools” — that category needs its own page with its own URL, title and text. A CSS filter can't give it one. For shops and large catalogues, make important filters real links to category pages.

Not every filter combination deserves a URL. Faceted navigation — colour × size × price × brand — can create thousands of near-identical URLs that waste crawl budget. Index the combinations people actually search for, and keep the rest out of the index (for example with a canonical tag to the main category, or noindex). Baidu crawls far fewer pages of most foreign sites than Google, so this matters even more there.

Filters that load results with JavaScript leave crawlers with whatever the first view shows. Items reachable only through a scripted filter may never be found.

Accessibility and browser support

SituationWithout JSDetails
KeyboardWorksTab to the chips, arrow keys to switch, Tab again into the results.
Screen readersPartlyThe chips are a labelled radio group. The changed count is not announced — that needs a script and a live region.
Browser supportAll:has() works in all current browsers. Without it, all items stay visible.
Filter in the URLNoA CSS filter can't be bookmarked or shared. Use real filter links if that matters.
Several filters at oncePossibleCheckboxes instead of radios, with one :has() rule per combination — manageable for two or three, not for dozens.
When you still need JavaScript — or a server
  • Announcing the result count to screen readers when the filter changes.
  • Search-as-you-type over many items.
  • Large catalogues: filter on the server, with real URLs for the categories people search for.

Questions about CSS filters

Can you filter a list with only CSS?

Yes: radio buttons for the choice, :has() to react to it, and an attribute selector to hide non-matching items. All current browsers support it.

Can an item belong to several categories?

Yes. List them separated by spaces — data-topic="web baidu" — and match one with [data-topic~="web"].

Is a CSS filter good for SEO?

It keeps every item in the HTML, which is good. But the filtered views have no URLs, so they can't rank on their own. Categories people search for need real pages.

How do I show the number of results without JavaScript?

With a CSS counter: counter-increment on every item, and content: counter(name) in an element after the list. Hidden items don't count.

Should every filter combination have its own URL?

No — only those people search for. The rest create near-duplicate pages that waste crawl budget, especially on Baidu.

This FAQ is built without JavaScript — see the accordion pattern.

Category pages that rank in China

Selling on your own site or on Tmall and JD?

We plan category structures and filter URLs that Baidu can crawl and rank, and run marketplace placements on Tmall and JD for Western brands.

CSS-First UI Patterns are written and maintained by Jademond Digital, a Chinese digital marketing agency founded in Guangzhou in 2012. We build and audit websites for Baidu, Google and AI search — which is why we care what works without JavaScript. About us →