{"id":13810142,"url":"https://github.com/sersavan/shadcn-multi-select-component","last_synced_at":"2025-04-05T09:04:46.682Z","repository":{"id":232692274,"uuid":"784949119","full_name":"sersavan/shadcn-multi-select-component","owner":"sersavan","description":"A multi-select component designed with shadcn/ui","archived":false,"fork":false,"pushed_at":"2024-09-12T22:49:47.000Z","size":215,"stargazers_count":584,"open_issues_count":7,"forks_count":26,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-10-17T06:24:19.894Z","etag":null,"topics":["multi-select","multi-select-container","multi-select-dropdown","multi-selection","multi-selector","nextjs","radix-ui","reactjs","shadcn-ui"],"latest_commit_sha":null,"homepage":"https://shadcn-multi-select-component.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sersavan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-04-10T22:11:14.000Z","updated_at":"2024-10-17T06:10:44.000Z","dependencies_parsed_at":"2024-11-20T00:15:49.907Z","dependency_job_id":"7187ac4b-144a-4a28-9d54-b1fd9585833d","html_url":"https://github.com/sersavan/shadcn-multi-select-component","commit_stats":null,"previous_names":["sersavan/shadcn-multi-select-component"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sersavan%2Fshadcn-multi-select-component","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sersavan%2Fshadcn-multi-select-component/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sersavan%2Fshadcn-multi-select-component/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sersavan%2Fshadcn-multi-select-component/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sersavan","download_url":"https://codeload.github.com/sersavan/shadcn-multi-select-component/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247312068,"owners_count":20918344,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["multi-select","multi-select-container","multi-select-dropdown","multi-selection","multi-selector","nextjs","radix-ui","reactjs","shadcn-ui"],"created_at":"2024-08-04T02:00:46.636Z","updated_at":"2025-04-05T09:04:46.650Z","avatar_url":"https://github.com/sersavan.png","language":"TypeScript","readme":"## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=sersavan/shadcn-multi-select-component\u0026type=Date)](https://star-history.com/#sersavan/shadcn-multi-select-component\u0026Date)\n\n## Multi-Select Component Setup in Next.js\n\n### Prerequisites\n\nEnsure you have a Next.js project set up. If not, create one:\n\n```bash\nnpx create-next-app my-app --typescript\ncd my-app\n```\n\n### Step 1: Install shadcn Components\n\nInstall required shadcn components:\n\n```bash\nnpx shadcn@latest init\nnpx shadcn@latest add command popover button separator badge\n```\n\n### Step 2: Create the Multi-Select Component\n\nCreate `multi-select.tsx` in your `components` directory:\n\n```tsx\n// src/components/multi-select.tsx\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n  CheckIcon,\n  XCircle,\n  ChevronDown,\n  XIcon,\n  WandSparkles,\n} from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from \"@/components/ui/command\";\n\n/**\n * Variants for the multi-select component to handle different styles.\n * Uses class-variance-authority (cva) to define different styles based on \"variant\" prop.\n */\nconst multiSelectVariants = cva(\n  \"m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-foreground/10 text-foreground bg-card hover:bg-card/80\",\n        secondary:\n          \"border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80\",\n        inverted: \"inverted\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n);\n\n/**\n * Props for MultiSelect component\n */\ninterface MultiSelectProps\n  extends React.ButtonHTMLAttributes\u003cHTMLButtonElement\u003e,\n    VariantProps\u003ctypeof multiSelectVariants\u003e {\n  /**\n   * An array of option objects to be displayed in the multi-select component.\n   * Each option object has a label, value, and an optional icon.\n   */\n  options: {\n    /** The text to display for the option. */\n    label: string;\n    /** The unique value associated with the option. */\n    value: string;\n    /** Optional icon component to display alongside the option. */\n    icon?: React.ComponentType\u003c{ className?: string }\u003e;\n  }[];\n\n  /**\n   * Callback function triggered when the selected values change.\n   * Receives an array of the new selected values.\n   */\n  onValueChange: (value: string[]) =\u003e void;\n\n  /** The default selected values when the component mounts. */\n  defaultValue?: string[];\n\n  /**\n   * Placeholder text to be displayed when no values are selected.\n   * Optional, defaults to \"Select options\".\n   */\n  placeholder?: string;\n\n  /**\n   * Animation duration in seconds for the visual effects (e.g., bouncing badges).\n   * Optional, defaults to 0 (no animation).\n   */\n  animation?: number;\n\n  /**\n   * Maximum number of items to display. Extra selected items will be summarized.\n   * Optional, defaults to 3.\n   */\n  maxCount?: number;\n\n  /**\n   * The modality of the popover. When set to true, interaction with outside elements\n   * will be disabled and only popover content will be visible to screen readers.\n   * Optional, defaults to false.\n   */\n  modalPopover?: boolean;\n\n  /**\n   * If true, renders the multi-select component as a child of another component.\n   * Optional, defaults to false.\n   */\n  asChild?: boolean;\n\n  /**\n   * Additional class names to apply custom styles to the multi-select component.\n   * Optional, can be used to add custom styles.\n   */\n  className?: string;\n}\n\nexport const MultiSelect = React.forwardRef\u003c\n  HTMLButtonElement,\n  MultiSelectProps\n\u003e(\n  (\n    {\n      options,\n      onValueChange,\n      variant,\n      defaultValue = [],\n      placeholder = \"Select options\",\n      animation = 0,\n      maxCount = 3,\n      modalPopover = false,\n      asChild = false,\n      className,\n      ...props\n    },\n    ref\n  ) =\u003e {\n    const [selectedValues, setSelectedValues] =\n      React.useState\u003cstring[]\u003e(defaultValue);\n    const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);\n    const [isAnimating, setIsAnimating] = React.useState(false);\n\n    const handleInputKeyDown = (\n      event: React.KeyboardEvent\u003cHTMLInputElement\u003e\n    ) =\u003e {\n      if (event.key === \"Enter\") {\n        setIsPopoverOpen(true);\n      } else if (event.key === \"Backspace\" \u0026\u0026 !event.currentTarget.value) {\n        const newSelectedValues = [...selectedValues];\n        newSelectedValues.pop();\n        setSelectedValues(newSelectedValues);\n        onValueChange(newSelectedValues);\n      }\n    };\n\n    const toggleOption = (option: string) =\u003e {\n      const newSelectedValues = selectedValues.includes(option)\n        ? selectedValues.filter((value) =\u003e value !== option)\n        : [...selectedValues, option];\n      setSelectedValues(newSelectedValues);\n      onValueChange(newSelectedValues);\n    };\n\n    const handleClear = () =\u003e {\n      setSelectedValues([]);\n      onValueChange([]);\n    };\n\n    const handleTogglePopover = () =\u003e {\n      setIsPopoverOpen((prev) =\u003e !prev);\n    };\n\n    const clearExtraOptions = () =\u003e {\n      const newSelectedValues = selectedValues.slice(0, maxCount);\n      setSelectedValues(newSelectedValues);\n      onValueChange(newSelectedValues);\n    };\n\n    const toggleAll = () =\u003e {\n      if (selectedValues.length === options.length) {\n        handleClear();\n      } else {\n        const allValues = options.map((option) =\u003e option.value);\n        setSelectedValues(allValues);\n        onValueChange(allValues);\n      }\n    };\n\n    return (\n      \u003cPopover\n        open={isPopoverOpen}\n        onOpenChange={setIsPopoverOpen}\n        modal={modalPopover}\n      \u003e\n        \u003cPopoverTrigger asChild\u003e\n          \u003cButton\n            ref={ref}\n            {...props}\n            onClick={handleTogglePopover}\n            className={cn(\n              \"flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [\u0026_svg]:pointer-events-auto\",\n              className\n            )}\n          \u003e\n            {selectedValues.length \u003e 0 ? (\n              \u003cdiv className=\"flex justify-between items-center w-full\"\u003e\n                \u003cdiv className=\"flex flex-wrap items-center\"\u003e\n                  {selectedValues.slice(0, maxCount).map((value) =\u003e {\n                    const option = options.find((o) =\u003e o.value === value);\n                    const IconComponent = option?.icon;\n                    return (\n                      \u003cBadge\n                        key={value}\n                        className={cn(\n                          isAnimating ? \"animate-bounce\" : \"\",\n                          multiSelectVariants({ variant })\n                        )}\n                        style={{ animationDuration: `${animation}s` }}\n                      \u003e\n                        {IconComponent \u0026\u0026 (\n                          \u003cIconComponent className=\"h-4 w-4 mr-2\" /\u003e\n                        )}\n                        {option?.label}\n                        \u003cXCircle\n                          className=\"ml-2 h-4 w-4 cursor-pointer\"\n                          onClick={(event) =\u003e {\n                            event.stopPropagation();\n                            toggleOption(value);\n                          }}\n                        /\u003e\n                      \u003c/Badge\u003e\n                    );\n                  })}\n                  {selectedValues.length \u003e maxCount \u0026\u0026 (\n                    \u003cBadge\n                      className={cn(\n                        \"bg-transparent text-foreground border-foreground/1 hover:bg-transparent\",\n                        isAnimating ? \"animate-bounce\" : \"\",\n                        multiSelectVariants({ variant })\n                      )}\n                      style={{ animationDuration: `${animation}s` }}\n                    \u003e\n                      {`+ ${selectedValues.length - maxCount} more`}\n                      \u003cXCircle\n                        className=\"ml-2 h-4 w-4 cursor-pointer\"\n                        onClick={(event) =\u003e {\n                          event.stopPropagation();\n                          clearExtraOptions();\n                        }}\n                      /\u003e\n                    \u003c/Badge\u003e\n                  )}\n                \u003c/div\u003e\n                \u003cdiv className=\"flex items-center justify-between\"\u003e\n                  \u003cXIcon\n                    className=\"h-4 mx-2 cursor-pointer text-muted-foreground\"\n                    onClick={(event) =\u003e {\n                      event.stopPropagation();\n                      handleClear();\n                    }}\n                  /\u003e\n                  \u003cSeparator\n                    orientation=\"vertical\"\n                    className=\"flex min-h-6 h-full\"\n                  /\u003e\n                  \u003cChevronDown className=\"h-4 mx-2 cursor-pointer text-muted-foreground\" /\u003e\n                \u003c/div\u003e\n              \u003c/div\u003e\n            ) : (\n              \u003cdiv className=\"flex items-center justify-between w-full mx-auto\"\u003e\n                \u003cspan className=\"text-sm text-muted-foreground mx-3\"\u003e\n                  {placeholder}\n                \u003c/span\u003e\n                \u003cChevronDown className=\"h-4 cursor-pointer text-muted-foreground mx-2\" /\u003e\n              \u003c/div\u003e\n            )}\n          \u003c/Button\u003e\n        \u003c/PopoverTrigger\u003e\n        \u003cPopoverContent\n          className=\"w-auto p-0\"\n          align=\"start\"\n          onEscapeKeyDown={() =\u003e setIsPopoverOpen(false)}\n        \u003e\n          \u003cCommand\u003e\n            \u003cCommandInput\n              placeholder=\"Search...\"\n              onKeyDown={handleInputKeyDown}\n            /\u003e\n            \u003cCommandList\u003e\n              \u003cCommandEmpty\u003eNo results found.\u003c/CommandEmpty\u003e\n              \u003cCommandGroup\u003e\n                \u003cCommandItem\n                  key=\"all\"\n                  onSelect={toggleAll}\n                  className=\"cursor-pointer\"\n                \u003e\n                  \u003cdiv\n                    className={cn(\n                      \"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary\",\n                      selectedValues.length === options.length\n                        ? \"bg-primary text-primary-foreground\"\n                        : \"opacity-50 [\u0026_svg]:invisible\"\n                    )}\n                  \u003e\n                    \u003cCheckIcon className=\"h-4 w-4\" /\u003e\n                  \u003c/div\u003e\n                  \u003cspan\u003e(Select All)\u003c/span\u003e\n                \u003c/CommandItem\u003e\n                {options.map((option) =\u003e {\n                  const isSelected = selectedValues.includes(option.value);\n                  return (\n                    \u003cCommandItem\n                      key={option.value}\n                      onSelect={() =\u003e toggleOption(option.value)}\n                      className=\"cursor-pointer\"\n                    \u003e\n                      \u003cdiv\n                        className={cn(\n                          \"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary\",\n                          isSelected\n                            ? \"bg-primary text-primary-foreground\"\n                            : \"opacity-50 [\u0026_svg]:invisible\"\n                        )}\n                      \u003e\n                        \u003cCheckIcon className=\"h-4 w-4\" /\u003e\n                      \u003c/div\u003e\n                      {option.icon \u0026\u0026 (\n                        \u003coption.icon className=\"mr-2 h-4 w-4 text-muted-foreground\" /\u003e\n                      )}\n                      \u003cspan\u003e{option.label}\u003c/span\u003e\n                    \u003c/CommandItem\u003e\n                  );\n                })}\n              \u003c/CommandGroup\u003e\n              \u003cCommandSeparator /\u003e\n              \u003cCommandGroup\u003e\n                \u003cdiv className=\"flex items-center justify-between\"\u003e\n                  {selectedValues.length \u003e 0 \u0026\u0026 (\n                    \u003c\u003e\n                      \u003cCommandItem\n                        onSelect={handleClear}\n                        className=\"flex-1 justify-center cursor-pointer\"\n                      \u003e\n                        Clear\n                      \u003c/CommandItem\u003e\n                      \u003cSeparator\n                        orientation=\"vertical\"\n                        className=\"flex min-h-6 h-full\"\n                      /\u003e\n                    \u003c/\u003e\n                  )}\n                  \u003cCommandItem\n                    onSelect={() =\u003e setIsPopoverOpen(false)}\n                    className=\"flex-1 justify-center cursor-pointer max-w-full\"\n                  \u003e\n                    Close\n                  \u003c/CommandItem\u003e\n                \u003c/div\u003e\n              \u003c/CommandGroup\u003e\n            \u003c/CommandList\u003e\n          \u003c/Command\u003e\n        \u003c/PopoverContent\u003e\n        {animation \u003e 0 \u0026\u0026 selectedValues.length \u003e 0 \u0026\u0026 (\n          \u003cWandSparkles\n            className={cn(\n              \"cursor-pointer my-2 text-foreground bg-background w-3 h-3\",\n              isAnimating ? \"\" : \"text-muted-foreground\"\n            )}\n            onClick={() =\u003e setIsAnimating(!isAnimating)}\n          /\u003e\n        )}\n      \u003c/Popover\u003e\n    );\n  }\n);\n\nMultiSelect.displayName = \"MultiSelect\";\n```\n\n### Step 3: Integrate the Component\n\nUpdate `page.tsx`:\n\n```tsx\n// src/app/page.tsx\n\n\"use client\";\n\nimport React, { useState } from \"react\";\nimport { MultiSelect } from \"@/components/multi-select\";\nimport { Cat, Dog, Fish, Rabbit, Turtle } from \"lucide-react\";\n\nconst frameworksList = [\n  { value: \"react\", label: \"React\", icon: Turtle },\n  { value: \"angular\", label: \"Angular\", icon: Cat },\n  { value: \"vue\", label: \"Vue\", icon: Dog },\n  { value: \"svelte\", label: \"Svelte\", icon: Rabbit },\n  { value: \"ember\", label: \"Ember\", icon: Fish },\n];\n\nfunction Home() {\n  const [selectedFrameworks, setSelectedFrameworks] = useState\u003cstring[]\u003e([\"react\", \"angular\"]);\n\n  return (\n    \u003cdiv className=\"p-4 max-w-xl\"\u003e\n      \u003ch1 className=\"text-2xl font-bold mb-4\"\u003eMulti-Select Component\u003c/h1\u003e\n      \u003cMultiSelect\n        options={frameworksList}\n        onValueChange={setSelectedFrameworks}\n        defaultValue={selectedFrameworks}\n        placeholder=\"Select frameworks\"\n        variant=\"inverted\"\n        animation={2}\n        maxCount={3}\n      /\u003e\n      \u003cdiv className=\"mt-4\"\u003e\n        \u003ch2 className=\"text-xl font-semibold\"\u003eSelected Frameworks:\u003c/h2\u003e\n        \u003cul className=\"list-disc list-inside\"\u003e\n          {selectedFrameworks.map((framework) =\u003e (\n            \u003cli key={framework}\u003e{framework}\u003c/li\u003e\n          ))}\n        \u003c/ul\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n\nexport default Home;\n```\n\n### Step 4: Run Your Project\n\n```bash\nnpm run dev\n```\n","funding_links":[],"categories":["TypeScript","Libs and Components","Components","Components \u0026 Libraries"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsersavan%2Fshadcn-multi-select-component","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsersavan%2Fshadcn-multi-select-component","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsersavan%2Fshadcn-multi-select-component/lists"}