Job Board — Candidate Pipeline
A recruiter-side ATS pipeline that turns a hiring funnel into a scannable kanban board. Five color-coded stages — applied, screening, interview, offer, and hired — carry candidate cards with generated avatars, star ratings, location and salary chips, remote badges, and skill tags. Drag candidates between columns or nudge them with arrow buttons, watch live column counts update, open a quick-view drawer for full context, and filter the whole board by role, minimum rating, or free-text search. Built with semantic HTML, CSS variables, and vanilla JavaScript.
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), 0 1px 3px rgba(15, 23, 42, 0.05);
--sh-md: 0 4px 14px rgba(15, 23, 42, 0.08);
--sh-lg: 0 18px 50px rgba(15, 23, 42, 0.18);
--c-applied: #2563eb;
--c-screening: #7c3aed;
--c-interview: #d97706;
--c-offer: #0891b2;
--c-hired: #16a34a;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button, input, select { font: inherit; }
.skip {
position: absolute;
left: -999px;
top: 8px;
background: var(--ink);
color: #fff;
padding: 8px 14px;
border-radius: var(--r-sm);
z-index: 100;
}
.skip:focus { left: 12px; }
:focus-visible {
outline: 3px solid color-mix(in srgb, var(--brand) 50%, transparent);
outline-offset: 2px;
}
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
gap: 22px;
padding: 14px 22px;
background: var(--surface);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 20;
}
.brand { display: flex; align-items: center; gap: 11px; }
.brand-mark {
width: 38px;
height: 38px;
border-radius: 11px;
display: grid;
place-items: center;
color: #fff;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
box-shadow: var(--sh-sm);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.25; }
.brand-text strong { font-size: 15px; font-weight: 800; letter-spacing: -0.01em; }
.brand-text span { font-size: 12px; color: var(--muted); }
.role-info {
margin-left: 8px;
padding-left: 22px;
border-left: 1px solid var(--line);
display: flex;
flex-direction: column;
}
.role-title { font-weight: 700; font-size: 15px; }
.role-meta { font-size: 12px; color: var(--muted); }
.topbar-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 16px;
}
.avatars { display: flex; }
.av {
width: 30px;
height: 30px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 11px;
font-weight: 700;
color: #fff;
background: var(--c, var(--brand));
border: 2px solid var(--surface);
margin-left: -8px;
}
.av:first-child { margin-left: 0; }
.av.more { background: #e2e8f0; color: var(--ink-2); }
.btn {
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink);
padding: 9px 15px;
border-radius: var(--r-sm);
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: background .15s, border-color .15s, transform .05s, box-shadow .15s;
}
.btn:hover { border-color: var(--line-2); background: #f8fafc; }
.btn:active { transform: translateY(1px); }
.btn-primary {
background: var(--brand);
border-color: var(--brand);
color: #fff;
box-shadow: var(--sh-sm);
}
.btn-primary:hover { background: var(--brand-d); border-color: var(--brand-d); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--ink-2); }
.btn-ghost:hover { background: #eef2f7; }
/* ---------- Toolbar ---------- */
.toolbar {
display: flex;
align-items: center;
gap: 14px;
padding: 13px 22px;
background: var(--surface);
border-bottom: 1px solid var(--line);
flex-wrap: wrap;
}
.search {
display: flex;
align-items: center;
gap: 8px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 0 11px;
height: 38px;
min-width: 240px;
flex: 1 1 240px;
max-width: 360px;
color: var(--muted);
}
.search input {
border: 0;
background: transparent;
outline: 0;
width: 100%;
color: var(--ink);
font-size: 14px;
}
.field { display: flex; flex-direction: column; gap: 3px; }
.field-label { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.field select {
height: 36px;
border: 1px solid var(--line-2);
background: var(--surface);
border-radius: var(--r-sm);
padding: 0 10px;
color: var(--ink);
cursor: pointer;
}
.result-note { margin: 0 0 0 auto; font-size: 13px; color: var(--muted); }
/* ---------- Board ---------- */
.board {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(264px, 1fr);
gap: 16px;
padding: 20px 22px 40px;
overflow-x: auto;
align-items: start;
}
.col {
background: #eef2f8;
border: 1px solid var(--line);
border-radius: var(--r-lg);
display: flex;
flex-direction: column;
min-height: 200px;
transition: background .15s, box-shadow .15s, outline-color .15s;
outline: 2px dashed transparent;
outline-offset: -2px;
}
.col.drag-over {
background: var(--brand-50);
outline-color: color-mix(in srgb, var(--brand) 55%, transparent);
}
.col-head {
display: flex;
align-items: center;
gap: 9px;
padding: 14px 15px 11px;
position: sticky;
top: 0;
}
.col-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--accent); flex: none; }
.col-name { font-weight: 700; font-size: 13.5px; letter-spacing: -0.01em; }
.col-count {
margin-left: auto;
min-width: 24px;
height: 22px;
padding: 0 7px;
border-radius: 999px;
background: var(--surface);
border: 1px solid var(--line);
display: grid;
place-items: center;
font-size: 12px;
font-weight: 700;
color: var(--ink-2);
}
.col-body {
display: flex;
flex-direction: column;
gap: 10px;
padding: 4px 11px 14px;
flex: 1;
}
.col-empty {
border: 1.5px dashed var(--line-2);
border-radius: var(--r-md);
padding: 22px 12px;
text-align: center;
font-size: 12.5px;
color: var(--muted);
}
/* ---------- Candidate card ---------- */
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 12px 11px;
box-shadow: var(--sh-sm);
cursor: grab;
transition: box-shadow .15s, transform .08s, border-color .15s, opacity .15s;
position: relative;
}
.card:hover { box-shadow: var(--sh-md); border-color: var(--line-2); }
.card:focus-visible { box-shadow: var(--sh-md); }
.card.dragging { opacity: .45; cursor: grabbing; }
.card[data-new="1"]::after {
content: "NEW";
position: absolute;
top: 10px;
right: 10px;
font-size: 9px;
font-weight: 800;
letter-spacing: .06em;
color: var(--new);
background: var(--brand-50);
padding: 2px 6px;
border-radius: 999px;
}
.card-top { display: flex; align-items: center; gap: 10px; }
.card-avatar {
width: 38px;
height: 38px;
border-radius: 11px;
display: grid;
place-items: center;
font-size: 13px;
font-weight: 700;
color: #fff;
flex: none;
}
.card-id { min-width: 0; }
.card-name { font-weight: 700; font-size: 14px; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-role { font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-stars { display: flex; gap: 2px; margin-top: 9px; color: #cbd5e1; }
.card-stars .on { color: #f59e0b; }
.card-stars svg { width: 14px; height: 14px; }
.card-chips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px; }
.chip {
font-size: 11px;
font-weight: 600;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
padding: 2px 8px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.chip.remote { color: var(--ok); background: color-mix(in srgb, var(--ok) 10%, #fff); border-color: color-mix(in srgb, var(--ok) 25%, transparent); }
.chip svg { width: 11px; height: 11px; }
.card-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px; }
.tag {
font-size: 10.5px;
font-weight: 600;
color: var(--brand-d);
background: var(--brand-50);
border-radius: var(--r-sm);
padding: 2px 7px;
}
.card-foot {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 11px;
padding-top: 9px;
border-top: 1px solid var(--line);
}
.card-meta { font-size: 11px; color: var(--muted); }
.card-move {
display: flex;
gap: 4px;
}
.mv {
width: 26px;
height: 26px;
border-radius: 7px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink-2);
display: grid;
place-items: center;
cursor: pointer;
transition: background .12s, color .12s, border-color .12s;
}
.mv:hover:not(:disabled) { background: var(--brand-50); color: var(--brand-d); border-color: color-mix(in srgb, var(--brand) 35%, transparent); }
.mv:disabled { opacity: .35; cursor: not-allowed; }
.mv svg { width: 14px; height: 14px; }
/* ---------- Drawer ---------- */
.drawer-scrim {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.42);
backdrop-filter: blur(2px);
z-index: 40;
animation: fade .18s ease;
}
@keyframes fade { from { opacity: 0; } }
.drawer {
position: fixed;
top: 0;
right: 0;
height: 100%;
width: min(420px, 92vw);
background: var(--surface);
border-left: 1px solid var(--line);
box-shadow: var(--sh-lg);
z-index: 50;
padding: 26px 24px;
overflow-y: auto;
animation: slide .22s cubic-bezier(.22,.61,.36,1);
}
@keyframes slide { from { transform: translateX(20px); opacity: 0; } }
.drawer-close {
position: absolute;
top: 16px;
right: 16px;
width: 34px;
height: 34px;
border-radius: 9px;
border: 1px solid var(--line);
background: var(--surface);
font-size: 22px;
line-height: 1;
color: var(--ink-2);
cursor: pointer;
}
.drawer-close:hover { background: #f1f5f9; }
.qv-head { display: flex; gap: 14px; align-items: center; margin-bottom: 20px; padding-right: 30px; }
.qv-avatar {
width: 54px;
height: 54px;
border-radius: 15px;
display: grid;
place-items: center;
font-size: 18px;
font-weight: 700;
color: #fff;
background: var(--brand);
flex: none;
}
.qv-head h2 { margin: 0; font-size: 19px; letter-spacing: -0.02em; }
.qv-role { margin: 2px 0 6px; color: var(--muted); font-size: 13px; }
.qv-stars { display: flex; gap: 2px; color: #cbd5e1; }
.qv-stars .on { color: #f59e0b; }
.qv-stars svg { width: 16px; height: 16px; }
.qv-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin: 0 0 18px;
}
.qv-grid dt { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); font-weight: 600; }
.qv-grid dd { margin: 3px 0 0; font-size: 14px; font-weight: 600; color: var(--ink); }
.qv-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; }
.qv-note {
font-size: 13px;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 13px;
margin: 0 0 20px;
}
.qv-move label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); font-weight: 600; margin-bottom: 7px; }
.qv-move-row { display: flex; gap: 8px; }
.qv-move-row select {
flex: 1;
height: 40px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 0 11px;
background: var(--surface);
cursor: pointer;
}
/* status pill (stage badge) */
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 700;
padding: 3px 10px;
border-radius: 999px;
color: var(--accent, var(--brand));
background: color-mix(in srgb, var(--accent, var(--brand)) 12%, #fff);
border: 1px solid color-mix(in srgb, var(--accent, var(--brand)) 30%, transparent);
}
.pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
bottom: 22px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 80;
align-items: center;
}
.toast {
background: var(--ink);
color: #fff;
padding: 11px 16px;
border-radius: var(--r-sm);
font-size: 13.5px;
font-weight: 600;
box-shadow: var(--sh-lg);
display: flex;
align-items: center;
gap: 9px;
animation: toastIn .22s ease;
}
.toast .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); }
.toast.leaving { animation: toastOut .2s ease forwards; }
@keyframes toastIn { from { transform: translateY(12px); opacity: 0; } }
@keyframes toastOut { to { transform: translateY(12px); opacity: 0; } }
[hidden] { display: none !important; }
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.role-info { display: none; }
.topbar { gap: 14px; }
}
@media (max-width: 520px) {
.topbar { flex-wrap: wrap; padding: 12px 14px; }
.topbar-actions { width: 100%; justify-content: space-between; margin-left: 0; }
.toolbar { padding: 12px 14px; gap: 10px; }
.search { max-width: none; min-width: 100%; }
.field { flex: 1 1 calc(50% - 5px); }
.field select { width: 100%; }
.result-note { margin: 4px 0 0; width: 100%; }
.board {
grid-auto-flow: row;
grid-auto-columns: auto;
grid-template-columns: 1fr;
padding: 14px 14px 36px;
}
.col { min-height: 0; }
.av { width: 28px; height: 28px; }
}(function () {
"use strict";
/* ---------- Data ---------- */
var STAGES = [
{ id: "applied", name: "Applied", color: "var(--c-applied)" },
{ id: "screening", name: "Screening", color: "var(--c-screening)" },
{ id: "interview", name: "Interview", color: "var(--c-interview)" },
{ id: "offer", name: "Offer", color: "var(--c-offer)" },
{ id: "hired", name: "Hired", color: "var(--c-hired)" }
];
var STAGE_MAP = {};
STAGES.forEach(function (s) { STAGE_MAP[s.id] = s; });
var AVATAR_COLORS = ["#2563eb", "#7c3aed", "#0891b2", "#16a34a", "#d97706", "#db2777", "#0d9488", "#4f46e5"];
var candidates = [
{ id: "c1", name: "Amara Okafor", role: "Frontend Engineer", rating: 5, stage: "interview", loc: "Lisbon, PT", remote: true, salary: "€72k", source: "Referral", applied: "5 days ago", tags: ["React", "TypeScript", "Design systems"], note: "Strong portfolio, led a component library migration at her last role. Scored well on the take-home." },
{ id: "c2", name: "Diego Marin", role: "Backend Engineer", rating: 4, stage: "screening", loc: "Bogotá, CO", remote: true, salary: "$68k", source: "LinkedIn", applied: "2 days ago", tags: ["Go", "Postgres", "AWS"], note: "Recruiter screen went well. Pushing for a systems-design round next week." },
{ id: "c3", name: "Priya Raman", role: "Product Designer", rating: 5, stage: "offer", loc: "Berlin, DE", remote: false, salary: "€65k", source: "Dribbble", applied: "12 days ago", tags: ["Figma", "Prototyping", "UX research"], note: "Verbal offer extended Tuesday. Awaiting signed paperwork." },
{ id: "c4", name: "Tobias Lindqvist", role: "Frontend Engineer", rating: 3, stage: "applied", loc: "Stockholm, SE", remote: true, salary: "€70k", source: "Career page", applied: "Just now", tags: ["Vue", "CSS", "a11y"], note: "Fresh application — needs an initial resume review." },
{ id: "c5", name: "Naomi Bennett", role: "Data Analyst", rating: 4, stage: "applied", loc: "Manchester, UK", remote: true, salary: "£48k", source: "Referral", applied: "1 day ago", tags: ["SQL", "Looker", "Python"], note: "Internal referral from the analytics team. Looks like a solid fit." },
{ id: "c6", name: "Hugo Almeida", role: "Backend Engineer", rating: 5, stage: "interview", loc: "Porto, PT", remote: true, salary: "€74k", source: "GitHub", applied: "8 days ago", tags: ["Rust", "gRPC", "Kubernetes"], note: "Crushed the systems round. Scheduling the final panel." },
{ id: "c7", name: "Mei-Ling Chen", role: "Product Designer", rating: 4, stage: "screening", loc: "Singapore, SG", remote: false, salary: "$70k", source: "Portfolio", applied: "3 days ago", tags: ["Brand", "Motion", "Figma"], note: "Excellent motion work. Portfolio review scheduled with design lead." },
{ id: "c8", name: "Lucas Moreau", role: "Frontend Engineer", rating: 2, stage: "applied", loc: "Lyon, FR", remote: true, salary: "€60k", source: "Career page", applied: "4 days ago", tags: ["jQuery", "PHP"], note: "Stack mismatch — likely not a fit for this req." },
{ id: "c9", name: "Sofia Rossi", role: "Data Analyst", rating: 5, stage: "offer", loc: "Milan, IT", remote: true, salary: "€58k", source: "LinkedIn", applied: "10 days ago", tags: ["dbt", "BigQuery", "Stats"], note: "Top candidate in the analyst pool. Offer sent, very enthusiastic." },
{ id: "c10", name: "Kwame Asante", role: "Backend Engineer", rating: 4, stage: "hired", loc: "Accra, GH", remote: true, salary: "$66k", source: "Referral", applied: "21 days ago", tags: ["Node", "Redis", "DevOps"], note: "Signed and starting next month. Onboarding kicked off." },
{ id: "c11", name: "Elena Petrova", role: "Frontend Engineer", rating: 4, stage: "screening", loc: "Tallinn, EE", remote: true, salary: "€69k", source: "GitHub", applied: "6 days ago", tags: ["Svelte", "WebGL", "Perf"], note: "Impressive open-source contributions. Booking a tech screen." },
{ id: "c12", name: "James Whitfield", role: "Product Designer", rating: 3, stage: "applied", loc: "Austin, US", remote: true, salary: "$78k", source: "Dribbble", applied: "2 days ago", tags: ["Figma", "Design ops"], note: "Solid but generalist profile. Worth a screening call." },
{ id: "c13", name: "Yuki Tanaka", role: "Frontend Engineer", rating: 5, stage: "interview", loc: "Tokyo, JP", remote: false, salary: "¥9.2M", source: "Referral", applied: "9 days ago", tags: ["React", "Three.js", "Animation"], note: "Outstanding interactive demos. Final round with the design team next." }
];
/* ---------- Helpers ---------- */
var board = document.getElementById("board");
var toastWrap = document.getElementById("toastWrap");
function initials(name) {
return name.split(/\s+/).slice(0, 2).map(function (w) { return w[0]; }).join("").toUpperCase();
}
function avatarColor(id) {
var sum = 0;
for (var i = 0; i < id.length; i++) sum += id.charCodeAt(i);
return AVATAR_COLORS[sum % AVATAR_COLORS.length];
}
function el(tag, cls, html) {
var n = document.createElement(tag);
if (cls) n.className = cls;
if (html != null) n.innerHTML = html;
return n;
}
var ICON = {
star: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m12 17.3-6.18 3.7 1.64-7.03L2 9.24l7.19-.61L12 2l2.81 6.63 7.19.61-5.46 4.73L18.18 21z"/></svg>',
remote: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>',
pin: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>',
money: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M14.5 9a2.5 2.5 0 0 0-2.5-1.5c-1.4 0-2.5.7-2.5 2s1.1 1.8 2.5 2 2.5.7 2.5 2-1.1 2-2.5 2A2.5 2.5 0 0 1 9.5 16M12 6v1.5M12 16.5V18"/></svg>',
left: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>',
right: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>'
};
function starHtml(rating, big) {
var out = "";
for (var i = 1; i <= 5; i++) {
out += '<span class="' + (i <= rating ? "on" : "") + '">' + ICON.star + "</span>";
}
return out;
}
function toast(msg, type) {
var t = el("div", "toast");
var dot = el("span", "dot");
if (type === "warn") dot.style.background = "var(--warn)";
if (type === "info") dot.style.background = "var(--brand)";
t.appendChild(dot);
t.appendChild(document.createTextNode(msg));
toastWrap.appendChild(t);
setTimeout(function () {
t.classList.add("leaving");
setTimeout(function () { t.remove(); }, 200);
}, 2600);
}
/* ---------- Filters ---------- */
var searchInput = document.getElementById("search");
var roleFilter = document.getElementById("roleFilter");
var ratingFilter = document.getElementById("ratingFilter");
var clearBtn = document.getElementById("clearBtn");
var resultNote = document.getElementById("resultNote");
function passesFilter(c) {
var q = searchInput.value.trim().toLowerCase();
if (q) {
var hay = (c.name + " " + c.role + " " + c.tags.join(" ") + " " + c.loc).toLowerCase();
if (hay.indexOf(q) === -1) return false;
}
if (roleFilter.value && c.role !== roleFilter.value) return false;
if (c.rating < parseInt(ratingFilter.value, 10)) return false;
return true;
}
/* ---------- Card ---------- */
function buildCard(c) {
var card = el("article", "card");
card.tabIndex = 0;
card.dataset.id = c.id;
if (c.isNew) card.dataset.new = "1";
card.setAttribute("draggable", "true");
card.setAttribute("aria-label", c.name + ", " + c.role + ", " + STAGE_MAP[c.stage].name + " stage");
var top = el("div", "card-top");
var av = el("div", "card-avatar");
av.style.background = avatarColor(c.id);
av.textContent = initials(c.name);
var idBox = el("div", "card-id");
idBox.appendChild(el("div", "card-name", c.name));
idBox.appendChild(el("div", "card-role", c.role));
top.appendChild(av);
top.appendChild(idBox);
card.appendChild(top);
var stars = el("div", "card-stars", starHtml(c.rating));
stars.setAttribute("aria-label", c.rating + " of 5 stars");
card.appendChild(stars);
var chips = el("div", "card-chips");
chips.appendChild(el("span", "chip", ICON.pin + c.loc));
chips.appendChild(el("span", "chip", ICON.money + c.salary));
if (c.remote) chips.appendChild(el("span", "chip remote", ICON.remote + "Remote"));
card.appendChild(chips);
var tags = el("div", "card-tags");
c.tags.slice(0, 3).forEach(function (tg) { tags.appendChild(el("span", "tag", tg)); });
card.appendChild(tags);
var foot = el("div", "card-foot");
foot.appendChild(el("span", "card-meta", c.applied));
var moveWrap = el("div", "card-move");
var idx = STAGES.findIndex(function (s) { return s.id === c.stage; });
var prev = el("button", "mv", ICON.left);
prev.type = "button";
prev.title = "Move left";
prev.setAttribute("aria-label", "Move " + c.name + " to previous stage");
prev.disabled = idx === 0;
prev.addEventListener("click", function (e) { e.stopPropagation(); moveCandidate(c.id, idx - 1); });
var next = el("button", "mv", ICON.right);
next.type = "button";
next.title = "Move right";
next.setAttribute("aria-label", "Move " + c.name + " to next stage");
next.disabled = idx === STAGES.length - 1;
next.addEventListener("click", function (e) { e.stopPropagation(); moveCandidate(c.id, idx + 1); });
moveWrap.appendChild(prev);
moveWrap.appendChild(next);
foot.appendChild(moveWrap);
card.appendChild(foot);
card.addEventListener("click", function () { openDrawer(c.id); });
card.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openDrawer(c.id); }
});
/* drag */
card.addEventListener("dragstart", function (e) {
card.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", c.id);
});
card.addEventListener("dragend", function () { card.classList.remove("dragging"); });
return card;
}
/* ---------- Render board ---------- */
function render() {
board.innerHTML = "";
var totalShown = 0;
STAGES.forEach(function (stage) {
var inStage = candidates.filter(function (c) { return c.stage === stage.id; });
var visible = inStage.filter(passesFilter);
totalShown += visible.length;
var col = el("section", "col");
col.dataset.stage = stage.id;
col.setAttribute("aria-label", stage.name + " column");
col.style.setProperty("--accent", stage.color);
var head = el("div", "col-head");
var dot = el("span", "col-dot");
dot.style.setProperty("--accent", stage.color);
head.appendChild(dot);
head.appendChild(el("span", "col-name", stage.name));
var count = el("span", "col-count", String(visible.length));
count.setAttribute("aria-label", visible.length + " candidates");
head.appendChild(count);
col.appendChild(head);
var body = el("div", "col-body");
if (visible.length === 0) {
body.appendChild(el("div", "col-empty", inStage.length ? "No matches in this stage" : "Drop a candidate here"));
} else {
visible.forEach(function (c) { body.appendChild(buildCard(c)); });
}
col.appendChild(body);
/* drop target */
col.addEventListener("dragover", function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
col.classList.add("drag-over");
});
col.addEventListener("dragleave", function (e) {
if (!col.contains(e.relatedTarget)) col.classList.remove("drag-over");
});
col.addEventListener("drop", function (e) {
e.preventDefault();
col.classList.remove("drag-over");
var id = e.dataTransfer.getData("text/plain");
moveCandidateToStage(id, stage.id);
});
board.appendChild(col);
});
var anyFilter = searchInput.value.trim() || roleFilter.value || ratingFilter.value !== "0";
resultNote.textContent = anyFilter
? "Showing " + totalShown + " of " + candidates.length + " candidates"
: candidates.length + " candidates across " + STAGES.length + " stages";
}
/* ---------- Move logic ---------- */
function moveCandidate(id, newIdx) {
if (newIdx < 0 || newIdx >= STAGES.length) return;
moveCandidateToStage(id, STAGES[newIdx].id);
}
function moveCandidateToStage(id, stageId) {
var c = candidates.find(function (x) { return x.id === id; });
if (!c || c.stage === stageId) return;
var fromName = STAGE_MAP[c.stage].name;
c.stage = stageId;
c.isNew = false;
render();
syncDrawerStage(c);
toast(c.name + " moved from " + fromName + " to " + STAGE_MAP[stageId].name, "info");
}
/* ---------- Drawer / quick view ---------- */
var drawer = document.getElementById("drawer");
var scrim = document.getElementById("scrim");
var drawerClose = document.getElementById("drawerClose");
var qvStageSelect = document.getElementById("qvStageSelect");
var qvMoveBtn = document.getElementById("qvMoveBtn");
var currentId = null;
var lastFocused = null;
STAGES.forEach(function (s) {
var o = el("option");
o.value = s.id;
o.textContent = s.name;
qvStageSelect.appendChild(o);
});
function openDrawer(id) {
var c = candidates.find(function (x) { return x.id === id; });
if (!c) return;
currentId = id;
lastFocused = document.activeElement;
document.getElementById("qvAvatar").textContent = initials(c.name);
document.getElementById("qvAvatar").style.background = avatarColor(c.id);
document.getElementById("qvName").textContent = c.name;
document.getElementById("qvRole").textContent = c.role;
document.getElementById("qvStars").innerHTML = starHtml(c.rating);
document.getElementById("qvLoc").textContent = c.loc + (c.remote ? " · Remote OK" : "");
document.getElementById("qvSalary").textContent = c.salary;
document.getElementById("qvSource").textContent = c.source;
document.getElementById("qvApplied").textContent = c.applied;
document.getElementById("qvNote").textContent = c.note;
var tagBox = document.getElementById("qvTags");
tagBox.innerHTML = "";
c.tags.forEach(function (tg) { tagBox.appendChild(el("span", "tag", tg)); });
syncDrawerStage(c);
qvStageSelect.value = c.stage;
scrim.hidden = false;
drawer.hidden = false;
drawerClose.focus();
document.addEventListener("keydown", onEsc);
}
function syncDrawerStage(c) {
if (currentId !== c.id) return;
var stageEl = document.getElementById("qvStage");
var s = STAGE_MAP[c.stage];
stageEl.innerHTML = '<span class="pill" style="--accent:' + s.color + '">' + s.name + "</span>";
qvStageSelect.value = c.stage;
}
function closeDrawer() {
drawer.hidden = true;
scrim.hidden = true;
currentId = null;
document.removeEventListener("keydown", onEsc);
if (lastFocused && lastFocused.focus) lastFocused.focus();
}
function onEsc(e) { if (e.key === "Escape") closeDrawer(); }
drawerClose.addEventListener("click", closeDrawer);
scrim.addEventListener("click", closeDrawer);
qvMoveBtn.addEventListener("click", function () {
if (currentId) moveCandidateToStage(currentId, qvStageSelect.value);
});
/* ---------- Add candidate (demo) ---------- */
var SAMPLE_NEW = [
{ name: "Ravi Suresh", role: "Backend Engineer", rating: 4, loc: "Pune, IN", remote: true, salary: "$60k", source: "Career page", tags: ["Java", "Spring", "Kafka"], note: "Newly sourced — pending first review." },
{ name: "Clara Nilsson", role: "Product Designer", rating: 5, loc: "Oslo, NO", remote: true, salary: "€67k", source: "Referral", tags: ["Figma", "Systems", "Research"], note: "Newly sourced — pending first review." },
{ name: "Marcus Webb", role: "Data Analyst", rating: 3, loc: "Toronto, CA", remote: true, salary: "$62k", source: "LinkedIn", tags: ["SQL", "Tableau"], note: "Newly sourced — pending first review." },
{ name: "Aisha Haddad", role: "Frontend Engineer", rating: 4, loc: "Tunis, TN", remote: true, salary: "€55k", source: "GitHub", tags: ["React", "Next.js", "a11y"], note: "Newly sourced — pending first review." }
];
var addCount = 0;
document.getElementById("addBtn").addEventListener("click", function () {
var tpl = SAMPLE_NEW[addCount % SAMPLE_NEW.length];
addCount++;
var c = {
id: "n" + Date.now(),
name: tpl.name,
role: tpl.role,
rating: tpl.rating,
stage: "applied",
loc: tpl.loc,
remote: tpl.remote,
salary: tpl.salary,
source: tpl.source,
applied: "Just now",
tags: tpl.tags,
note: tpl.note,
isNew: true
};
candidates.unshift(c);
/* reset filters so the new card is visible */
searchInput.value = "";
roleFilter.value = "";
ratingFilter.value = "0";
render();
toast(c.name + " added to Applied");
});
/* ---------- Wire filters ---------- */
searchInput.addEventListener("input", render);
roleFilter.addEventListener("change", render);
ratingFilter.addEventListener("change", render);
clearBtn.addEventListener("click", function () {
searchInput.value = "";
roleFilter.value = "";
ratingFilter.value = "0";
render();
toast("Filters cleared", "info");
});
/* ---------- Init ---------- */
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Job Board — Candidate Pipeline</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" href="#board">Skip to pipeline</a>
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/></svg>
</span>
<div class="brand-text">
<strong>Northwind Talent</strong>
<span>Candidate Pipeline</span>
</div>
</div>
<div class="role-info">
<span class="role-title" id="reqTitle">Senior Frontend Engineer</span>
<span class="role-meta">Req #ENG-2041 · Remote · 38 candidates</span>
</div>
<div class="topbar-actions">
<div class="avatars" aria-label="Hiring team">
<span class="av" style="--c:#2563eb">PR</span>
<span class="av" style="--c:#16a34a">KS</span>
<span class="av" style="--c:#d97706">LM</span>
<span class="av more">+3</span>
</div>
<button class="btn btn-primary" id="addBtn" type="button">+ Add candidate</button>
</div>
</header>
<section class="toolbar" aria-label="Filters">
<div class="search">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="search" id="search" placeholder="Search candidates…" aria-label="Search candidates" />
</div>
<label class="field">
<span class="field-label">Role</span>
<select id="roleFilter" aria-label="Filter by role">
<option value="">All roles</option>
<option value="Frontend Engineer">Frontend Engineer</option>
<option value="Backend Engineer">Backend Engineer</option>
<option value="Product Designer">Product Designer</option>
<option value="Data Analyst">Data Analyst</option>
</select>
</label>
<label class="field">
<span class="field-label">Min rating</span>
<select id="ratingFilter" aria-label="Filter by minimum rating">
<option value="0">Any</option>
<option value="3">3+ stars</option>
<option value="4">4+ stars</option>
<option value="5">5 stars</option>
</select>
</label>
<button class="btn btn-ghost" id="clearBtn" type="button">Clear</button>
<p class="result-note" id="resultNote" aria-live="polite"></p>
</section>
<main class="board" id="board" aria-label="Candidate pipeline">
<!-- columns injected by script -->
</main>
<!-- Quick-view drawer -->
<div class="drawer-scrim" id="scrim" hidden></div>
<aside class="drawer" id="drawer" role="dialog" aria-modal="true" aria-labelledby="qvName" hidden>
<button class="drawer-close" id="drawerClose" type="button" aria-label="Close quick view">×</button>
<div class="qv-head">
<span class="qv-avatar" id="qvAvatar" aria-hidden="true">--</span>
<div>
<h2 id="qvName">Candidate</h2>
<p class="qv-role" id="qvRole">Role</p>
<div class="qv-stars" id="qvStars" aria-label="Rating"></div>
</div>
</div>
<dl class="qv-grid">
<div><dt>Stage</dt><dd id="qvStage">—</dd></div>
<div><dt>Location</dt><dd id="qvLoc">—</dd></div>
<div><dt>Salary expectation</dt><dd id="qvSalary">—</dd></div>
<div><dt>Source</dt><dd id="qvSource">—</dd></div>
<div><dt>Applied</dt><dd id="qvApplied">—</dd></div>
</dl>
<div class="qv-tags" id="qvTags" aria-label="Tags"></div>
<p class="qv-note" id="qvNote"></p>
<div class="qv-move">
<label for="qvStageSelect">Move to stage</label>
<div class="qv-move-row">
<select id="qvStageSelect"></select>
<button class="btn btn-primary" id="qvMoveBtn" type="button">Move</button>
</div>
</div>
</aside>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Candidate Pipeline
A recruiter’s applicant-tracking board that lays a hiring funnel out as a horizontal kanban. Five stages — Applied, Screening, Interview, Offer, and Hired — each carry a colored accent, a live count badge, and a column of dense candidate cards. Every card shows a generated avatar, a star rating, location and salary chips, a remote badge, and the top skill tags, with a “NEW” marker on freshly sourced applicants.
The board is fully interactive. Drag any card between columns to advance or rewind a candidate, or use the per-card arrow buttons to step one stage at a time — counts and toasts update instantly. Clicking a card opens a quick-view drawer with the full profile, recruiter notes, and a stage selector to move the candidate without leaving the panel. A toolbar filters the entire board by role, minimum rating, or free-text search across names, skills, and locations, and an “Add candidate” action drops a fresh applicant into the funnel.
The layout scrolls horizontally on desktop and reflows to stacked columns down to roughly 360px, with WCAG AA contrast, focus-visible outlines, ARIA roles on the dialog and columns, keyboard-openable cards, and a small reusable toast() helper. No frameworks and no build step — just semantic HTML, CSS custom properties, and vanilla JavaScript.
Illustrative UI only — fictional jobs & companies, not a real hiring platform.