More Navigation Snippets
Chip Filter — Free HTML CSS JS Snippet
Chip Filter · Navigation · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Chip Filter — HTML CSS JavaScript

Pill-shaped filter chips that instantly filter a card grid by category. Active state, count badges, and animated cards. Zero dependencies.
Chip filters — sometimes called filter pills or tag filters — are a core navigation pattern in component libraries, design systems, marketplaces, and content-heavy interfaces. They give users a tactile, scannable way to narrow a collection without leaving the page. This snippet builds a complete chip filter system with per-category counts, active state management, and live grid filtering using pure HTML, CSS, and vanilla JavaScript.
Chip active state and styling
Each chip button uses border-radius: 999px to create a fully rounded pill shape regardless of text length. In the inactive state the chip has a 1.5px solid #e2e8f0 border on a white background with muted grey text. The .active class switches to a solid indigo fill (background: #6366f1) with white text — a single class toggle drives the full visual transition. A CSS transition on border-color, background, and color ensures the switch animates smoothly rather than snapping.
Count badges
Each chip contains a .chip-count span showing how many items belong to that category. On inactive chips the badge uses a light #f1f5f9 background; on the active chip it inherits white text with a semi-transparent white background (rgba(255,255,255,0.25)) to maintain legibility against the indigo fill. Counts are computed once on load by filtering the ITEMS array — this keeps the JS simple and avoids DOM queries for counting.
Rendering approach
Rather than toggling display or visibility on existing DOM nodes, the render() function replaces grid.innerHTML entirely with only the matching items. This is deliberately simple — for small datasets (under 200 items) it is faster and less error-prone than a dual-state approach with CSS hidden classes. For larger datasets where DOM churn is costly, a hidden class approach with position: absolute would be preferable to avoid reflow.
Data model
Each item object carries name, desc, cat, icon, and bg fields. The category string on each item matches the data-cat attribute on the corresponding chip, making the filter a simple Array.filter() call: ITEMS.filter(i => i.cat === active). Adding a new category requires only adding items with the new cat string and a matching chip button — no other logic changes.
Empty state
When no items match the active filter (impossible with the default data but relevant when connecting to dynamic data), the grid renders a centred "No components match this filter" message. The empty state div is always in the DOM but hidden; show class makes it visible.
Accessibility
The chip group has role="group" and aria-label="Filter by category". The grid has aria-live="polite" so screen readers announce when the content updates after a filter change. Chip buttons are native <button> elements so they receive keyboard focus and fire on Enter/Space by default.
Step by step
How to Use
- 1Click any category chipThe chip switches to the filled indigo active state and the card grid instantly re-renders with only matching components.
- 2Click "All"The "All" chip activates and all 12 cards are shown. The count badge on the chip shows the total item count.
- 3Read the count badgesEach chip shows a small badge with the number of items in that category, so users know what to expect before filtering.
- 4Hover a cardCards show a subtle indigo border and a soft shadow lift to indicate they are interactive links in a real implementation.
- 5Add your own itemsExtend the ITEMS array with your own objects — each needs name, desc, cat (matching a chip data-cat), icon, and bg color.
- 6Add a new categoryAdd a chip button with data-cat="yourcat" and add ITEMS entries with cat:"yourcat". No other code changes are required.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Change active from a string to a Set: const active = new Set(["all"]). On chip click, toggle the category in/out of the set. In render(), filter items where the set has "all" or includes item.cat. Update chip active class based on set membership.
Add an opacity:0 and transform:scale(0.95) state on .card before re-rendering. Use setTimeout to apply the new innerHTML after a 150ms fade-out. Then requestAnimationFrame to add an opacity:1 class on the new cards.
Replace the ITEMS array with a fetch() call to your API endpoint. On page load, fetch all items once, store them in the ITEMS variable, then call updateCounts() and render(). Chip clicks just re-filter the in-memory array without additional network requests.
On chip click, update the URL with history.pushState(null, "", "?cat=" + active). On page load, read the parameter with new URLSearchParams(location.search).get("cat") and set the initial active value before calling render().
Yes — replace the button text with an SVG icon and a hidden <span> for screen readers, keeping aria-label on the button. The pill shape and count badge work identically.
Yes. Click the React, Vue, Angular, or Tailwind export button above the preview. In React, store the active category (or a Set for multi-select) in useState and toggle each chip class with a conditional className; in Vue bind :class to a reactive value with @click; in Angular use [class.active] with (click). The count-badge calculation and the filtered-list re-render are framework-agnostic, so only the event binding and class-toggling syntax differ from the vanilla version. The Tailwind export converts the pill shape, active state, and badge styling into utility classes for a Tailwind codebase.
Chip Filter — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chip Filter</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: #f8fafc; min-height: 100vh; padding: 24px 20px; }
.page { max-width: 680px; margin: 0 auto; }
.header { margin-bottom: 20px; }
.heading { font-size: 22px; font-weight: 800; color: #0f172a; margin-bottom: 4px; }
.sub { font-size: 13px; color: #64748b; }
.chips {
display: flex; flex-wrap: wrap; gap: 8px;
margin-bottom: 20px;
}
.chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 7px 14px; border-radius: 999px;
border: 1.5px solid #e2e8f0;
background: #fff; color: #64748b;
font-size: 13px; font-weight: 600;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.1s;
white-space: nowrap;
user-select: none;
}
.chip:hover { border-color: #6366f1; color: #6366f1; background: #f5f3ff; }
.chip.active { background: #6366f1; border-color: #6366f1; color: #fff; }
.chip-count {
font-size: 10px; font-weight: 700;
min-width: 18px; height: 18px; padding: 0 4px;
border-radius: 9px;
background: rgba(255,255,255,0.25); color: inherit;
display: flex; align-items: center; justify-content: center;
}
.chip:not(.active) .chip-count { background: #f1f5f9; color: #94a3b8; }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
transition: border-color 0.15s, box-shadow 0.15s, opacity 0.2s, transform 0.2s;
}
.card:hover { border-color: #a5b4fc; box-shadow: 0 4px 14px rgba(99,102,241,0.1); }
.card.hidden { opacity: 0; transform: scale(0.94); pointer-events: none; position: absolute; }
.card-icon {
width: 38px; height: 38px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px; margin-bottom: 10px;
}
.card-name { font-size: 13px; font-weight: 700; color: #1e293b; margin-bottom: 3px; }
.card-desc { font-size: 11.5px; color: #64748b; line-height: 1.4; margin-bottom: 8px; }
.card-tag {
display: inline-block;
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px;
padding: 2px 8px; border-radius: 4px;
}
.tag-layout { background: #eff6ff; color: #3b82f6; }
.tag-forms { background: #f0fdf4; color: #22c55e; }
.tag-data { background: #fefce8; color: #eab308; }
.tag-feedback { background: #fdf4ff; color: #a855f7; }
.tag-nav { background: #fff7ed; color: #f97316; }
.empty { grid-column: 1/-1; text-align: center; padding: 40px; color: #94a3b8; font-size: 14px; display: none; }
.empty.show { display: block; }
</style>
</head>
<body>
<div class="page">
<div class="header">
<h1 class="heading">Component Library</h1>
<p class="sub">48 components across 6 categories</p>
</div>
<div class="chips" id="chips" role="group" aria-label="Filter by category">
<button class="chip active" data-cat="all">All <span class="chip-count" id="count-all">12</span></button>
<button class="chip" data-cat="layout">Layout <span class="chip-count" id="count-layout"></span></button>
<button class="chip" data-cat="forms">Forms <span class="chip-count" id="count-forms"></span></button>
<button class="chip" data-cat="data">Data <span class="chip-count" id="count-data"></span></button>
<button class="chip" data-cat="feedback">Feedback <span class="chip-count" id="count-feedback"></span></button>
<button class="chip" data-cat="nav">Navigation <span class="chip-count" id="count-nav"></span></button>
</div>
<div class="grid" id="grid" aria-live="polite"></div>
</div>
<script>
const ITEMS = [
{ name:'Bento Grid', desc:'Responsive asymmetric grid', cat:'layout', icon:'⬛', bg:'#eff6ff' },
{ name:'Masonry Layout', desc:'Pinterest-style column layout', cat:'layout', icon:'🧱', bg:'#eff6ff' },
{ name:'Dashboard Grid', desc:'Admin panel skeleton layout', cat:'layout', icon:'📐', bg:'#eff6ff' },
{ name:'Floating Label', desc:'Animated placeholder input', cat:'forms', icon:'✏️', bg:'#f0fdf4' },
{ name:'OTP Input', desc:'6-digit code entry field', cat:'forms', icon:'🔢', bg:'#f0fdf4' },
{ name:'Tag Input', desc:'Multi-value chip input', cat:'forms', icon:'🏷️', bg:'#f0fdf4' },
{ name:'Data Table', desc:'Sortable & filterable table', cat:'data', icon:'📊', bg:'#fefce8' },
{ name:'Donut Chart', desc:'SVG donut with tooltip', cat:'data', icon:'🍩', bg:'#fefce8' },
{ name:'Line Chart', desc:'SVG sparkline widget', cat:'data', icon:'📈', bg:'#fefce8' },
{ name:'Toast Queue', desc:'Stacked notification toasts', cat:'feedback', icon:'🔔', bg:'#fdf4ff' },
{ name:'Progress Bar', desc:'Animated step progress bar', cat:'feedback', icon:'📶', bg:'#fdf4ff' },
{ name:'Mega Menu', desc:'Multi-column dropdown nav', cat:'nav', icon:'🗂️', bg:'#fff7ed' },
];
const grid = document.getElementById('grid');
const chips = document.querySelectorAll('.chip');
let active = 'all';
function countByCat(cat) { return cat === 'all' ? ITEMS.length : ITEMS.filter(i => i.cat === cat).length; }
function updateCounts() {
chips.forEach(c => {
const cat = c.dataset.cat;
const span = c.querySelector('.chip-count');
span.textContent = countByCat(cat);
});
}
function render() {
const shown = active === 'all' ? ITEMS : ITEMS.filter(i => i.cat === active);
grid.innerHTML = shown.map(item => `
<div class="card">
<div class="card-icon" style="background:${item.bg}">${item.icon}</div>
<div class="card-name">${item.name}</div>
<div class="card-desc">${item.desc}</div>
<span class="card-tag tag-${item.cat}">${item.cat}</span>
</div>
`).join('');
if (!shown.length) {
grid.innerHTML = '<div class="empty show">No components match this filter.</div>';
}
}
chips.forEach(chip => {
chip.addEventListener('click', () => {
chips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
active = chip.dataset.cat;
render();
});
});
updateCounts();
render();
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chip Filter</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing: border-box; margin: 0; padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif; background: #f8fafc; min-height: 100vh; padding: 24px 20px;
}
.chip.active {
background: #6366f1; border-color: #6366f1; color: #fff;
}
.chip:not(.active) .chip-count {
background: #f1f5f9; color: #94a3b8;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
transition: border-color 0.15s, box-shadow 0.15s, opacity 0.2s, transform 0.2s;
}
.card:hover {
border-color: #a5b4fc; box-shadow: 0 4px 14px rgba(99,102,241,0.1);
}
.card.hidden {
opacity: 0; transform: scale(0.94); pointer-events: none; position: absolute;
}
.card-icon {
width: 38px; height: 38px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px; margin-bottom: 10px;
}
.card-name {
font-size: 13px; font-weight: 700; color: #1e293b; margin-bottom: 3px;
}
.card-desc {
font-size: 11.5px; color: #64748b; line-height: 1.4; margin-bottom: 8px;
}
.card-tag {
display: inline-block;
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px;
padding: 2px 8px; border-radius: 4px;
}
.empty {
grid-column: 1/-1; text-align: center; padding: 40px; color: #94a3b8; font-size: 14px; display: none;
}
.empty.show {
display: block;
}
</style>
</head>
<body>
<div class="max-w-[680px] my-0 mx-auto">
<div class="mb-5">
<h1 class="text-[22px] font-extrabold text-[#0f172a] mb-1">Component Library</h1>
<p class="text-[13px] text-[#64748b]">48 components across 6 categories</p>
</div>
<div class="flex flex-wrap gap-2 mb-5" id="chips" role="group" aria-label="Filter by category">
<button class="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff] active" data-cat="all">All <span class="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-all">12</span></button>
<button class="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff]" data-cat="layout">Layout <span class="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-layout"></span></button>
<button class="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff]" data-cat="forms">Forms <span class="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-forms"></span></button>
<button class="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff]" data-cat="data">Data <span class="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-data"></span></button>
<button class="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff]" data-cat="feedback">Feedback <span class="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-feedback"></span></button>
<button class="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff]" data-cat="nav">Navigation <span class="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-nav"></span></button>
</div>
<div class="grid" id="grid" aria-live="polite"></div>
</div>
<script>
const ITEMS = [
{ name:'Bento Grid', desc:'Responsive asymmetric grid', cat:'layout', icon:'⬛', bg:'#eff6ff' },
{ name:'Masonry Layout', desc:'Pinterest-style column layout', cat:'layout', icon:'🧱', bg:'#eff6ff' },
{ name:'Dashboard Grid', desc:'Admin panel skeleton layout', cat:'layout', icon:'📐', bg:'#eff6ff' },
{ name:'Floating Label', desc:'Animated placeholder input', cat:'forms', icon:'✏️', bg:'#f0fdf4' },
{ name:'OTP Input', desc:'6-digit code entry field', cat:'forms', icon:'🔢', bg:'#f0fdf4' },
{ name:'Tag Input', desc:'Multi-value chip input', cat:'forms', icon:'🏷️', bg:'#f0fdf4' },
{ name:'Data Table', desc:'Sortable & filterable table', cat:'data', icon:'📊', bg:'#fefce8' },
{ name:'Donut Chart', desc:'SVG donut with tooltip', cat:'data', icon:'🍩', bg:'#fefce8' },
{ name:'Line Chart', desc:'SVG sparkline widget', cat:'data', icon:'📈', bg:'#fefce8' },
{ name:'Toast Queue', desc:'Stacked notification toasts', cat:'feedback', icon:'🔔', bg:'#fdf4ff' },
{ name:'Progress Bar', desc:'Animated step progress bar', cat:'feedback', icon:'📶', bg:'#fdf4ff' },
{ name:'Mega Menu', desc:'Multi-column dropdown nav', cat:'nav', icon:'🗂️', bg:'#fff7ed' },
];
const grid = document.getElementById('grid');
const chips = document.querySelectorAll('.chip');
let active = 'all';
function countByCat(cat) { return cat === 'all' ? ITEMS.length : ITEMS.filter(i => i.cat === cat).length; }
function updateCounts() {
chips.forEach(c => {
const cat = c.dataset.cat;
const span = c.querySelector('.chip-count');
span.textContent = countByCat(cat);
});
}
function render() {
const shown = active === 'all' ? ITEMS : ITEMS.filter(i => i.cat === active);
grid.innerHTML = shown.map(item => `
<div class="card">
<div class="card-icon" style="background:${item.bg}">${item.icon}</div>
<div class="card-name">${item.name}</div>
<div class="card-desc">${item.desc}</div>
<span class="card-tag tag-${item.cat}">${item.cat}</span>
</div>
`).join('');
if (!shown.length) {
grid.innerHTML = '<div class="empty show">No components match this filter.</div>';
}
}
chips.forEach(chip => {
chip.addEventListener('click', () => {
chips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
active = chip.dataset.cat;
render();
});
});
updateCounts();
render();
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to ChipFilter.module.css
const css = `
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: #f8fafc; min-height: 100vh; padding: 24px 20px; }
.page { max-width: 680px; margin: 0 auto; }
.header { margin-bottom: 20px; }
.heading { font-size: 22px; font-weight: 800; color: #0f172a; margin-bottom: 4px; }
.sub { font-size: 13px; color: #64748b; }
.chips {
display: flex; flex-wrap: wrap; gap: 8px;
margin-bottom: 20px;
}
.chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 7px 14px; border-radius: 999px;
border: 1.5px solid #e2e8f0;
background: #fff; color: #64748b;
font-size: 13px; font-weight: 600;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.1s;
white-space: nowrap;
user-select: none;
}
.chip:hover { border-color: #6366f1; color: #6366f1; background: #f5f3ff; }
.chip.active { background: #6366f1; border-color: #6366f1; color: #fff; }
.chip-count {
font-size: 10px; font-weight: 700;
min-width: 18px; height: 18px; padding: 0 4px;
border-radius: 9px;
background: rgba(255,255,255,0.25); color: inherit;
display: flex; align-items: center; justify-content: center;
}
.chip:not(.active) .chip-count { background: #f1f5f9; color: #94a3b8; }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
transition: border-color 0.15s, box-shadow 0.15s, opacity 0.2s, transform 0.2s;
}
.card:hover { border-color: #a5b4fc; box-shadow: 0 4px 14px rgba(99,102,241,0.1); }
.card.hidden { opacity: 0; transform: scale(0.94); pointer-events: none; position: absolute; }
.card-icon {
width: 38px; height: 38px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px; margin-bottom: 10px;
}
.card-name { font-size: 13px; font-weight: 700; color: #1e293b; margin-bottom: 3px; }
.card-desc { font-size: 11.5px; color: #64748b; line-height: 1.4; margin-bottom: 8px; }
.card-tag {
display: inline-block;
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px;
padding: 2px 8px; border-radius: 4px;
}
.tag-layout { background: #eff6ff; color: #3b82f6; }
.tag-forms { background: #f0fdf4; color: #22c55e; }
.tag-data { background: #fefce8; color: #eab308; }
.tag-feedback { background: #fdf4ff; color: #a855f7; }
.tag-nav { background: #fff7ed; color: #f97316; }
.empty { grid-column: 1/-1; text-align: center; padding: 40px; color: #94a3b8; font-size: 14px; display: none; }
.empty.show { display: block; }
`;
export default function ChipFilter() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
const ITEMS = [
{ name:'Bento Grid', desc:'Responsive asymmetric grid', cat:'layout', icon:'⬛', bg:'#eff6ff' },
{ name:'Masonry Layout', desc:'Pinterest-style column layout', cat:'layout', icon:'🧱', bg:'#eff6ff' },
{ name:'Dashboard Grid', desc:'Admin panel skeleton layout', cat:'layout', icon:'📐', bg:'#eff6ff' },
{ name:'Floating Label', desc:'Animated placeholder input', cat:'forms', icon:'✏️', bg:'#f0fdf4' },
{ name:'OTP Input', desc:'6-digit code entry field', cat:'forms', icon:'🔢', bg:'#f0fdf4' },
{ name:'Tag Input', desc:'Multi-value chip input', cat:'forms', icon:'🏷️', bg:'#f0fdf4' },
{ name:'Data Table', desc:'Sortable & filterable table', cat:'data', icon:'📊', bg:'#fefce8' },
{ name:'Donut Chart', desc:'SVG donut with tooltip', cat:'data', icon:'🍩', bg:'#fefce8' },
{ name:'Line Chart', desc:'SVG sparkline widget', cat:'data', icon:'📈', bg:'#fefce8' },
{ name:'Toast Queue', desc:'Stacked notification toasts', cat:'feedback', icon:'🔔', bg:'#fdf4ff' },
{ name:'Progress Bar', desc:'Animated step progress bar', cat:'feedback', icon:'📶', bg:'#fdf4ff' },
{ name:'Mega Menu', desc:'Multi-column dropdown nav', cat:'nav', icon:'🗂️', bg:'#fff7ed' },
];
const grid = document.getElementById('grid');
const chips = document.querySelectorAll('.chip');
let active = 'all';
function countByCat(cat) { return cat === 'all' ? ITEMS.length : ITEMS.filter(i => i.cat === cat).length; }
function updateCounts() {
chips.forEach(c => {
const cat = c.dataset.cat;
const span = c.querySelector('.chip-count');
span.textContent = countByCat(cat);
});
}
function render() {
const shown = active === 'all' ? ITEMS : ITEMS.filter(i => i.cat === active);
grid.innerHTML = shown.map(item => `
<div class="card">
<div class="card-icon" style="background:${item.bg}">${item.icon}</div>
<div class="card-name">${item.name}</div>
<div class="card-desc">${item.desc}</div>
<span class="card-tag tag-${item.cat}">${item.cat}</span>
</div>
`).join('');
if (!shown.length) {
grid.innerHTML = '<div class="empty show">No components match this filter.</div>';
}
}
chips.forEach(chip => {
chip.addEventListener('click', () => {
chips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
active = chip.dataset.cat;
render();
});
});
updateCounts();
render();
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="page">
<div className="header">
<h1 className="heading">Component Library</h1>
<p className="sub">48 components across 6 categories</p>
</div>
<div className="chips" id="chips" role="group" aria-label="Filter by category">
<button className="chip active" data-cat="all">All <span className="chip-count" id="count-all">12</span></button>
<button className="chip" data-cat="layout">Layout <span className="chip-count" id="count-layout"></span></button>
<button className="chip" data-cat="forms">Forms <span className="chip-count" id="count-forms"></span></button>
<button className="chip" data-cat="data">Data <span className="chip-count" id="count-data"></span></button>
<button className="chip" data-cat="feedback">Feedback <span className="chip-count" id="count-feedback"></span></button>
<button className="chip" data-cat="nav">Navigation <span className="chip-count" id="count-nav"></span></button>
</div>
<div className="grid" id="grid" aria-live="polite"></div>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function ChipFilter() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
const ITEMS = [
{ name:'Bento Grid', desc:'Responsive asymmetric grid', cat:'layout', icon:'⬛', bg:'#eff6ff' },
{ name:'Masonry Layout', desc:'Pinterest-style column layout', cat:'layout', icon:'🧱', bg:'#eff6ff' },
{ name:'Dashboard Grid', desc:'Admin panel skeleton layout', cat:'layout', icon:'📐', bg:'#eff6ff' },
{ name:'Floating Label', desc:'Animated placeholder input', cat:'forms', icon:'✏️', bg:'#f0fdf4' },
{ name:'OTP Input', desc:'6-digit code entry field', cat:'forms', icon:'🔢', bg:'#f0fdf4' },
{ name:'Tag Input', desc:'Multi-value chip input', cat:'forms', icon:'🏷️', bg:'#f0fdf4' },
{ name:'Data Table', desc:'Sortable & filterable table', cat:'data', icon:'📊', bg:'#fefce8' },
{ name:'Donut Chart', desc:'SVG donut with tooltip', cat:'data', icon:'🍩', bg:'#fefce8' },
{ name:'Line Chart', desc:'SVG sparkline widget', cat:'data', icon:'📈', bg:'#fefce8' },
{ name:'Toast Queue', desc:'Stacked notification toasts', cat:'feedback', icon:'🔔', bg:'#fdf4ff' },
{ name:'Progress Bar', desc:'Animated step progress bar', cat:'feedback', icon:'📶', bg:'#fdf4ff' },
{ name:'Mega Menu', desc:'Multi-column dropdown nav', cat:'nav', icon:'🗂️', bg:'#fff7ed' },
];
const grid = document.getElementById('grid');
const chips = document.querySelectorAll('.chip');
let active = 'all';
function countByCat(cat) { return cat === 'all' ? ITEMS.length : ITEMS.filter(i => i.cat === cat).length; }
function updateCounts() {
chips.forEach(c => {
const cat = c.dataset.cat;
const span = c.querySelector('.chip-count');
span.textContent = countByCat(cat);
});
}
function render() {
const shown = active === 'all' ? ITEMS : ITEMS.filter(i => i.cat === active);
grid.innerHTML = shown.map(item => `
<div class="card">
<div class="card-icon" style="background:${item.bg}">${item.icon}</div>
<div class="card-name">${item.name}</div>
<div class="card-desc">${item.desc}</div>
<span class="card-tag tag-${item.cat}">${item.cat}</span>
</div>
`).join('');
if (!shown.length) {
grid.innerHTML = '<div class="empty show">No components match this filter.</div>';
}
}
chips.forEach(chip => {
chip.addEventListener('click', () => {
chips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
active = chip.dataset.cat;
render();
});
});
updateCounts();
render();
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
* {
box-sizing: border-box; margin: 0; padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif; background: #f8fafc; min-height: 100vh; padding: 24px 20px;
}
.chip.active {
background: #6366f1; border-color: #6366f1; color: #fff;
}
.chip:not(.active) .chip-count {
background: #f1f5f9; color: #94a3b8;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
transition: border-color 0.15s, box-shadow 0.15s, opacity 0.2s, transform 0.2s;
}
.card:hover {
border-color: #a5b4fc; box-shadow: 0 4px 14px rgba(99,102,241,0.1);
}
.card.hidden {
opacity: 0; transform: scale(0.94); pointer-events: none; position: absolute;
}
.card-icon {
width: 38px; height: 38px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px; margin-bottom: 10px;
}
.card-name {
font-size: 13px; font-weight: 700; color: #1e293b; margin-bottom: 3px;
}
.card-desc {
font-size: 11.5px; color: #64748b; line-height: 1.4; margin-bottom: 8px;
}
.card-tag {
display: inline-block;
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px;
padding: 2px 8px; border-radius: 4px;
}
.empty {
grid-column: 1/-1; text-align: center; padding: 40px; color: #94a3b8; font-size: 14px; display: none;
}
.empty.show {
display: block;
}
`}</style>
<div className="max-w-[680px] my-0 mx-auto">
<div className="mb-5">
<h1 className="text-[22px] font-extrabold text-[#0f172a] mb-1">Component Library</h1>
<p className="text-[13px] text-[#64748b]">48 components across 6 categories</p>
</div>
<div className="flex flex-wrap gap-2 mb-5" id="chips" role="group" aria-label="Filter by category">
<button className="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff] active" data-cat="all">All <span className="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-all">12</span></button>
<button className="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff]" data-cat="layout">Layout <span className="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-layout"></span></button>
<button className="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff]" data-cat="forms">Forms <span className="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-forms"></span></button>
<button className="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff]" data-cat="data">Data <span className="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-data"></span></button>
<button className="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff]" data-cat="feedback">Feedback <span className="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-feedback"></span></button>
<button className="chip inline-flex items-center gap-1.5 py-[7px] px-3.5 rounded-[999px] border bg-[#fff] text-[#64748b] text-[13px] font-semibold cursor-pointer [transition:border-color_0.15s,_background_0.15s,_color_0.15s,_transform_0.1s] whitespace-nowrap select-none hover:border-[#6366f1] hover:text-[#6366f1] hover:bg-[#f5f3ff]" data-cat="nav">Navigation <span className="chip-count text-xs font-bold min-w-[18px] h-[18px] py-0 px-1 rounded-[9px] bg-[rgba(255,255,255,0.25)] text-inherit flex items-center justify-center" id="count-nav"></span></button>
</div>
<div className="grid" id="grid" aria-live="polite"></div>
</div>
</>
);
}<template>
<div class="page">
<div class="header">
<h1 class="heading">Component Library</h1>
<p class="sub">48 components across 6 categories</p>
</div>
<div class="chips" id="chips" role="group" aria-label="Filter by category">
<button class="chip active" data-cat="all">All <span class="chip-count" id="count-all">12</span></button>
<button class="chip" data-cat="layout">Layout <span class="chip-count" id="count-layout"></span></button>
<button class="chip" data-cat="forms">Forms <span class="chip-count" id="count-forms"></span></button>
<button class="chip" data-cat="data">Data <span class="chip-count" id="count-data"></span></button>
<button class="chip" data-cat="feedback">Feedback <span class="chip-count" id="count-feedback"></span></button>
<button class="chip" data-cat="nav">Navigation <span class="chip-count" id="count-nav"></span></button>
</div>
<div class="grid" id="grid" aria-live="polite"></div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
const ITEMS = [
{ name:'Bento Grid', desc:'Responsive asymmetric grid', cat:'layout', icon:'⬛', bg:'#eff6ff' },
{ name:'Masonry Layout', desc:'Pinterest-style column layout', cat:'layout', icon:'🧱', bg:'#eff6ff' },
{ name:'Dashboard Grid', desc:'Admin panel skeleton layout', cat:'layout', icon:'📐', bg:'#eff6ff' },
{ name:'Floating Label', desc:'Animated placeholder input', cat:'forms', icon:'✏️', bg:'#f0fdf4' },
{ name:'OTP Input', desc:'6-digit code entry field', cat:'forms', icon:'🔢', bg:'#f0fdf4' },
{ name:'Tag Input', desc:'Multi-value chip input', cat:'forms', icon:'🏷️', bg:'#f0fdf4' },
{ name:'Data Table', desc:'Sortable & filterable table', cat:'data', icon:'📊', bg:'#fefce8' },
{ name:'Donut Chart', desc:'SVG donut with tooltip', cat:'data', icon:'🍩', bg:'#fefce8' },
{ name:'Line Chart', desc:'SVG sparkline widget', cat:'data', icon:'📈', bg:'#fefce8' },
{ name:'Toast Queue', desc:'Stacked notification toasts', cat:'feedback', icon:'🔔', bg:'#fdf4ff' },
{ name:'Progress Bar', desc:'Animated step progress bar', cat:'feedback', icon:'📶', bg:'#fdf4ff' },
{ name:'Mega Menu', desc:'Multi-column dropdown nav', cat:'nav', icon:'🗂️', bg:'#fff7ed' },
];
let grid;
let chips;
let active = 'all';
function countByCat(cat) { return cat === 'all' ? ITEMS.length : ITEMS.filter(i => i.cat === cat).length; }
function updateCounts() {
chips.forEach(c => {
const cat = c.dataset.cat;
const span = c.querySelector('.chip-count');
span.textContent = countByCat(cat);
});
}
function render() {
const shown = active === 'all' ? ITEMS : ITEMS.filter(i => i.cat === active);
grid.innerHTML = shown.map(item => `
<div class="card">
<div class="card-icon" style="background:${item.bg}">${item.icon}</div>
<div class="card-name">${item.name}</div>
<div class="card-desc">${item.desc}</div>
<span class="card-tag tag-${item.cat}">${item.cat}</span>
</div>
`).join('');
if (!shown.length) {
grid.innerHTML = '<div class="empty show">No components match this filter.</div>';
}
}
onMounted(() => {
grid = document.getElementById('grid');
chips = document.querySelectorAll('.chip');
chips.forEach(chip => {
chip.addEventListener('click', () => {
chips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
active = chip.dataset.cat;
render();
});
});
updateCounts();
render();
});
</script>
<style scoped>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: #f8fafc; min-height: 100vh; padding: 24px 20px; }
.page { max-width: 680px; margin: 0 auto; }
.header { margin-bottom: 20px; }
.heading { font-size: 22px; font-weight: 800; color: #0f172a; margin-bottom: 4px; }
.sub { font-size: 13px; color: #64748b; }
.chips {
display: flex; flex-wrap: wrap; gap: 8px;
margin-bottom: 20px;
}
.chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 7px 14px; border-radius: 999px;
border: 1.5px solid #e2e8f0;
background: #fff; color: #64748b;
font-size: 13px; font-weight: 600;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.1s;
white-space: nowrap;
user-select: none;
}
.chip:hover { border-color: #6366f1; color: #6366f1; background: #f5f3ff; }
.chip.active { background: #6366f1; border-color: #6366f1; color: #fff; }
.chip-count {
font-size: 10px; font-weight: 700;
min-width: 18px; height: 18px; padding: 0 4px;
border-radius: 9px;
background: rgba(255,255,255,0.25); color: inherit;
display: flex; align-items: center; justify-content: center;
}
.chip:not(.active) .chip-count { background: #f1f5f9; color: #94a3b8; }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
transition: border-color 0.15s, box-shadow 0.15s, opacity 0.2s, transform 0.2s;
}
.card:hover { border-color: #a5b4fc; box-shadow: 0 4px 14px rgba(99,102,241,0.1); }
.card.hidden { opacity: 0; transform: scale(0.94); pointer-events: none; position: absolute; }
.card-icon {
width: 38px; height: 38px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px; margin-bottom: 10px;
}
.card-name { font-size: 13px; font-weight: 700; color: #1e293b; margin-bottom: 3px; }
.card-desc { font-size: 11.5px; color: #64748b; line-height: 1.4; margin-bottom: 8px; }
.card-tag {
display: inline-block;
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px;
padding: 2px 8px; border-radius: 4px;
}
.tag-layout { background: #eff6ff; color: #3b82f6; }
.tag-forms { background: #f0fdf4; color: #22c55e; }
.tag-data { background: #fefce8; color: #eab308; }
.tag-feedback { background: #fdf4ff; color: #a855f7; }
.tag-nav { background: #fff7ed; color: #f97316; }
.empty { grid-column: 1/-1; text-align: center; padding: 40px; color: #94a3b8; font-size: 14px; display: none; }
.empty.show { display: block; }
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-chip-filter',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="page">
<div class="header">
<h1 class="heading">Component Library</h1>
<p class="sub">48 components across 6 categories</p>
</div>
<div class="chips" id="chips" role="group" aria-label="Filter by category">
<button class="chip active" data-cat="all">All <span class="chip-count" id="count-all">12</span></button>
<button class="chip" data-cat="layout">Layout <span class="chip-count" id="count-layout"></span></button>
<button class="chip" data-cat="forms">Forms <span class="chip-count" id="count-forms"></span></button>
<button class="chip" data-cat="data">Data <span class="chip-count" id="count-data"></span></button>
<button class="chip" data-cat="feedback">Feedback <span class="chip-count" id="count-feedback"></span></button>
<button class="chip" data-cat="nav">Navigation <span class="chip-count" id="count-nav"></span></button>
</div>
<div class="grid" id="grid" aria-live="polite"></div>
</div>
`,
styles: [`
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: #f8fafc; min-height: 100vh; padding: 24px 20px; }
.page { max-width: 680px; margin: 0 auto; }
.header { margin-bottom: 20px; }
.heading { font-size: 22px; font-weight: 800; color: #0f172a; margin-bottom: 4px; }
.sub { font-size: 13px; color: #64748b; }
.chips {
display: flex; flex-wrap: wrap; gap: 8px;
margin-bottom: 20px;
}
.chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 7px 14px; border-radius: 999px;
border: 1.5px solid #e2e8f0;
background: #fff; color: #64748b;
font-size: 13px; font-weight: 600;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.1s;
white-space: nowrap;
user-select: none;
}
.chip:hover { border-color: #6366f1; color: #6366f1; background: #f5f3ff; }
.chip.active { background: #6366f1; border-color: #6366f1; color: #fff; }
.chip-count {
font-size: 10px; font-weight: 700;
min-width: 18px; height: 18px; padding: 0 4px;
border-radius: 9px;
background: rgba(255,255,255,0.25); color: inherit;
display: flex; align-items: center; justify-content: center;
}
.chip:not(.active) .chip-count { background: #f1f5f9; color: #94a3b8; }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
transition: border-color 0.15s, box-shadow 0.15s, opacity 0.2s, transform 0.2s;
}
.card:hover { border-color: #a5b4fc; box-shadow: 0 4px 14px rgba(99,102,241,0.1); }
.card.hidden { opacity: 0; transform: scale(0.94); pointer-events: none; position: absolute; }
.card-icon {
width: 38px; height: 38px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px; margin-bottom: 10px;
}
.card-name { font-size: 13px; font-weight: 700; color: #1e293b; margin-bottom: 3px; }
.card-desc { font-size: 11.5px; color: #64748b; line-height: 1.4; margin-bottom: 8px; }
.card-tag {
display: inline-block;
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px;
padding: 2px 8px; border-radius: 4px;
}
.tag-layout { background: #eff6ff; color: #3b82f6; }
.tag-forms { background: #f0fdf4; color: #22c55e; }
.tag-data { background: #fefce8; color: #eab308; }
.tag-feedback { background: #fdf4ff; color: #a855f7; }
.tag-nav { background: #fff7ed; color: #f97316; }
.empty { grid-column: 1/-1; text-align: center; padding: 40px; color: #94a3b8; font-size: 14px; display: none; }
.empty.show { display: block; }
`]
})
export class ChipFilterComponent implements AfterViewInit {
ngAfterViewInit(): void {
const ITEMS = [
{ name:'Bento Grid', desc:'Responsive asymmetric grid', cat:'layout', icon:'⬛', bg:'#eff6ff' },
{ name:'Masonry Layout', desc:'Pinterest-style column layout', cat:'layout', icon:'🧱', bg:'#eff6ff' },
{ name:'Dashboard Grid', desc:'Admin panel skeleton layout', cat:'layout', icon:'📐', bg:'#eff6ff' },
{ name:'Floating Label', desc:'Animated placeholder input', cat:'forms', icon:'✏️', bg:'#f0fdf4' },
{ name:'OTP Input', desc:'6-digit code entry field', cat:'forms', icon:'🔢', bg:'#f0fdf4' },
{ name:'Tag Input', desc:'Multi-value chip input', cat:'forms', icon:'🏷️', bg:'#f0fdf4' },
{ name:'Data Table', desc:'Sortable & filterable table', cat:'data', icon:'📊', bg:'#fefce8' },
{ name:'Donut Chart', desc:'SVG donut with tooltip', cat:'data', icon:'🍩', bg:'#fefce8' },
{ name:'Line Chart', desc:'SVG sparkline widget', cat:'data', icon:'📈', bg:'#fefce8' },
{ name:'Toast Queue', desc:'Stacked notification toasts', cat:'feedback', icon:'🔔', bg:'#fdf4ff' },
{ name:'Progress Bar', desc:'Animated step progress bar', cat:'feedback', icon:'📶', bg:'#fdf4ff' },
{ name:'Mega Menu', desc:'Multi-column dropdown nav', cat:'nav', icon:'🗂️', bg:'#fff7ed' },
];
const grid = document.getElementById('grid');
const chips = document.querySelectorAll('.chip');
let active = 'all';
function countByCat(cat) { return cat === 'all' ? ITEMS.length : ITEMS.filter(i => i.cat === cat).length; }
function updateCounts() {
chips.forEach(c => {
const cat = c.dataset.cat;
const span = c.querySelector('.chip-count');
span.textContent = countByCat(cat);
});
}
function render() {
const shown = active === 'all' ? ITEMS : ITEMS.filter(i => i.cat === active);
grid.innerHTML = shown.map(item => `
<div class="card">
<div class="card-icon" style="background:${item.bg}">${item.icon}</div>
<div class="card-name">${item.name}</div>
<div class="card-desc">${item.desc}</div>
<span class="card-tag tag-${item.cat}">${item.cat}</span>
</div>
`).join('');
if (!shown.length) {
grid.innerHTML = '<div class="empty show">No components match this filter.</div>';
}
}
chips.forEach(chip => {
chip.addEventListener('click', () => {
chips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
active = chip.dataset.cat;
render();
});
});
updateCounts();
render();
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { countByCat, updateCounts, render });
}
}