# P group 23

> Group with filter label, combobox multi-select, and remove button

A shadcn/ui block from the [@coss](https://coss.com/ui) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/coss/p-group-23).

- Page, with a live preview: https://shadcn.exchange/r/coss/p-group-23
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/coss/p-group-23-light-d1ba20ee062b166b.png
- Registry item: https://coss.com/ui/r/p-group-23.json
- Categories: avatar, badge, button, combobox, group
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://coss.com/ui/r/p-group-23.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@coss/avatar`, `@coss/badge`, `@coss/button`, `@coss/combobox`, `@coss/group`

## Files

### `registry/default/particles/p-group-23.tsx`

```tsx
"use client";

import {
  ChevronsUpDownIcon,
  FunnelIcon,
  SearchIcon,
  XIcon,
} from "lucide-react";
import { useState } from "react";
import { cn } from "@/registry/default/lib/utils";
import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/registry/default/ui/avatar";
import { Badge } from "@/registry/default/ui/badge";
import { Button, buttonVariants } from "@/registry/default/ui/button";
import {
  Combobox,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
  ComboboxTrigger,
} from "@/registry/default/ui/combobox";
import { Group, GroupSeparator, GroupText } from "@/registry/default/ui/group";

type FilterOption = {
  id: string;
  label: string;
  avatar?: string;
};

const members: FilterOption[] = [
  {
    avatar:
      "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=64&h=64&fit=crop&crop=faces",
    id: "alex-chen",
    label: "Alex Chen",
  },
  {
    avatar:
      "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=64&h=64&fit=crop&crop=faces",
    id: "sarah-johnson",
    label: "Sarah Johnson",
  },
  {
    avatar:
      "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=64&h=64&fit=crop&crop=faces",
    id: "marcus-williams",
    label: "Marcus Williams",
  },
  {
    avatar:
      "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=64&h=64&fit=crop&crop=faces",
    id: "emma-davis",
    label: "Emma Davis",
  },
  {
    avatar:
      "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=64&h=64&fit=crop&crop=faces",
    id: "james-miller",
    label: "James Miller",
  },
];

function getInitials(name: string): string {
  const parts = name.trim().split(/\s+/);
  if (parts.length === 1) {
    return parts[0]?.charAt(0).toUpperCase() ?? "";
  }
  const first = parts[0]?.charAt(0) ?? "";
  const last = parts[parts.length - 1]?.charAt(0) ?? "";
  return (first + last).toUpperCase();
}

function MemberAvatar({
  name,
  avatarUrl,
  className,
}: {
  name: string;
  avatarUrl?: string;
  className?: string;
}) {
  return (
    <Avatar className={cn("size-5", className)}>
      {avatarUrl ? <AvatarImage alt={name} src={avatarUrl} /> : null}
      <AvatarFallback className="text-[0.5rem]">
        {getInitials(name)}
      </AvatarFallback>
    </Avatar>
  );
}

export default function Particle() {
  const [selectedMembers, setSelectedMembers] = useState<FilterOption[]>(
    members.slice(0, 2),
  );

  const renderTriggerContent = () => {
    if (selectedMembers.length === 0) return "Select";
    const firstMember = selectedMembers[0];
    const remainingCount = selectedMembers.length - 1;

    return (
      <div className="flex items-center gap-2">
        <MemberAvatar
          avatarUrl={firstMember?.avatar}
          name={firstMember?.label ?? ""}
        />
        <span className="truncate">{firstMember?.label}</span>
        {remainingCount > 0 && (
          <Badge className="tabular-nums" variant="secondary">
            +{remainingCount}
          </Badge>
        )}
      </div>
    );
  };

  return (
    <Group>
      <GroupText
        className={cn(
          buttonVariants({
            size: "sm",
            variant: "outline",
          }),
          "pointer-events-none",
        )}
      >
        <FunnelIcon />
        Member
      </GroupText>
      <GroupSeparator />
      <Combobox
        autoHighlight
        items={members}
        multiple
        onValueChange={(value) => {
          if (Array.isArray(value)) {
            setSelectedMembers(value);
          }
        }}
        value={selectedMembers}
      >
        <ComboboxTrigger
          render={
            <Button
              className={
                selectedMembers.length === 0 ? "justify-between" : undefined
              }
              size="sm"
              variant="outline"
            />
          }
        >
          {renderTriggerContent()}
          {selectedMembers.length === 0 && (
            <ChevronsUpDownIcon className="-me-1!" />
          )}
        </ComboboxTrigger>
        <ComboboxPopup aria-label="Select member">
          <div className="border-b p-2">
            <ComboboxInput
              className="rounded-md before:rounded-[calc(var(--radius-md)-1px)]"
              placeholder="Search members..."
              showTrigger={false}
              startAddon={<SearchIcon />}
            />
          </div>
          <ComboboxEmpty>No members found.</ComboboxEmpty>
          <ComboboxList>
            {(option: FilterOption) => (
              <ComboboxItem key={option.id} value={option}>
                <div className="flex items-center gap-2">
                  <MemberAvatar avatarUrl={option.avatar} name={option.label} />
                  <span>{option.label}</span>
                </div>
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxPopup>
      </Combobox>
      <GroupSeparator />
      <Button
        aria-label="Remove filter"
        onClick={() => setSelectedMembers([])}
        size="icon-sm"
        variant="outline"
      >
        <XIcon />
      </Button>
    </Group>
  );
}
```
