Chip Filter — Free HTML CSS JS Snippet

Chip Filter · Navigation · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Pill-shaped chips
border-radius: 999px creates perfect pills regardless of label length. Active chips switch to solid indigo fill with a smooth CSS transition.
Count badges
Each chip shows the item count for its category. Inactive badges use a light background; active badges inherit white text on the indigo fill.
Live grid re-render
Clicking a chip replaces grid.innerHTML with only matching cards — simple, fast, and zero DOM diffing complexity for small datasets.
Empty state
When a filter returns no items the grid shows a clear empty-state message instead of a confusing blank area.
Category tag on cards
Each card has a colour-coded category badge so the category is visible after filtering (useful when items span multiple tags in future extensions).
Accessible group semantics
Chip container has role="group" and aria-label. The grid has aria-live="polite" so screen readers announce filter result changes.
Data-driven
All categories and items live in a single ITEMS array. Adding new categories requires no logic changes — only new data and a chip button.
Hover card lift
Cards animate with border-color and box-shadow transitions on hover, signalling interactivity without distracting from the filter flow.

About this UI Snippet

Chip Filter — HTML CSS JavaScript

Screenshot of the Chip Filter snippet rendered live

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

  1. 1
    Click any category chipThe chip switches to the filled indigo active state and the card grid instantly re-renders with only matching components.
  2. 2
    Click "All"The "All" chip activates and all 12 cards are shown. The count badge on the chip shows the total item count.
  3. 3
    Read the count badgesEach chip shows a small badge with the number of items in that category, so users know what to expect before filtering.
  4. 4
    Hover a cardCards show a subtle indigo border and a soft shadow lift to indicate they are interactive links in a real implementation.
  5. 5
    Add your own itemsExtend the ITEMS array with your own objects — each needs name, desc, cat (matching a chip data-cat), icon, and bg color.
  6. 6
    Add 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

Component libraries
Filter a UI kit by category — layout, forms, navigation, feedback — exactly as shown. Pair each card with a modal to show component details on click.
Job boards and marketplaces
Filter listings by role, location, or salary range. Combine with tag input to allow multi-select filtering with typed tags.
Blog and article archives
Filter posts by topic. Each chip represents a category; the grid shows article cards. Pair with a search box for keyword + category filtering.
Product catalogues
Filter products by department. Each card is a product card with price, image, and add-to-cart. Chip filter drives the visible subset.
Portfolio sites
Filter projects by skill or technology. Combine with a masonry grid layout so project screenshots fill space naturally.

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

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>
  <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>
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>
  <!-- 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>
React
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>
    </>
  );
}
React + Tailwind
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>
    </>
  );
}
Vue
<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>
Angular
// @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 });
  }
}