Job Board — Filter Rail
A faceted job-search filter rail with collapsible groups for role, job type, experience and remote mode, plus a minimum-salary slider, a location input that turns cities into removable chips, and segmented controls. Every interaction updates a live results count and a row of active-filter chips with one-tap removal and clear-all. On narrow screens the rail becomes an accessible slide-in drawer, and job cards carry salary, location and remote badges with a bookmark toggle.
MCP
Code
:root {
--brand: #2563eb;
--brand-d: #1d4ed8;
--brand-50: #eaf1ff;
--ink: #0f172a;
--ink-2: #475569;
--muted: #64748b;
--bg: #f6f8fb;
--surface: #ffffff;
--line: rgba(15, 23, 42, 0.1);
--line-2: rgba(15, 23, 42, 0.18);
--ok: #16a34a;
--warn: #d97706;
--danger: #dc2626;
--new: #2563eb;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 1px 2px rgba(15, 23, 42, 0.06);
--sh-md: 0 6px 20px rgba(15, 23, 42, 0.08);
--sh-lg: 0 18px 50px rgba(15, 23, 42, 0.16);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
font-size: 15px;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 { margin: 0; }
button { font-family: inherit; cursor: pointer; }
input, select { font-family: inherit; }
.skip-link {
position: absolute;
left: -999px;
top: 8px;
z-index: 100;
background: var(--ink);
color: #fff;
padding: 8px 14px;
border-radius: var(--r-sm);
}
.skip-link:focus { left: 12px; }
/* ===== Topbar ===== */
.topbar {
position: sticky;
top: 0;
z-index: 30;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.topbar__inner {
max-width: 1180px;
margin: 0 auto;
display: flex;
align-items: center;
gap: 16px;
padding: 14px 20px;
}
.brand { display: flex; align-items: center; gap: 9px; font-weight: 700; font-size: 17px; }
.brand__mark { color: var(--brand); font-size: 18px; }
.brand__name b { color: var(--brand); font-weight: 800; }
.searchbox {
flex: 1;
display: flex;
align-items: center;
gap: 9px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 0 14px;
height: 44px;
box-shadow: var(--sh-sm);
transition: border-color .15s, box-shadow .15s;
}
.searchbox:focus-within { border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-50); }
.searchbox__icon { width: 18px; height: 18px; color: var(--muted); flex: none; }
.searchbox input {
border: 0; outline: none; background: none; width: 100%;
font-size: 14.5px; color: var(--ink);
}
.filters-toggle {
display: none;
align-items: center;
gap: 8px;
height: 44px;
padding: 0 14px;
border: 1px solid var(--line-2);
background: var(--surface);
border-radius: var(--r-md);
font-weight: 600;
color: var(--ink);
}
.filters-toggle svg { width: 18px; height: 18px; }
.filters-toggle__count {
min-width: 20px; height: 20px; padding: 0 5px;
display: grid; place-items: center;
background: var(--brand); color: #fff;
font-size: 12px; font-weight: 700; border-radius: 999px;
}
.filters-toggle__count[data-empty="true"] { display: none; }
/* ===== Layout ===== */
.layout {
max-width: 1180px;
margin: 0 auto;
display: grid;
grid-template-columns: 286px 1fr;
gap: 24px;
padding: 24px 20px 64px;
align-items: start;
}
/* ===== Rail ===== */
.rail {
position: sticky;
top: 88px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-md);
overflow: hidden;
}
.rail__head { display: none; }
.rail__body { padding: 6px; }
.fgroup { border-bottom: 1px solid var(--line); }
.fgroup:last-child { border-bottom: 0; }
.fgroup__head {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 12px;
border: 0;
background: none;
font-size: 13.5px;
font-weight: 700;
color: var(--ink);
letter-spacing: .01em;
}
.fgroup__head .chev { width: 18px; height: 18px; color: var(--muted); transition: transform .2s; }
.fgroup[data-open="false"] .chev { transform: rotate(-90deg); }
.fgroup__body {
padding: 0 12px 14px;
display: grid;
gap: 2px;
}
.fgroup[data-open="false"] .fgroup__body { display: none; }
.check {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 8px;
border-radius: var(--r-sm);
cursor: pointer;
font-size: 14px;
color: var(--ink-2);
transition: background .12s;
}
.check:hover { background: var(--bg); }
.check input { width: 17px; height: 17px; accent-color: var(--brand); flex: none; }
.check span { flex: 1; }
.check em { font-style: normal; font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.check input:checked ~ span { color: var(--ink); font-weight: 600; }
/* segmented radios */
.seg { grid-template-columns: 1fr 1fr; gap: 8px; }
.radio { position: relative; }
.radio input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.radio span {
display: block;
text-align: center;
padding: 9px 6px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
font-size: 13.5px;
font-weight: 600;
color: var(--ink-2);
transition: all .14s;
}
.radio input:hover ~ span { border-color: var(--brand); }
.radio input:checked ~ span {
background: var(--brand-50);
border-color: var(--brand);
color: var(--brand-d);
}
.radio input:focus-visible ~ span { box-shadow: 0 0 0 3px var(--brand-50); }
/* salary */
.salary__out {
display: inline-block;
font-weight: 800;
font-size: 18px;
color: var(--brand-d);
margin-bottom: 8px;
font-variant-numeric: tabular-nums;
}
.salary input[type="range"] { width: 100%; accent-color: var(--brand); margin: 4px 0; }
.salary__scale {
display: flex;
justify-content: space-between;
font-size: 11.5px;
color: var(--muted);
margin-top: 2px;
}
/* location */
.locinput { display: flex; gap: 8px; }
.locinput input {
flex: 1;
height: 40px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 0 12px;
font-size: 14px;
outline: none;
}
.locinput input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-50); }
.locinput__btn {
height: 40px;
padding: 0 14px;
border: 0;
background: var(--ink);
color: #fff;
border-radius: var(--r-sm);
font-weight: 600;
font-size: 13.5px;
}
.locinput__btn:hover { background: #1e293b; }
.locchips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.locchips:empty { display: none; }
.locchip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 8px 5px 11px;
background: var(--brand-50);
color: var(--brand-d);
border-radius: 999px;
font-size: 12.5px;
font-weight: 600;
}
.locchip button {
border: 0; background: none; color: inherit; cursor: pointer;
width: 16px; height: 16px; display: grid; place-items: center;
border-radius: 50%; font-size: 13px; line-height: 1;
}
.locchip button:hover { background: rgba(37, 99, 235, 0.18); }
.locsugg { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.locsugg button {
border: 1px dashed var(--line-2);
background: none;
border-radius: 999px;
padding: 4px 11px;
font-size: 12.5px;
color: var(--ink-2);
font-weight: 500;
}
.locsugg button:hover { border-color: var(--brand); color: var(--brand-d); }
.locsugg button[hidden] { display: none; }
.rail__foot {
display: none;
gap: 10px;
padding: 14px;
border-top: 1px solid var(--line);
}
/* buttons */
.btn {
height: 44px;
border-radius: var(--r-md);
font-weight: 700;
font-size: 14.5px;
border: 1px solid transparent;
padding: 0 16px;
}
.btn--primary { flex: 1; background: var(--brand); color: #fff; }
.btn--primary:hover { background: var(--brand-d); }
.btn--ghost { background: var(--surface); border-color: var(--line-2); color: var(--ink); }
.btn--ghost:hover { background: var(--bg); }
/* ===== Results ===== */
.results__bar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}
.results__count { font-size: 22px; font-weight: 800; letter-spacing: -.02em; }
.results__count span { color: var(--brand-d); }
.results__sub { margin: 2px 0 0; color: var(--muted); font-size: 13.5px; }
.sort { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); font-weight: 600; }
.sort select {
height: 40px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface);
padding: 0 12px;
font-size: 13.5px;
color: var(--ink);
font-weight: 600;
outline: none;
}
.sort select:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-50); }
.activebar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
padding: 12px 14px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
margin-bottom: 16px;
box-shadow: var(--sh-sm);
}
.activebar__chips { display: flex; flex-wrap: wrap; gap: 8px; flex: 1; }
.achip {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 5px 8px 5px 12px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
font-size: 12.5px;
font-weight: 600;
color: var(--ink);
}
.achip b { color: var(--muted); font-weight: 600; }
.achip button {
border: 0; background: none; cursor: pointer; color: var(--muted);
width: 17px; height: 17px; display: grid; place-items: center;
border-radius: 50%; font-size: 13px; line-height: 1;
}
.achip button:hover { background: var(--line-2); color: var(--ink); }
.activebar__clear {
border: 0; background: none; color: var(--brand-d);
font-weight: 700; font-size: 13px; padding: 4px 6px; border-radius: var(--r-sm);
}
.activebar__clear:hover { background: var(--brand-50); }
/* ===== Job list ===== */
.joblist { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.jobcard {
display: grid;
grid-template-columns: 48px 1fr auto;
gap: 14px;
padding: 16px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-sm);
transition: border-color .15s, box-shadow .15s, transform .15s;
}
.jobcard:hover { border-color: var(--line-2); box-shadow: var(--sh-md); transform: translateY(-1px); }
.logo {
width: 48px; height: 48px;
border-radius: 12px;
display: grid; place-items: center;
font-weight: 800; font-size: 18px; color: #fff;
}
.job__main { min-width: 0; }
.job__title {
font-size: 16px; font-weight: 700; color: var(--ink);
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.tag-new {
font-size: 10.5px; font-weight: 800; letter-spacing: .04em;
color: var(--new); background: var(--brand-50);
padding: 2px 7px; border-radius: 999px; text-transform: uppercase;
}
.job__company { color: var(--ink-2); font-size: 13.5px; margin-top: 2px; }
.chips { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 11px; }
.chip {
display: inline-flex; align-items: center; gap: 5px;
font-size: 12px; font-weight: 600;
padding: 4px 9px; border-radius: 999px;
background: var(--bg); color: var(--ink-2);
border: 1px solid var(--line);
}
.chip--salary { color: var(--ok); background: #ecfdf3; border-color: rgba(22,163,74,.18); }
.chip--remote { color: var(--brand-d); background: var(--brand-50); border-color: rgba(37,99,235,.18); }
.chip svg { width: 13px; height: 13px; }
.job__side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 10px; }
.job__age { font-size: 12px; color: var(--muted); white-space: nowrap; }
.save {
width: 36px; height: 36px;
border: 1px solid var(--line-2);
border-radius: 10px;
background: var(--surface);
display: grid; place-items: center;
color: var(--muted);
transition: all .14s;
}
.save svg { width: 18px; height: 18px; }
.save:hover { border-color: var(--brand); color: var(--brand); }
.save[aria-pressed="true"] { background: var(--brand-50); border-color: var(--brand); color: var(--brand-d); }
.save[aria-pressed="true"] svg { fill: currentColor; }
/* empty */
.empty {
text-align: center;
padding: 56px 20px;
background: var(--surface);
border: 1px dashed var(--line-2);
border-radius: var(--r-lg);
}
.empty__icon { font-size: 34px; }
.empty h3 { margin: 12px 0 4px; font-size: 18px; }
.empty p { color: var(--muted); margin: 0 0 18px; }
.empty .btn { display: inline-flex; align-items: center; }
/* ===== Toast ===== */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
background: var(--ink);
color: #fff;
padding: 11px 18px;
border-radius: 999px;
font-size: 13.5px;
font-weight: 600;
box-shadow: var(--sh-lg);
opacity: 0;
pointer-events: none;
transition: opacity .22s, transform .22s;
z-index: 60;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ===== Drawer / mobile ===== */
.drawer-scrim {
position: fixed; inset: 0;
background: rgba(15, 23, 42, 0.45);
z-index: 40;
opacity: 0;
transition: opacity .22s;
}
.drawer-scrim.show { opacity: 1; }
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; }
.filters-toggle { display: inline-flex; }
.rail {
position: fixed;
top: 0; left: 0; bottom: 0;
width: min(340px, 88vw);
border-radius: 0 var(--r-lg) var(--r-lg) 0;
transform: translateX(-104%);
transition: transform .26s cubic-bezier(.4,0,.2,1);
z-index: 50;
display: flex;
flex-direction: column;
}
.rail.open { transform: none; }
.rail__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 18px;
border-bottom: 1px solid var(--line);
}
.rail__title { font-size: 17px; font-weight: 800; }
.rail__close {
width: 36px; height: 36px; border: 1px solid var(--line);
border-radius: 10px; background: var(--surface); font-size: 15px; color: var(--ink-2);
}
.rail__body { flex: 1; overflow-y: auto; }
.rail__foot { display: flex; }
}
@media (max-width: 520px) {
.topbar__inner { flex-wrap: wrap; gap: 10px; padding: 12px 14px; }
.searchbox { order: 3; flex-basis: 100%; }
.layout { padding: 16px 14px 48px; gap: 16px; }
.jobcard { grid-template-columns: 40px 1fr; }
.logo { width: 40px; height: 40px; font-size: 16px; }
.job__side {
grid-column: 1 / -1;
flex-direction: row;
align-items: center;
border-top: 1px solid var(--line);
padding-top: 12px;
margin-top: 4px;
}
.results__bar { flex-direction: column; align-items: flex-start; gap: 8px; }
.results__count { font-size: 20px; }
}(function () {
"use strict";
// ---------- Fictional job data ----------
var JOBS = [
{ id: 1, title: "Senior Frontend Engineer", company: "Lumen Labs", color: "#2563eb", role: "Engineering", type: "Full-time", exp: "Senior", remote: "Remote", salary: 165, loc: "San Francisco", days: 1, isNew: true },
{ id: 2, title: "Product Designer", company: "Maple & Co", color: "#9333ea", role: "Design", type: "Full-time", exp: "Mid", remote: "Hybrid", salary: 120, loc: "Berlin", days: 2, isNew: true },
{ id: 3, title: "Data Engineer", company: "Driftwood AI", color: "#0891b2", role: "Data", type: "Full-time", exp: "Senior", remote: "Remote", salary: 150, loc: "Austin", days: 4 },
{ id: 4, title: "Growth Marketing Lead", company: "Brightpath", color: "#d97706", role: "Marketing", type: "Full-time", exp: "Lead", remote: "On-site", salary: 135, loc: "London", days: 3 },
{ id: 5, title: "Junior Backend Developer", company: "Northgate", color: "#16a34a", role: "Engineering", type: "Full-time", exp: "Junior", remote: "Hybrid", salary: 85, loc: "Austin", days: 6 },
{ id: 6, title: "UX Researcher", company: "Maple & Co", color: "#9333ea", role: "Design", type: "Contract", exp: "Mid", remote: "Remote", salary: 95, loc: "Berlin", days: 5 },
{ id: 7, title: "Staff Platform Engineer", company: "Lumen Labs", color: "#2563eb", role: "Engineering", type: "Full-time", exp: "Lead", remote: "Remote", salary: 205, loc: "San Francisco", days: 8 },
{ id: 8, title: "Product Manager", company: "Driftwood AI", color: "#0891b2", role: "Product", type: "Full-time", exp: "Senior", remote: "Hybrid", salary: 158, loc: "London", days: 2, isNew: true },
{ id: 9, title: "Data Analyst Intern", company: "Brightpath", color: "#d97706", role: "Data", type: "Internship", exp: "Junior", remote: "On-site", salary: 55, loc: "Austin", days: 9 },
{ id: 10, title: "Senior UI Engineer", company: "Northgate", color: "#16a34a", role: "Engineering", type: "Full-time", exp: "Senior", remote: "Remote", salary: 170, loc: "Remote", days: 7 },
{ id: 11, title: "Content Marketing Specialist", company: "Maple & Co", color: "#9333ea", role: "Marketing", type: "Part-time", exp: "Mid", remote: "Remote", salary: 70, loc: "Berlin", days: 11 },
{ id: 12, title: "Machine Learning Engineer", company: "Driftwood AI", color: "#0891b2", role: "Data", type: "Full-time", exp: "Senior", remote: "Hybrid", salary: 190, loc: "San Francisco", days: 1, isNew: true },
{ id: 13, title: "Associate Product Designer", company: "Brightpath", color: "#d97706", role: "Design", type: "Full-time", exp: "Junior", remote: "Hybrid", salary: 88, loc: "London", days: 5 },
{ id: 14, title: "Backend Engineer (Contract)", company: "Lumen Labs", color: "#2563eb", role: "Engineering", type: "Contract", exp: "Mid", remote: "Remote", salary: 130, loc: "Remote", days: 10 },
{ id: 15, title: "Lead Product Manager", company: "Northgate", color: "#16a34a", role: "Product", type: "Full-time", exp: "Lead", remote: "On-site", salary: 180, loc: "Austin", days: 4 },
];
// ---------- State ----------
var state = {
keyword: "",
role: [], type: [], exp: [],
remote: "any",
salary: 0,
locations: [],
sort: "relevance",
saved: {},
};
// ---------- DOM ----------
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
var form = $("#filterForm");
var jobList = $("#jobList");
var countEl = $("#count");
var subEl = $("#sub");
var activeBar = $("#activeBar");
var activeChips = $("#activeChips");
var emptyEl = $("#empty");
var toggleCount = $("#toggleCount");
var rail = $("#rail");
var scrim = $("#scrim");
// ---------- Toast ----------
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2000);
}
// ---------- Filtering ----------
function matches(job) {
var k = state.keyword.trim().toLowerCase();
if (k) {
var hay = (job.title + " " + job.company + " " + job.role).toLowerCase();
if (hay.indexOf(k) === -1) return false;
}
if (state.role.length && state.role.indexOf(job.role) === -1) return false;
if (state.type.length && state.type.indexOf(job.type) === -1) return false;
if (state.exp.length && state.exp.indexOf(job.exp) === -1) return false;
if (state.remote !== "any" && job.remote !== state.remote) return false;
if (state.salary > 0 && job.salary < state.salary) return false;
if (state.locations.length) {
var hit = state.locations.some(function (l) {
return job.loc.toLowerCase() === l.toLowerCase();
});
if (!hit) return false;
}
return true;
}
function sortJobs(list) {
var arr = list.slice();
if (state.sort === "salary") arr.sort(function (a, b) { return b.salary - a.salary; });
else if (state.sort === "recent") arr.sort(function (a, b) { return a.days - b.days; });
else arr.sort(function (a, b) { return (b.isNew ? 1 : 0) - (a.isNew ? 1 : 0) || a.days - b.days; });
return arr;
}
// ---------- Render ----------
function icon(name) {
var p = {
pin: '<path d="M12 21s7-6.2 7-11a7 7 0 1 0-14 0c0 4.8 7 11 7 11z" fill="none" stroke="currentColor" stroke-width="1.7"/><circle cx="12" cy="10" r="2.4" fill="none" stroke="currentColor" stroke-width="1.7"/>',
cash: '<path d="M3 6h18v12H3z" fill="none" stroke="currentColor" stroke-width="1.7"/><circle cx="12" cy="12" r="2.5" fill="none" stroke="currentColor" stroke-width="1.7"/>',
clock: '<circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M12 8v4l2.5 1.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>',
wifi: '<path d="M5 12.5a10 10 0 0 1 14 0M8 15.5a5.5 5.5 0 0 1 8 0" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><circle cx="12" cy="18.5" r="1.2" fill="currentColor"/>',
};
return '<svg viewBox="0 0 24 24" aria-hidden="true">' + p[name] + "</svg>";
}
function initials(name) {
return name.split(" ").map(function (w) { return w[0]; }).join("").slice(0, 2).toUpperCase();
}
function render() {
var filtered = JOBS.filter(matches);
var sorted = sortJobs(filtered);
countEl.textContent = sorted.length;
var n = activeCount();
subEl.textContent = n
? n + " filter" + (n === 1 ? "" : "s") + " applied"
: "Showing all open roles";
jobList.innerHTML = "";
sorted.forEach(function (job) {
var li = document.createElement("li");
var isSaved = !!state.saved[job.id];
li.className = "jobcard";
li.innerHTML =
'<div class="logo" style="background:' + job.color + '">' + initials(job.company) + "</div>" +
'<div class="job__main">' +
'<div class="job__title">' + esc(job.title) +
(job.isNew ? '<span class="tag-new">New</span>' : "") +
"</div>" +
'<div class="job__company">' + esc(job.company) + " · " + esc(job.exp) + "-level</div>" +
'<div class="chips">' +
'<span class="chip chip--salary">' + icon("cash") + "$" + job.salary + "k</span>" +
'<span class="chip">' + icon("pin") + esc(job.loc) + "</span>" +
'<span class="chip chip--remote">' + icon("wifi") + esc(job.remote) + "</span>" +
'<span class="chip">' + esc(job.type) + "</span>" +
"</div>" +
"</div>" +
'<div class="job__side">' +
'<button class="save" type="button" aria-pressed="' + isSaved + '" aria-label="Save ' + esc(job.title) + '" data-save="' + job.id + '">' +
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 4h12v16l-6-4-6 4z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>' +
"</button>" +
'<span class="job__age">' + icon("clock") + " " + (job.days === 1 ? "1 day" : job.days + " days") + " ago</span>" +
"</div>";
jobList.appendChild(li);
});
emptyEl.hidden = sorted.length !== 0;
jobList.hidden = sorted.length === 0;
renderActiveChips();
toggleCount.textContent = n;
toggleCount.setAttribute("data-empty", n === 0);
}
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
// ---------- Active filter chips ----------
function activeChipDefs() {
var defs = [];
if (state.keyword.trim()) defs.push({ label: "Search", val: state.keyword.trim(), kind: "keyword" });
state.role.forEach(function (v) { defs.push({ label: "Role", val: v, kind: "role" }); });
state.type.forEach(function (v) { defs.push({ label: "Type", val: v, kind: "type" }); });
state.exp.forEach(function (v) { defs.push({ label: "Level", val: v, kind: "exp" }); });
if (state.remote !== "any") defs.push({ label: "Mode", val: state.remote, kind: "remote" });
if (state.salary > 0) defs.push({ label: "Min", val: "$" + state.salary + "k", kind: "salary" });
state.locations.forEach(function (v) { defs.push({ label: "City", val: v, kind: "loc" }); });
return defs;
}
function activeCount() { return activeChipDefs().length; }
function renderActiveChips() {
var defs = activeChipDefs();
activeBar.hidden = defs.length === 0;
activeChips.innerHTML = "";
defs.forEach(function (d) {
var chip = document.createElement("span");
chip.className = "achip";
chip.innerHTML = "<b>" + d.label + ":</b> " + esc(d.val) +
'<button type="button" aria-label="Remove ' + esc(d.label + " " + d.val) + '">✕</button>';
chip.querySelector("button").addEventListener("click", function () {
removeFilter(d.kind, d.val);
});
activeChips.appendChild(chip);
});
}
function removeFilter(kind, val) {
if (kind === "keyword") { state.keyword = ""; $("#keyword").value = ""; }
else if (kind === "remote") { state.remote = "any"; $('input[name="remote"][value="any"]').checked = true; }
else if (kind === "salary") { state.salary = 0; $("#salary").value = 0; updateSalaryOut(); }
else if (kind === "loc") {
state.locations = state.locations.filter(function (l) { return l !== val; });
renderLocChips();
} else {
var arr = state[kind];
var i = arr.indexOf(val);
if (i > -1) arr.splice(i, 1);
var cb = $('input[name="' + kind + '"][value="' + cssEsc(val) + '"]');
if (cb) cb.checked = false;
}
render();
}
function cssEsc(v) { return v.replace(/"/g, '\\"'); }
// ---------- Salary ----------
var salary = $("#salary");
var salaryOut = $("#salaryOut");
function updateSalaryOut() {
var v = +salary.value;
salaryOut.textContent = v >= 220 ? "$220k+" : "$" + v + "k+";
}
salary.addEventListener("input", function () {
state.salary = +salary.value;
updateSalaryOut();
render();
});
// ---------- Checkboxes & radios ----------
form.addEventListener("change", function (e) {
var t = e.target;
if (t.type === "checkbox") {
var name = t.name;
if (!state[name]) return;
if (t.checked) state[name].push(t.value);
else state[name] = state[name].filter(function (v) { return v !== t.value; });
render();
} else if (t.type === "radio" && t.name === "remote") {
state.remote = t.value;
render();
}
});
// ---------- Keyword ----------
$("#keyword").addEventListener("input", function (e) {
state.keyword = e.target.value;
render();
});
// ---------- Sort ----------
$("#sort").addEventListener("change", function (e) {
state.sort = e.target.value;
render();
});
// ---------- Location ----------
var locField = $("#locField");
var locChips = $("#locChips");
function addLocation(raw) {
var v = raw.trim();
if (!v) return;
var exists = state.locations.some(function (l) { return l.toLowerCase() === v.toLowerCase(); });
if (exists) { toast(v + " already added"); return; }
state.locations.push(v);
renderLocChips();
render();
}
function renderLocChips() {
locChips.innerHTML = "";
state.locations.forEach(function (loc) {
var c = document.createElement("span");
c.className = "locchip";
c.innerHTML = esc(loc) + '<button type="button" aria-label="Remove ' + esc(loc) + '">✕</button>';
c.querySelector("button").addEventListener("click", function () {
state.locations = state.locations.filter(function (l) { return l !== loc; });
renderLocChips();
render();
});
locChips.appendChild(c);
});
// hide suggestion buttons already chosen
$$(".locsugg button").forEach(function (b) {
var chosen = state.locations.some(function (l) { return l.toLowerCase() === b.dataset.loc.toLowerCase(); });
b.hidden = chosen;
});
}
locField.addEventListener("keydown", function (e) {
if (e.key === "Enter") { e.preventDefault(); addLocation(locField.value); locField.value = ""; }
});
$("#locAdd").addEventListener("click", function () { addLocation(locField.value); locField.value = ""; });
$$(".locsugg button").forEach(function (b) {
b.addEventListener("click", function () { addLocation(b.dataset.loc); });
});
// ---------- Collapsible groups ----------
$$(".fgroup__head").forEach(function (head) {
head.addEventListener("click", function () {
var group = head.closest(".fgroup");
var open = group.getAttribute("data-open") === "true";
group.setAttribute("data-open", String(!open));
head.setAttribute("aria-expanded", String(!open));
});
});
// ---------- Save toggle (delegated) ----------
jobList.addEventListener("click", function (e) {
var btn = e.target.closest("[data-save]");
if (!btn) return;
var id = +btn.dataset.save;
state.saved[id] = !state.saved[id];
btn.setAttribute("aria-pressed", String(state.saved[id]));
toast(state.saved[id] ? "Saved to your shortlist" : "Removed from shortlist");
});
// ---------- Clear all ----------
function clearAll() {
state.keyword = ""; state.role = []; state.type = []; state.exp = [];
state.remote = "any"; state.salary = 0; state.locations = [];
form.reset();
$("#keyword").value = "";
$('input[name="remote"][value="any"]').checked = true;
updateSalaryOut();
renderLocChips();
render();
toast("Filters cleared");
}
$("#clearAll").addEventListener("click", clearAll);
$("#clearAllRail").addEventListener("click", clearAll);
$("#emptyClear").addEventListener("click", clearAll);
// ---------- Drawer ----------
function openDrawer() {
rail.classList.add("open");
scrim.hidden = false;
requestAnimationFrame(function () { scrim.classList.add("show"); });
document.body.style.overflow = "hidden";
rail.focus();
}
function closeDrawer() {
rail.classList.remove("open");
scrim.classList.remove("show");
document.body.style.overflow = "";
setTimeout(function () { scrim.hidden = true; }, 240);
}
$("#openDrawer").addEventListener("click", openDrawer);
$("#closeDrawer").addEventListener("click", closeDrawer);
$("#applyDrawer").addEventListener("click", function () {
closeDrawer();
toast(JOBS.filter(matches).length + " jobs match");
});
scrim.addEventListener("click", closeDrawer);
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && rail.classList.contains("open")) closeDrawer();
});
// ---------- Init ----------
updateSalaryOut();
renderLocChips();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Job Board — Filter Rail</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#results">Skip to results</a>
<header class="topbar">
<div class="topbar__inner">
<div class="brand">
<span class="brand__mark" aria-hidden="true">◆</span>
<span class="brand__name">Northwind <b>Jobs</b></span>
</div>
<div class="searchbox">
<svg viewBox="0 0 24 24" aria-hidden="true" class="searchbox__icon"><path d="M21 21l-4.3-4.3M11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input id="keyword" type="search" placeholder="Search title, skill or company…" aria-label="Search jobs" />
</div>
<button class="filters-toggle" id="openDrawer" aria-haspopup="dialog" aria-controls="rail">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5h18M6 12h12M10 19h4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
Filters <span class="filters-toggle__count" id="toggleCount">0</span>
</button>
</div>
</header>
<div class="layout">
<!-- ===== FILTER RAIL ===== -->
<aside class="rail" id="rail" aria-label="Job filters" tabindex="-1">
<div class="rail__head">
<h2 class="rail__title">Filters</h2>
<button class="rail__close" id="closeDrawer" aria-label="Close filters">✕</button>
</div>
<form id="filterForm" class="rail__body">
<!-- Role -->
<section class="fgroup" data-open="true">
<button type="button" class="fgroup__head" aria-expanded="true">
<span>Role</span>
<svg class="chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="fgroup__body">
<label class="check"><input type="checkbox" name="role" value="Engineering" /><span>Engineering</span><em>62</em></label>
<label class="check"><input type="checkbox" name="role" value="Design" /><span>Design</span><em>24</em></label>
<label class="check"><input type="checkbox" name="role" value="Product" /><span>Product</span><em>18</em></label>
<label class="check"><input type="checkbox" name="role" value="Data" /><span>Data</span><em>31</em></label>
<label class="check"><input type="checkbox" name="role" value="Marketing" /><span>Marketing</span><em>15</em></label>
</div>
</section>
<!-- Job type -->
<section class="fgroup" data-open="true">
<button type="button" class="fgroup__head" aria-expanded="true">
<span>Job type</span>
<svg class="chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="fgroup__body">
<label class="check"><input type="checkbox" name="type" value="Full-time" /><span>Full-time</span><em>88</em></label>
<label class="check"><input type="checkbox" name="type" value="Part-time" /><span>Part-time</span><em>12</em></label>
<label class="check"><input type="checkbox" name="type" value="Contract" /><span>Contract</span><em>27</em></label>
<label class="check"><input type="checkbox" name="type" value="Internship" /><span>Internship</span><em>9</em></label>
</div>
</section>
<!-- Experience -->
<section class="fgroup" data-open="true">
<button type="button" class="fgroup__head" aria-expanded="true">
<span>Experience</span>
<svg class="chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="fgroup__body">
<label class="check"><input type="checkbox" name="exp" value="Junior" /><span>Junior</span><em>34</em></label>
<label class="check"><input type="checkbox" name="exp" value="Mid" /><span>Mid-level</span><em>49</em></label>
<label class="check"><input type="checkbox" name="exp" value="Senior" /><span>Senior</span><em>41</em></label>
<label class="check"><input type="checkbox" name="exp" value="Lead" /><span>Lead / Staff</span><em>17</em></label>
</div>
</section>
<!-- Remote -->
<section class="fgroup" data-open="true">
<button type="button" class="fgroup__head" aria-expanded="true">
<span>Remote</span>
<svg class="chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="fgroup__body seg">
<label class="radio"><input type="radio" name="remote" value="any" checked /><span>Any</span></label>
<label class="radio"><input type="radio" name="remote" value="Remote" /><span>Remote</span></label>
<label class="radio"><input type="radio" name="remote" value="Hybrid" /><span>Hybrid</span></label>
<label class="radio"><input type="radio" name="remote" value="On-site" /><span>On-site</span></label>
</div>
</section>
<!-- Salary -->
<section class="fgroup" data-open="true">
<button type="button" class="fgroup__head" aria-expanded="true">
<span>Min. salary</span>
<svg class="chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="fgroup__body">
<div class="salary">
<output id="salaryOut" class="salary__out">$0k+</output>
<input id="salary" type="range" min="0" max="220" step="10" value="0" aria-label="Minimum salary in thousands" />
<div class="salary__scale"><span>$0k</span><span>$110k</span><span>$220k+</span></div>
</div>
</div>
</section>
<!-- Location -->
<section class="fgroup" data-open="true">
<button type="button" class="fgroup__head" aria-expanded="true">
<span>Location</span>
<svg class="chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="fgroup__body">
<div class="locinput">
<input id="locField" type="text" placeholder="Add a city, then Enter" aria-label="Add location" autocomplete="off" />
<button type="button" id="locAdd" class="locinput__btn">Add</button>
</div>
<div class="locchips" id="locChips" aria-live="polite"></div>
<div class="locsugg">
<button type="button" data-loc="San Francisco">San Francisco</button>
<button type="button" data-loc="Berlin">Berlin</button>
<button type="button" data-loc="Austin">Austin</button>
<button type="button" data-loc="London">London</button>
</div>
</div>
</section>
</form>
<div class="rail__foot">
<button type="button" class="btn btn--ghost" id="clearAllRail">Clear all</button>
<button type="button" class="btn btn--primary" id="applyDrawer">Show results</button>
</div>
</aside>
<div class="drawer-scrim" id="scrim" hidden></div>
<!-- ===== RESULTS ===== -->
<main class="results" id="results">
<div class="results__bar">
<div>
<h1 class="results__count"><span id="count">0</span> jobs</h1>
<p class="results__sub" id="sub">Adjust the rail to refine your search</p>
</div>
<label class="sort">
<span>Sort</span>
<select id="sort" aria-label="Sort results">
<option value="relevance">Most relevant</option>
<option value="salary">Highest salary</option>
<option value="recent">Most recent</option>
</select>
</label>
</div>
<div class="activebar" id="activeBar" hidden>
<div class="activebar__chips" id="activeChips" aria-live="polite"></div>
<button type="button" class="activebar__clear" id="clearAll">Clear all</button>
</div>
<ul class="joblist" id="jobList"></ul>
<div class="empty" id="empty" hidden>
<div class="empty__icon" aria-hidden="true">🔍</div>
<h3>No matching roles</h3>
<p>Try widening your salary range or removing a filter.</p>
<button type="button" class="btn btn--ghost" id="emptyClear">Clear filters</button>
</div>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Filter Rail
A scannable faceted sidebar for a job board. Filters are grouped into collapsible sections — role, job type, experience, remote mode, minimum salary and location — each with per-option counts so candidates can see how many roles sit behind every choice. The remote mode uses a segmented radio set, salary is a live range slider, and locations are typed (or picked from suggestions) and rendered as removable chips.
Every control feeds a single state object that re-filters the listing and updates the headline results count in real time. Applied filters surface as a row of active chips above the results, each with its own remove button, alongside a clear-all action. Job cards show company initials as a colored logo, salary, location and remote badges, a “New” tag, posting age and a bookmark toggle that fires a toast.
The layout is a sticky two-column grid on desktop. Below ~920px the rail collapses into a focus-trappable slide-in drawer opened from a Filters button in the top bar, with a scrim, Escape-to-close, and a sticky footer for clear-all and apply. Everything is keyboard-usable and built with semantic landmarks and vanilla JS only.
Illustrative UI only — fictional jobs & companies, not a real hiring platform.