Open the full-page example → · Download complete recipe (ZIP)
Try the working example
The preview runs only our supplied example code. Entries stay in the page; the practice form cannot send them. For keyboard testing, use the full-page example.
Build it step by step
- Keep every project in the HTML so the collection is readable without JavaScript. Store sortable values in data attributes.
- Copy the card list before sorting. Convert years to numbers and use a collator for titles.
- Apply the filter and append the existing cards in their new order. The document order now matches the visual order.
- Update the count without moving focus away from the controls. Test no matches, mixed-case input, equal years and reset.
Why it works
Filtering decides which cards are visible; sorting decides their order. Both are calculated from the same full collection each time, so a previously hidden card can return. A title tie-breaker makes equal years predictable.
A mistake worth catching
Sorting only the currently visible cards can produce inconsistent results after the filter changes. A CSS order property alone changes visual order without changing the reading order.
Read the complete source
The ZIP also includes the local font and its licence, a README and any illustrations. The source below is the same code used in the preview.
HTML source
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="Mario V.W.B.R. Obst"><meta name="robots" content="noindex, follow"><meta name="referrer" content="no-referrer">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; form-action 'none'; object-src 'none'; base-uri 'none'">
<title>Cards that filter and sort</title><link rel="stylesheet" href="style.css">
<script src="script.js" defer></script></head>
<body><main><h1>A shelf of small experiments.</h1><div id="controls" hidden><label for="query">Find a project</label><input id="query" type="search" autocomplete="off" placeholder="Try photo or code"><label for="order">Order</label><select id="order"><option value="title">Title A–Z</option><option value="newest">Newest first</option></select><button type="button" id="reset">Reset filter and order</button><p id="count" role="status" aria-atomic="true"></p></div><div class="cards" id="cards"><article class="panel" data-title="Window studies" data-year="2024" data-tags="photo city"><h2>Window studies</h2><p>Patterns found in everyday windows.</p><p>2024 · photo city</p></article><article class="panel" data-title="Tiny markup" data-year="2026" data-tags="code html"><h2>Tiny markup</h2><p>Experiments in readable page structure.</p><p>2026 · code html</p></article><article class="panel" data-title="Footpath notes" data-year="2025" data-tags="travel writing"><h2>Footpath notes</h2><p>Short observations from familiar routes.</p><p>2025 · travel writing</p></article><article class="panel" data-title="Colour walks" data-year="2026" data-tags="photo colour"><h2>Colour walks</h2><p>A neighbourhood recorded one colour at a time.</p><p>2026 · photo colour</p></article></div><noscript><p>All projects are shown. Filtering and sorting need JavaScript.</p></noscript></main></body></html>CSS source
@font-face{font-family:FYI;src:url("inter.ttf") format("truetype");font-weight:100 900;font-display:swap}
*{box-sizing:border-box}html{color-scheme:dark}body{margin:0;background:#20212a;color:#efedf6;font-family:FYI,sans-serif;line-height:1.6}main{max-width:960px;margin:auto;padding:24px}h1,h2{line-height:1.15}a{color:#ff9b45}button,input,select,textarea{font:inherit;color:inherit;background:#282934;border:1px solid #777582;border-radius:6px;padding:10px}button{cursor:pointer}button:disabled{cursor:default;opacity:.5}:focus-visible{outline:3px solid #ff9b45;outline-offset:4px}label{display:block;margin-top:16px}button{margin:12px 8px 12px 0}p{max-width:70ch}.hint{color:#c7c3d2} [hidden]{display:none!important}.panel{padding:20px;border:1px solid #555361;border-radius:12px;background:#282934}img{max-width:100%;height:auto}code{overflow-wrap:anywhere}input,select,textarea{max-width:100%}a,button{touch-action:manipulation}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,230px),1fr));gap:16px}JavaScript source
const container = document.querySelector('#cards');
const cards = [...container.querySelectorAll('[data-title]')];
const query = document.querySelector('#query');
const order = document.querySelector('#order');
const count = document.querySelector('#count');
const collator = new Intl.Collator('en', { sensitivity: 'base', numeric: true });
function update() {
const term = query.value.trim().toLocaleLowerCase('en');
const sorted = [...cards].sort((a, b) => {
const alphabetical = collator.compare(a.dataset.title, b.dataset.title);
return order.value === 'newest' ? Number(b.dataset.year) - Number(a.dataset.year) || alphabetical : alphabetical;
});
let visible = 0;
for (const card of sorted) {
const text = `${card.textContent} ${card.dataset.tags}`.toLocaleLowerCase('en');
card.hidden = !text.includes(term);
if (!card.hidden) visible++;
container.append(card);
}
count.textContent = visible ? `${visible} of ${cards.length} projects shown. ${order.value === 'newest' ? 'Newest first.' : 'Title A–Z.'}` : 'No matches. Clear the filter or try another word.';
}
query.addEventListener('input', update);
order.addEventListener('change', update);
document.querySelector('#reset').addEventListener('click', () => {
query.value = '';
order.value = 'title';
update();
query.focus();
});
document.querySelector('#controls').hidden = false;
update();
Take it one step further
Add an oldest-first option. Give two projects the same year and explain how the title tie-breaker affects their order.
Check your work
Try a narrow window, 200% zoom and keyboard-only operation. Disable JavaScript to inspect the fallback. For motion, test your reduced-motion preference. Do not use real personal details in learning forms.
Keep exploring
Read the companion guide · Practise finding small mistakes · All code recipes
Technical reference and authorship
Read the underlying platform documentation (new tab). The explanation, composition and example were written for this site; this link is a factual reference, not a source of a copied template. Inter has its own font licence in the download.
Use and adapt the original example for your own learning. For other reuse, see Legal information. About AI assistance.