Category Cloud
alphav0.2.0Flex-wrapped cloud of clickable category chips with optional counts — single-select, toggleable, controlled or uncontrolled.
Context
Sidebar / inline filter affordance. Differs from `entity-picker` (popover-driven select with search) by being always-visible and count-augmented. Differs from `filter-panel` (multi-section schema-driven filter panel) by handling a single category dimension. Generic over category items via `string[]` shorthand or full `CategoryCloudItem[]` shape. Migration origin: kasder kas-social-front-v0 NewsMagazineGrid.tsx sidebar Categories block. Composed by `magazine-layout` in the news-domain family.
Installation
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/category-cloudAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/category-cloud-fixturesPreview
Filter by department
Demo source
Usage
When to use
Reach for CategoryCloud when you have a fixed set of categories / tags / segments and want users to filter by clicking one. Always-visible flex-wrap of pill chips with optional counts. Single-select; re-clicking the active chip clears it (toggleable).
Minimal example
import { CategoryCloud } from "@/components/category-cloud";
<CategoryCloud items={["All", "Tech", "Design", "Engineering"]} />;String-array shorthand is desugared internally to [{ value, label }]. No counts in this form.
With counts
<CategoryCloud
items={[
{ value: "tech", label: "Technology", count: 12 },
{ value: "design", label: "Design", count: 8 },
]}
title="Categories"
/>;Controlled
const [active, setActive] = useState<string | null>(null);
<CategoryCloud
items={categories}
value={active}
onChange={setActive}
/>;For URL-state sync, drive value from your router and update via onChange. Pass null to clear.
Custom count format
<CategoryCloud
items={items}
formatCount={(c) => c > 999 ? ` (${(c/1000).toFixed(1)}k)` : ` (${c})`}
/>;Disable toggle (always-active)
By default, re-clicking the active chip clears the selection. Pass toggleable={false} to disable this — useful when nullisn't a valid filter state in your app.
Accessibility
- Each chip is a real
<button>witharia-pressed. - Container is
role="group";aria-labeldefaults totitleif provided. - Tab moves between chips; Enter / Space activates. Focus-visible ring per chip.
- Heading level configurable via
headingAs={"h2" | "h3" | "h4"}. Defaulth3.
Stable references
The card is React.memo-wrapped. Hoist the items array outside the parent render or memoize it to prevent unnecessary re-renders.
Features
- Always-visible flex-wrap of pill chips (vs entity-picker's popover-driven select)
- Optional inline counts via `count` field on items; configurable format via `formatCount` callback
- String-array shorthand: `items={["All", "Tech"]}` desugars to `{value, label}` form
- Controlled-or-uncontrolled value (single-select; pass null to clear)
- Toggleable — re-clicking active clears (configurable via `toggleable` prop)
- Native `<button>` semantics with `aria-pressed`; Tab + Enter/Space work natively
- Optional editorial-header title with `pb-2 border-b` separator (matches the magazine sidebar header rhythm)
- Heading semantic level configurable via `headingAs` (h2 | h3 | h4)
- ARIA group with auto-derived label (from `title` or explicit `ariaLabel`)
- React.memo wrapped