Skip to content
goodtabs UI
BrowseBrowse components

Tag list

Wrapping tags that collapse into a +N count past a limit.

npx shadcn@latest add https://ui.goodtabs.co/r/tag-list.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/tag-list

  • react
  • accessibility
  • and 4 more: motion, tailwind, forms, charts

Two tags shown, the rest counted in a mono chip.

Wrapping

  • next.js
  • react
  • typescript
  • tailwind
  • shadcn
  • motion
  • vercel
  • postgres
  • zod

No limit in play, so tags flow onto new lines.

In a list

Command palette

  • cmdk
  • search
  • keyboard
  • and 1 more: overlay

Date field

  • forms
  • calendar

Stat card

  • dashboard
  • metric
  • kpi
  • and 2 more: card, trend

One row per item, each with its own tags.

Usage

import { TagList } from "@/components/ui/tag-list"

<TagList tags={["react", "motion", "forms"]} max={2} />

Props

PropTypeDefaultDescription
tagsstring[]The tags, in display order.
maxnumber4Tags shown before the rest collapse into +N.
labelstring"Tags"Accessible name for the list.

Source

tag-list/tag-list.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

type TagListProps = Omit<React.ComponentProps<"ul">, "children"> & {
  /** The tags, in the order they should show. */
  tags: string[];
  /** How many tags to show before the rest collapse into a count. */
  max?: number;
  /** Accessible name for the list. */
  label?: string;
};

/**
 * Tags that wrap onto as many lines as they need. Past `max`, the rest collapse
 * into a "+N" chip whose title lists them, and the full list stays in the markup
 * for screen readers.
 */
export function TagList({ tags, max = 4, label = "Tags", className, ...props }: TagListProps) {
  const shown = tags.slice(0, max);
  const hidden = tags.slice(max);
  const chip = "inline-flex h-6 items-center rounded-full border border-border px-2.5 text-xs whitespace-nowrap";

  return (
    <ul aria-label={label} className={cn("m-0 flex list-none flex-wrap gap-1.5 p-0", className)} {...props}>
      {shown.map((tag, i) => (
        <li key={i} className={cn(chip, "bg-muted text-foreground")}>
          {tag}
        </li>
      ))}
      {hidden.length > 0 && (
        <li title={hidden.join(", ")} className={cn(chip, "font-mono text-muted-foreground tabular-nums")}>
          <span aria-hidden="true">+{hidden.length}</span>
          <span className="sr-only">{`and ${hidden.length} more: ${hidden.join(", ")}`}</span>
        </li>
      )}
    </ul>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/tag-list/prompt.md, or the Copy AI prompt button above.

When to use

  • Cards and rows that carry a few labels under a title
  • Any set of short, non-interactive keywords
  • Places where a long tag list must not break the layout

When not to use

  • Tags a person can add or remove: use tag-input
  • Tags that filter a list: use filter-chip

Agent notes

  • Tags are static text in list items, not buttons or links
  • The +N chip carries a title and a screen reader sentence naming the hidden tags
  • Wrapping is plain flex-wrap; set a width on the list or its parent
  • Pass max larger than the count to show everything