Skip to content
goodtabs UI
BrowseBrowse components

Filter menu

A pill that opens checkbox items with counts and shows how many are checked.

npx shadcn@latest add https://ui.goodtabs.co/r/filter-menu.json

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

A pricing filter with two of four checked, so the trigger reads Pricing · 2.

None selected

Nothing checked: the trigger shows only the label and there is no Clear item.

Many selected

Seven categories with five checked and a custom clear label.

Usage

import { FilterMenu } from "@/components/ui/filter-menu"

const [pricing, setPricing] = useState<string[]>([])

<FilterMenu
  label="Pricing"
  options={[{ value: "free", label: "Free", count: 128 }, { value: "paid", label: "Paid", count: 31 }]}
  value={pricing}
  onValueChange={setPricing}
/>

Props

PropTypeDefaultDescription
options{ value: string; label: string; count?: number }[]The options, in menu order. A count shows in mono on the right.
valuestring[]The checked option values. The component is controlled.
onValueChange(value: string[]) => voidCalled with the new list of checked values, or an empty list on Clear.
labelstring"Filter"Trigger text.
clearLabelstring"Clear"Text of the item at the bottom that unchecks everything.
align"start" | "end""start"Which side of the trigger the menu aligns to.
sizeButton size"sm"Trigger height: sm is 28px, default is 32px. Any other Button prop applies too.

Source

filter-menu/filter-menu.tsx
"use client";

import * as React from "react";
import { ListFilterIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";

export type FilterOption = {
  /** The key reported in onValueChange. */
  value: string;
  /** Menu item text. */
  label: string;
  /** Optional count shown in mono on the right. */
  count?: number;
};

type FilterMenuProps = Omit<React.ComponentProps<typeof Button>, "value" | "onChange" | "children"> & {
  /** The options, in menu order. */
  options: FilterOption[];
  /** The checked option values. */
  value: string[];
  /** Called with the new list of checked values. */
  onValueChange: (value: string[]) => void;
  /** Trigger text. */
  label?: string;
  /** Text of the item at the bottom that unchecks everything. */
  clearLabel?: string;
  /** Which side of the trigger the menu aligns to. */
  align?: "start" | "end";
};

/**
 * A filter control: an outline pill that opens a menu of checkbox items with counts.
 * The trigger shows how many are checked and a Clear item appears when any is.
 */
export function FilterMenu({
  options,
  value,
  onValueChange,
  label = "Filter",
  clearLabel = "Clear",
  align = "start",
  variant = "outline",
  size = "sm",
  className,
  ...props
}: FilterMenuProps) {
  const active = value.length;

  const toggle = (option: string, checked: boolean) => {
    onValueChange(checked ? [...value, option] : value.filter((v) => v !== option));
  };

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button
          type="button"
          variant={variant}
          size={size}
          data-active={active > 0 || undefined}
          className={cn("rounded-full", className)}
          {...props}
        >
          <ListFilterIcon data-icon="inline-start" aria-hidden="true" />
          <span>{label}</span>
          {active > 0 && (
            <>
              <span aria-hidden="true" className="text-muted-foreground">
                ·
              </span>
              <span className="font-mono text-xs">
                {active}
                <span className="sr-only"> selected</span>
              </span>
            </>
          )}
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align={align} className="min-w-48 rounded-md">
        {options.map((option) => {
          const checked = value.includes(option.value);
          return (
            <DropdownMenuCheckboxItem
              key={option.value}
              checked={checked}
              onCheckedChange={(next) => toggle(option.value, next === true)}
              onSelect={(event) => event.preventDefault()}
              className="h-8 rounded-sm focus:bg-muted focus:text-foreground"
            >
              <span className="truncate">{option.label}</span>
              {option.count !== undefined && (
                <span className="ml-auto font-mono text-xs text-muted-foreground">{option.count}</span>
              )}
            </DropdownMenuCheckboxItem>
          );
        })}
        {active > 0 && (
          <>
            <DropdownMenuSeparator />
            <DropdownMenuItem
              onSelect={() => onValueChange([])}
              className="h-8 rounded-sm text-muted-foreground focus:bg-muted focus:text-foreground"
            >
              {clearLabel}
            </DropdownMenuItem>
          </>
        )}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

For AI agents

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

When to use

  • To narrow a list or grid by one facet with a handful of values
  • In a toolbar next to search and sort, one menu per facet
  • When counts help people see what each choice would leave

When not to use

  • For a single on or off facet, use a filter chip or a switch
  • For dozens of values that need search, use a combobox
  • For choosing exactly one value, use a select or a sort menu

Agent notes

  • Controlled only: keep the checked values in state and pass value and onValueChange, so the parent must be a client component
  • The menu stays open while people check items; Clear closes it and calls onValueChange with an empty list
  • The trigger shows the count after a middle dot and sets data-active when anything is checked
  • It extends shadcn's Button, so variant and size apply; the default is an outline sm pill