Display

Tag Group

Accessible group of tag pills built on react-aria-components, styled with the quebi design system. Supports selection, removable tags, links, and disabled state.

tagchippilllabelinteractive

Default

A simple set of static tag pills.

React
TypeScript
Tailwind
react-aria

Selection

Single-select tags; the active tag fills with brand teal.

All
Active
Archived

Removable

Tags with a remove button, driven by controlled state.

Design
Engineering
Product

Multiple selection

JavaScript
Python
Go
Rust

Disabled

Ready
Pending
Done

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import { X } from "lucide-react"
import {
  Button,
  type TagGroupProps,
  type TagListProps,
  type TagProps,
  Tag as TagPrimitive,
  TagGroup as TagGroupPrimitive,
  TagList as TagListPrimitive,
  composeRenderProps,
} from "react-aria-components"
import { cn } from "@/lib/utils"

/**
 * TagGroup — quebi design system
 *
 * Built on react-aria-components. Tags are hairline cyan-tinted pills; the
 * selected state fills with brand teal. Removable tags expose a small remove
 * button. Focus uses the quebi teal ring; disabled dims.
 */
export function TagGroup({ className, ...props }: TagGroupProps) {
  return (
    <TagGroupPrimitive
      data-slot="control"
      className={cn("flex flex-col gap-y-1.5 *:data-[slot=label]:font-medium", className)}
      {...props}
    />
  )
}

export function TagList<T extends object>({ className, ...props }: TagListProps<T>) {
  return (
    <TagListPrimitive
      className={composeRenderProps(className, (className) =>
        cn("flex flex-wrap gap-1.5", className),
      )}
      {...props}
    />
  )
}

export function Tag({ children, className, ...props }: TagProps) {
  const textValue = typeof children === "string" ? children : undefined

  return (
    <TagPrimitive
      textValue={textValue}
      data-slot="control"
      className={composeRenderProps(className, (className, { allowsRemoving, isDisabled }) =>
        cn(
          "inline-flex cursor-default items-center gap-x-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium",
          "border-cyan-500/10 bg-transparent text-quebi-fg-muted",
          "transition-colors duration-150",
          "outline-none focus-visible:ring-2 focus-visible:ring-quebi-brand/50 focus-visible:ring-offset-2 focus-visible:ring-offset-quebi-bg",
          "hover:border-cyan-500/20",
          "data-[selected]:border-quebi-brand data-[selected]:bg-quebi-brand data-[selected]:text-quebi-bg",
          "data-[href]:cursor-pointer",
          allowsRemoving && "pr-1",
          isDisabled && "cursor-not-allowed opacity-50",
          className,
        ),
      )}
      {...props}
    >
      {composeRenderProps(children, (children, { allowsRemoving }) => (
        <>
          {children}
          {allowsRemoving && (
            <Button
              slot="remove"
              className={cn(
                "-mr-0.5 flex size-4 shrink-0 items-center justify-center rounded-full",
                "text-quebi-fg-subtle outline-none transition-colors duration-150",
                "hover:bg-cyan-500/10 hover:text-white",
                "data-[focus-visible]:ring-2 data-[focus-visible]:ring-quebi-brand/50",
                "group-data-[selected]:text-quebi-bg/70 group-data-[selected]:hover:bg-quebi-bg/20 group-data-[selected]:hover:text-quebi-bg",
              )}
            >
              <X className="size-3" strokeWidth={2.5} aria-hidden="true" />
            </Button>
          )}
        </>
      ))}
    </TagPrimitive>
  )
}