npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@choice-ui/chips-input

v0.0.6

Published

An input component that allows users to enter and manage multiple values as chips or tags

Readme

ChipsInput

An advanced input component that allows users to enter multiple values as chips (tags). Perfect for tag inputs, email addresses, keywords, or any scenario requiring multiple discrete values.

Import

import { ChipsInput } from "@choice-ui/react"

Features

  • Enter multiple values as removable chips
  • Keyboard navigation and shortcuts (Enter to add, Backspace to remove)
  • Duplicate prevention option
  • Custom chip rendering support
  • Controlled and uncontrolled modes
  • Auto-convert input to chip on blur
  • Click-to-select chip functionality
  • Composition event support for IME
  • Two size variants
  • Disabled state support
  • Fully accessible with proper ARIA attributes

Usage

Basic

const [tags, setTags] = useState<string[]>([])

<ChipsInput
  value={tags}
  onChange={setTags}
  placeholder="Add tags..."
/>

With initial values

<ChipsInput
  value={["React", "TypeScript", "Design System"]}
  onChange={setTags}
  placeholder="Add more tags..."
/>

Prevent duplicates

<ChipsInput
  value={tags}
  onChange={setTags}
  allowDuplicates={false}
  placeholder="Enter unique tags..."
/>

With callbacks

<ChipsInput
  value={tags}
  onChange={setTags}
  onAdd={(tag) => console.log("Added:", tag)}
  onRemove={(tag) => console.log("Removed:", tag)}
/>

Large size

<ChipsInput
  size="large"
  value={tags}
  onChange={setTags}
  placeholder="Large input..."
/>

Disabled state

<ChipsInput
  disabled
  value={["Can't", "Edit", "These"]}
  placeholder="Disabled input"
/>

Custom chip rendering

<ChipsInput
  value={emails}
  onChange={setEmails}
  renderChip={({ chip, isSelected, handleChipClick, handleChipRemoveClick, index }) => (
    <Chip
      key={index}
      selected={isSelected}
      onClick={() => handleChipClick(index)}
      onRemove={() => handleChipRemoveClick(index)}
      prefixElement={<EmailIcon />}
    >
      {chip}
    </Chip>
  )}
/>

With nested content

<ChipsInput
  value={tags}
  onChange={setTags}
>
  <Button
    size="small"
    variant="ghost"
  >
    Clear All
  </Button>
</ChipsInput>

Props

interface ChipsInputProps extends Omit<
  HTMLProps<HTMLDivElement>,
  "value" | "onChange" | "defaultValue" | "size"
> {
  /** Whether to allow duplicate values */
  allowDuplicates?: boolean

  /** Additional content to render after the input */
  children?: React.ReactNode

  /** Whether the input is disabled */
  disabled?: boolean

  /** Input element ID */
  id?: string

  /** Called when a chip is added */
  onAdd?: (value: string) => void

  /** Called when chips array changes */
  onChange?: (value: string[]) => void

  /** Called when a chip is removed */
  onRemove?: (value: string) => void

  /** Placeholder text when no chips */
  placeholder?: string

  /** Custom chip renderer */
  renderChip?: (props: RenderChipProps) => ReactNode

  /** Size variant */
  size?: "default" | "large"

  /** Current chips array */
  value?: string[]
}

interface RenderChipProps {
  /** The chip value */
  chip: string

  /** Whether the input is disabled */
  disabled?: boolean

  /** Handler for chip click */
  handleChipClick: (index: number) => void

  /** Handler for chip removal */
  handleChipRemoveClick: (index: number) => void

  /** Chip index */
  index: number

  /** Whether this chip is selected */
  isSelected: boolean
}
  • Defaults:

    • allowDuplicates: false
    • size: "default"
    • value: [] (when uncontrolled)
  • Behavior:

    • Enter key adds current input as chip
    • Backspace removes last chip when input is empty
    • Click on chip to select it
    • Delete/Backspace removes selected chip
    • Input automatically resizes based on content
    • Blur event converts input to chip

Styling

  • Uses Tailwind CSS via tailwind-variants
  • Slots available: root, input, chip, nesting
  • Focus state shows border highlight
  • Hover state on container
  • Selected chips have visual distinction

Best practices

  • Use clear placeholders to indicate expected input
  • Consider preventing duplicates for most use cases
  • Provide visual feedback with onAdd/onRemove callbacks
  • Keep chip text concise
  • Group related functionality with nested content
  • Use custom rendering for complex chip designs
  • Validate input before adding (in custom implementation)

Examples

Email input with validation

function EmailInput() {
  const [emails, setEmails] = useState<string[]>([])

  const validateEmail = (email: string) => {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  }

  const handleAdd = (email: string) => {
    if (validateEmail(email)) {
      console.log("Valid email added:", email)
    } else {
      console.error("Invalid email:", email)
      // Could show error notification
    }
  }

  return (
    <ChipsInput
      value={emails}
      onChange={setEmails}
      onAdd={handleAdd}
      placeholder="Enter email addresses..."
      allowDuplicates={false}
    />
  )
}

Filter tags with categories

function FilterTags() {
  const [filters, setFilters] = useState<string[]>([])

  return (
    <ChipsInput
      value={filters}
      onChange={setFilters}
      placeholder="Add filters..."
      renderChip={({ chip, ...props }) => {
        const [category, value] = chip.split(":")
        return (
          <Chip
            {...props}
            prefixElement={<span className="text-body-small opacity-60">{category}</span>}
          >
            {value}
          </Chip>
        )
      }}
    />
  )
}

Skill input with suggestions

function SkillInput() {
  const [skills, setSkills] = useState(["JavaScript", "React"])
  const suggestedSkills = ["TypeScript", "Node.js", "CSS", "HTML"]

  return (
    <div className="space-y-2">
      <ChipsInput
        value={skills}
        onChange={setSkills}
        placeholder="Enter your skills..."
        allowDuplicates={false}
      />

      <div className="flex gap-2">
        <span className="text-secondary-foreground text-body-small">Suggestions:</span>
        {suggestedSkills
          .filter((skill) => !skills.includes(skill))
          .map((skill) => (
            <Button
              key={skill}
              size="small"
              variant="ghost"
              onClick={() => setSkills([...skills, skill])}
            >
              + {skill}
            </Button>
          ))}
      </div>
    </div>
  )
}

Controlled with max limit

function LimitedTags() {
  const [tags, setTags] = useState<string[]>([])
  const maxTags = 5

  const handleChange = (newTags: string[]) => {
    if (newTags.length <= maxTags) {
      setTags(newTags)
    } else {
      // Show notification about limit
      console.warn(`Maximum ${maxTags} tags allowed`)
    }
  }

  return (
    <>
      <ChipsInput
        value={tags}
        onChange={handleChange}
        placeholder={`Add up to ${maxTags} tags...`}
      />
      <p className="text-secondary-foreground text-body-small mt-2">
        {tags.length}/{maxTags} tags
      </p>
    </>
  )
}

Notes

  • The component uses useControllableValue for flexible state management
  • Dynamic input width calculation prevents layout shifts
  • Click outside handling deselects any selected chip
  • Composition events are properly handled for IME support
  • The input element is always present for continuous typing
  • Chips are rendered with the Chip component by default but can be customized