Category Cloud
ilinxa/category-cloudFreeBlock
Flex-wrapped cloud of clickable category chips with optional counts — single-select, toggleable, controlled or uncontrolled.
Install it
npx shadcn@latest add https://ui.ilinxa.com/r/category-cloud.jsonSource
1"use client";23import { memo, useCallback, useMemo, useState } from "react";4import { cn } from "@/lib/utils";5import { CategoryChip } from "./parts/category-chip";6import type {7 CategoryCloudItem,8 CategoryCloudProps,9 NormalizedItem,10} from "./types";1112const defaultFormatCount = (count: number): string => ` (${count})`;1314const normalizeItems = (15 items: CategoryCloudItem[] | string[],16): NormalizedItem[] =>17 items.map((entry) => {18 if (typeof entry === "string") {19 return { value: entry, label: entry, count: undefined };20 }21 return {22 value: entry.value,23 label: entry.label ?? entry.value,24 count: entry.count,25 };26 });2728/**29 * CategoryCloud — always-visible flex-wrap of clickable category chips30 * with optional counts. Single-select, controlled-or-uncontrolled.31 *32 * Pass `string[]` as shorthand for items without counts:33 * <CategoryCloud items={["All", "Tech", "Design"]} />34 *35 * Or full form for counts + display labels:36 * <CategoryCloud items={[{ value: "tech", label: "Technology", count: 12 }]} />37 */38function CategoryCloudImpl(props: CategoryCloudProps) {39 const {40 items,41 value: controlledValue,42 defaultValue = null,43 onChange,44 toggleable = true,45 title,46 headingAs = "h3",47 formatCount = defaultFormatCount,48 ariaLabel,49 className,50 titleClassName,51 } = props;5253 const [internalValue, setInternalValue] = useState<string | null>(defaultValue);54 const isControlled = controlledValue !== undefined;55 const value = isControlled ? controlledValue : internalValue;5657 const normalized = useMemo(() => normalizeItems(items), [items]);5859 const handleClick = useCallback(60 (next: string) => {61 const resolved =62 toggleable && value === next ? null : next;63 if (!isControlled) setInternalValue(resolved);64 onChange?.(resolved);65 },66 [isControlled, onChange, toggleable, value],67 );6869 const HeadingTag = headingAs;70 const groupLabel = ariaLabel ?? title ?? "Categories";7172 return (73 <div className={className}>74 {title ? (75 <HeadingTag76 className={cn(77 "mb-4 border-b border-border pb-2 font-serif text-lg font-bold text-foreground",78 titleClassName,79 )}80 >81 {title}82 </HeadingTag>83 ) : null}84 <div85 role="group"86 aria-label={groupLabel}87 className="flex flex-wrap gap-2"88 >89 {normalized.map((item) => (90 <CategoryChip91// … truncated
What it pulls in
Other registry items
Files it writes
More from ilinxa
All 184 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Account Switcheraccount-switcher | ilinxa | Blocks | Free | 1 dep · 10 files | |
| Account Switcher (deprecated alias)account-switcher-01 | ilinxa | Blocks | Free | no deps | |
| Account Switcher — fixturesaccount-switcher-fixtures | ilinxa | Blocks | Free | no deps | |
| App Sidebarapp-sidebar | ilinxa | Blocks | Free | 1 dep · 37 files | |
| App Sidebar — fixturesapp-sidebar-fixtures | ilinxa | Blocks | Free | no deps | |
| Rich Text Editor (deprecated alias)article-body-01 | ilinxa | Blocks | Free | no deps | |
| Article Metaarticle-meta | ilinxa | Blocks | Free | 1 dep · 4 files | |
| Article Meta (deprecated alias)article-meta-01 | ilinxa | Blocks | Free | no deps |
