React Chip Input Component

Chip Input

React Chip Input component for CoreUI helps you create tag-like multi-value inputs for skills, categories, or recipients with keyboard support, selection, and form integration.

Available in Other JavaScript Frameworks

CoreUI React Chip Input Component is also available for Bootstrap and Vue. Explore framework-specific implementations below:

Overview

The CoreUI React Chip Input component lets users enter multiple values as chips inside a single field. Use it as a tag input, multi-value selector, or token field for skills, categories, email recipients, and more. The chip input component supports keyboard-friendly entry, chip removal, and optional selection for bulk actions.

  • Type values and press Enter or a separator to create chips.
  • Chips can be removable and selectable.
  • The input stays inline and grows as you type.
  • Form integration through a hidden field the browser validates, submitted when name is provided.

When to use Chip Input

Use the React Chip Input component when you need:

  • A tag input or token field for free-form multi-value entry
  • An email or recipient input where users add multiple addresses
  • A skills or category selector in forms
  • A filterable multi-select that integrates with standard HTML forms

Basic example

Use CChipInput to render a multi-value field with predefined chips and inline input.

import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputExample = () => {
  return (
    <CChipInput
      id="skillsInputBasic"
      label="Skills:"
      defaultValue={['JavaScript', 'TypeScript', 'Accessibility']}
      name="chipInputBasicExample"
      placeholder="Add a skill..."
    />
  )
}
import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputExample = () => {
  return (
    <CChipInput
      id="skillsInputBasic"
      label="Skills:"
      defaultValue={['JavaScript', 'TypeScript', 'Accessibility']}
      name="chipInputBasicExample"
      placeholder="Add a skill..."
    />
  )
}

Note: CChipInput renders the inline text input automatically, and a hidden <textarea> that carries the chip values separated by commas. When name is provided, that field takes part in standard form submission.

Variants

Use contextual chip classes inside React Chip Input to represent categories, status, or priority.

import React from 'react'
import { CChipInput } from '@coreui/react'

const variants: Record<string, string> = {
  feature: 'theme-primary',
  approved: 'theme-success',
  'needs review': 'theme-warning',
  blocking: 'theme-danger',
}

export const ChipInputVariantsExample = () => {
  return (
    <CChipInput
      defaultValue={['Feature', 'Approved', 'Needs review', 'Blocking']}
      chipClassName={(value) => variants[value.trim().toLowerCase()] || ''}
      name="issues"
      placeholder="Add label..."
    />
  )
}
import React from 'react'
import { CChipInput } from '@coreui/react'

const variants: Record<string, string> = {
  feature: 'theme-primary',
  approved: 'theme-success',
  'needs review': 'theme-warning',
  blocking: 'theme-danger',
}

export const ChipInputVariantsExample = () => {
  return (
    <CChipInput
      defaultValue={['Feature', 'Approved', 'Needs review', 'Blocking']}
      chipClassName={(value) => variants[value.trim().toLowerCase()] || ''}
      name="issues"
      placeholder="Add label..."
    />
  )
}

In the example below, chip color is assigned dynamically based on the chip text using the chipClassName callback.

import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputClassNameExample = () => {
  return (
    <CChipInput
      defaultValue={['Feature', 'Blocking', 'Approved']}
      chipClassName={(value) => {
        const variants: Record<string, string> = {
          approved: 'theme-success',
          blocking: 'theme-danger',
          feature: 'theme-primary',
        }
        return variants[value.trim().toLowerCase()] || 'theme-secondary'
      }}
      placeholder="Type and press Enter"
    />
  )
}
import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputClassNameExample = () => {
  return (
    <CChipInput
      defaultValue={['Feature', 'Blocking', 'Approved']}
      chipClassName={(value) => {
        const variants: Record<string, string> = {
          approved: 'theme-success',
          blocking: 'theme-danger',
          feature: 'theme-primary',
        }
        return variants[value.trim().toLowerCase()] || 'theme-secondary'
      }}
      placeholder="Type and press Enter"
    />
  )
}

Sizes

Use size="sm" and size="lg" to match surrounding form controls. Default size is provided without a size modifier.

import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputSizesExample = () => {
  return (
    <>
      <div className="mb-4">
        <CChipInput
          id="skillsInputSm"
          label="Small"
          size="sm"
          defaultValue={['HTML']}
          placeholder="Add small tag..."
        />
      </div>

      <div className="mb-4">
        <CChipInput
          id="skillsInputMd"
          label="Default"
          defaultValue={['JavaScript']}
          placeholder="Add default tag..."
        />
      </div>

      <div>
        <CChipInput
          id="skillsInputLg"
          label="Large"
          size="lg"
          defaultValue={['TypeScript']}
          placeholder="Add large tag..."
        />
      </div>
    </>
  )
}
import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputSizesExample = () => {
  return (
    <>
      <div className="mb-4">
        <CChipInput
          id="skillsInputSm"
          label="Small"
          size="sm"
          defaultValue={['HTML']}
          placeholder="Add small tag..."
        />
      </div>

      <div className="mb-4">
        <CChipInput
          id="skillsInputMd"
          label="Default"
          defaultValue={['JavaScript']}
          placeholder="Add default tag..."
        />
      </div>

      <div>
        <CChipInput
          id="skillsInputLg"
          label="Large"
          size="lg"
          defaultValue={['TypeScript']}
          placeholder="Add large tag..."
        />
      </div>
    </>
  )
}

Separator

By default, chips are created when the user types a comma ,. Change the separator with the separator property.

import React, { useState } from 'react'
import { CChipInput } from '@coreui/react-pro'

export const ChipInputSeparatorExample = () => {
  const [tags, setTags] = useState(['Item 1'])

  return (
    <>
      <CChipInput
        value={tags}
        onChange={setTags}
        separator=";"
        placeholder="Type and use ; to separate..."
      />
      <p className="fg-2 mt-3">
        <small>Use semicolon (;) as separator</small>
      </p>
    </>
  )
}
import React, { useState } from 'react'
import { CChipInput } from '@coreui/react-pro'

export const ChipInputSeparatorExample = () => {
  const [tags, setTags] = useState(['Item 1'])

  return (
    <>
      <CChipInput
        value={tags}
        onChange={setTags}
        separator=";"
        placeholder="Type and use ; to separate..."
      />
      <p className="fg-2 mt-3">
        <small>Use semicolon (;) as separator</small>
      </p>
    </>
  )
}

No separator

Set separator to an empty string or null to stop chips being created as the user types. They must press Enter instead.

import React, { useState } from 'react'
import { CChipInput } from '@coreui/react-pro'

export const ChipInputNoSeparatorExample = () => {
  const [tags, setTags] = useState(['Item 1'])

  return (
    <>
      <CChipInput
        value={tags}
        onChange={setTags}
        separator={null}
        placeholder="Type and press Enter..."
      />
      <p className="fg-2 mt-3">
        <small>Separator disabled — use the Enter key to create chips</small>
      </p>
    </>
  )
}
import React, { useState } from 'react'
import { CChipInput } from '@coreui/react-pro'

export const ChipInputNoSeparatorExample = () => {
  const [tags, setTags] = useState(['Item 1'])

  return (
    <>
      <CChipInput
        value={tags}
        onChange={setTags}
        separator={null}
        placeholder="Type and press Enter..."
      />
      <p className="fg-2 mt-3">
        <small>Separator disabled — use the Enter key to create chips</small>
      </p>
    </>
  )
}

Maximum chips

Limit how many chips can be added with maxChips.

import React, { useState } from 'react'
import { CChipInput } from '@coreui/react-pro'

export const ChipInputMaxChipsExample = () => {
  const [tags, setTags] = useState(['Item 1'])

  return (
    <>
      <CChipInput value={tags} onChange={setTags} maxChips={5} placeholder="Max 5 chips" />
      <p className="fg-2 mt-3">
        <small>
          {tags.length}/5 chips — {5 - tags.length} remaining
        </small>
      </p>
    </>
  )
}
import React, { useState } from 'react'
import { CChipInput } from '@coreui/react-pro'

export const ChipInputMaxChipsExample = () => {
  const [tags, setTags] = useState(['Item 1'])

  return (
    <>
      <CChipInput value={tags} onChange={setTags} maxChips={5} placeholder="Max 5 chips" />
      <p className="fg-2 mt-3">
        <small>
          {tags.length}/5 chips — {5 - tags.length} remaining
        </small>
      </p>
    </>
  )
}

Create on blur

Typing a value and leaving the input creates a chip. Set createOnBlur to false to require Enter.

import React, { useState } from 'react'
import { CChipInput } from '@coreui/react-pro'

export const ChipInputCreateOnBlurExample = () => {
  const [tags, setTags] = useState(['Item 1'])

  return (
    <>
      <CChipInput
        value={tags}
        onChange={setTags}
        createOnBlur={false}
        placeholder="Must press Enter"
      />
      <p className="fg-2 mt-3">
        <small>Blurring will not create a chip — you must press Enter</small>
      </p>
    </>
  )
}
import React, { useState } from 'react'
import { CChipInput } from '@coreui/react-pro'

export const ChipInputCreateOnBlurExample = () => {
  const [tags, setTags] = useState(['Item 1'])

  return (
    <>
      <CChipInput
        value={tags}
        onChange={setTags}
        createOnBlur={false}
        placeholder="Must press Enter"
      />
      <p className="fg-2 mt-3">
        <small>Blurring will not create a chip — you must press Enter</small>
      </p>
    </>
  )
}

Empty state

Start with just the input and let users add chips as they type.

import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputEmptyStateExample = () => {
  return <CChipInput name="tags" placeholder="Start typing tags..." />
}
import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputEmptyStateExample = () => {
  return <CChipInput name="tags" placeholder="Start typing tags..." />
}

With form field

Wrap the control in <CFormField> to lay it out with its label and description, and to get validation feedback in the right place. The label goes inside the container as .chip-input-label — pass it through the label property, and the component points it at the text input it builds.

import React from 'react'
import { CChipInput, CFormField, CFormText } from '@coreui/react'

export const ChipInputFormFieldExample = () => {
  return (
    <CFormField>
      <CChipInput
        id="techStackField"
        label="Tech stack"
        defaultValue={['Vue', 'Vite']}
        name="techStack"
        placeholder="Add package..."
      />
      <CFormText>Press Enter or comma to add a value.</CFormText>
    </CFormField>
  )
}
import React from 'react'
import { CChipInput, CFormField, CFormText } from '@coreui/react'

export const ChipInputFormFieldExample = () => {
  return (
    <CFormField>
      <CChipInput
        id="techStackField"
        label="Tech stack"
        defaultValue={['Vue', 'Vite']}
        name="techStack"
        placeholder="Add package..."
      />
      <CFormText>Press Enter or comma to add a value.</CFormText>
    </CFormField>
  )
}

With label

Use a standard form label for accessibility.

import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputWithLabelExample = () => {
  return (
    <div className="mb-4">
      <label className="form-label" htmlFor="techStackInput">
        Tech stack
      </label>
      <CChipInput
        id="techStackInput"
        name="techStack"
        defaultValue={['Vue', 'Vite']}
        placeholder="Add package..."
      />
      <div className="form-text">Press Enter or comma to add a value.</div>
    </div>
  )
}
import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputWithLabelExample = () => {
  return (
    <div className="mb-4">
      <label className="form-label" htmlFor="techStackInput">
        Tech stack
      </label>
      <CChipInput
        id="techStackInput"
        name="techStack"
        defaultValue={['Vue', 'Vite']}
        placeholder="Add package..."
      />
      <div className="form-text">Press Enter or comma to add a value.</div>
    </div>
  )
}

Validation

CChipInput carries its chips in a hidden <textarea> the browser validates, so required stops the submit of an empty field and the browser shows its message over the component. The state shows on the frame and on the text input, and the message after the frame is linked to the text input.

import React, { useState } from 'react'
import { CButton, CChipInput, CForm } from '@coreui/react'

export const ChipInputValidationExample = () => {
  const [recipientsState, setRecipientsState] = useState<string | undefined>('invalid')

  return (
    <CForm className="row g-4" noValidate validate>
      <div className="col-md-6">
        <CChipInput
          feedbackInvalid="Add at least one topic."
          id="validationTopics"
          label="Topics"
          name="topics"
          placeholder="Add a topic..."
          required
        />
      </div>
      <div className="col-md-6">
        <CChipInput
          defaultValue={['[email protected]', 'bob@example']}
          feedback="bob@example is not a full address."
          id="validationRecipients"
          label="Recipients"
          name="recipients"
          onValidationStateChange={setRecipientsState}
          validationState={recipientsState}
        />
      </div>
      <div className="col-12">
        <CButton color="primary" type="submit">
          Submit
        </CButton>
      </div>
    </CForm>
  )
}
import React, { useState } from 'react'
import { CButton, CChipInput, CForm } from '@coreui/react'

export const ChipInputValidationExample = () => {
  const [recipientsState, setRecipientsState] = useState<string | undefined>('invalid')

  return (
    <CForm className="row g-4" noValidate validate>
      <div className="col-md-6">
        <CChipInput
          feedbackInvalid="Add at least one topic."
          id="validationTopics"
          label="Topics"
          name="topics"
          placeholder="Add a topic..."
          required
        />
      </div>
      <div className="col-md-6">
        <CChipInput
          defaultValue={['[email protected]', 'bob@example']}
          feedback="bob@example is not a full address."
          id="validationRecipients"
          label="Recipients"
          name="recipients"
          onValidationStateChange={setRecipientsState}
          validationState={recipientsState}
        />
      </div>
      <div className="col-12">
        <CButton color="primary" type="submit">
          Submit
        </CButton>
      </div>
    </CForm>
  )
}

The state comes from three places, in this order:

  • validationState: 'valid', 'invalid' or a state you added to $form-validation-states. While it is set, the component draws it instead of what the browser reports. While it is 'invalid', the hidden field blocks the submit with the text of the linked message, or Invalid value. when there is none, unless the component is disabled or readOnly.
  • A validation that reports the hidden field invalid: a submit, checkValidity() or reportValidity(), which CForm and the stepper call as well. From then on the frame and the text input show it invalid whenever the value is invalid, until the form is reset, and the message after the frame shows.
  • In a form with validate, the :user-invalid state of the hidden field, and with validate="valid" also its :user-valid state.

A state you give is a verdict on the chips it was given for. When the user adds or removes a chip, or the form is reset, the component calls onValidationStateChange(undefined); pass your state setter there to drop the state, as the second field above does. A change your code makes through value keeps it, and so does an action that changes nothing, such as entering a value the field already holds. A reset asks once it has run, so a state you set from a reset listener on the form gets the same call, and your handler decides whether to drop it. A native reset of the form puts back the chips given by defaultValue or the CChip children, and leaves a controlled value to you.

Disabled

Set disabled state on the component to make the input and chips non-interactive. Combine it with removable={false} for disabled, non-removable chips.

import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputDisabledExample = () => {
  return (
    <CChipInput
      defaultValue={['Read only', 'Locked']}
      disabled
      removable={false}
      placeholder="Input disabled"
    />
  )
}
import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputDisabledExample = () => {
  return (
    <CChipInput
      defaultValue={['Read only', 'Locked']}
      disabled
      removable={false}
      placeholder="Input disabled"
    />
  )
}

Readonly

Use readonly state when chips should stay visible and focusable, but values must not change.

import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputReadonlyExample = () => {
  return (
    <CChipInput
      defaultValue={['JavaScript', 'TypeScript']}
      readOnly
      placeholder="Read-only values"
    />
  )
}
import React from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputReadonlyExample = () => {
  return (
    <CChipInput
      defaultValue={['JavaScript', 'TypeScript']}
      readOnly
      placeholder="Read-only values"
    />
  )
}

Selectable chips

Enable selection for chips managed by Chip Input. In this example, onSelect updates a live list of selected values below the field.

import React, { useState } from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputSelectableExample = () => {
  const [selected, setSelected] = useState<string[]>([])

  return (
    <>
      <CChipInput
        defaultValue={['Design', 'Backend', 'QA', 'DevOps']}
        selectable
        onSelect={setSelected}
        placeholder="Select chips"
      />
      <p className="mt-3 mb-0 small fg-2">
        Selected: {selected.length > 0 ? selected.join(', ') : 'None'}
      </p>
    </>
  )
}
import React, { useState } from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputSelectableExample = () => {
  const [selected, setSelected] = useState<string[]>([])

  return (
    <>
      <CChipInput
        defaultValue={['Design', 'Backend', 'QA', 'DevOps']}
        selectable
        onSelect={setSelected}
        placeholder="Select chips"
      />
      <p className="mt-3 mb-0 small fg-2">
        Selected: {selected.length > 0 ? selected.join(', ') : 'None'}
      </p>
    </>
  )
}

Paste support

Several values can be pasted at once. If the pasted text contains the separator, it is split into one chip per value.

import React, { useState } from 'react'
import { CChipInput } from '@coreui/react-pro'

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

  return (
    <>
      <CChipInput value={tags} onChange={setTags} placeholder="Try pasting: apple,banana,cherry" />
      <p className="fg-2 mt-3">
        <small>Paste comma-separated values to create multiple chips at once</small>
      </p>
    </>
  )
}
import React, { useState } from 'react'
import { CChipInput } from '@coreui/react-pro'

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

  return (
    <>
      <CChipInput value={tags} onChange={setTags} placeholder="Try pasting: apple,banana,cherry" />
      <p className="fg-2 mt-3">
        <small>Paste comma-separated values to create multiple chips at once</small>
      </p>
    </>
  )
}

Controlled and uncontrolled

The chip list works either way, and the prop you reach for is what picks the mode.

propwho owns the chips
UncontrolleddefaultValue, or the CChip childrenthe chip input, from that starting point
Controlledvalueyour component

onChange fires in both modes with the new list of values — so you can watch the chips without taking them over. A native reset of the form puts back the chips the input started with: uncontrolled, it sets them and calls onChange when that changes the chips; controlled, it asks for the first value through onChange and shows your value until you take it.

Uncontrolled

Seed the list with defaultValue, as the basic example does, or with CChip children, and let the chip input manage it.

<CChipInput defaultValue={['React', 'Vue']} />

Controlled

Pass value and the chips render exactly what you pass — adding or removing a chip only fires onChange. Feed the change back, or nothing moves.

import React, { useState } from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputControlledExample = () => {
  const [values, setValues] = useState<string[]>(['React'])

  return (
    <>
      <CChipInput value={values} onChange={setValues} placeholder="Controlled chip input" />
      <p className="mt-3 mb-0 small fg-2">Current value: {values.join(', ') || '-'}</p>
    </>
  )
}
import React, { useState } from 'react'
import { CChipInput } from '@coreui/react'

export const ChipInputControlledExample = () => {
  const [values, setValues] = useState<string[]>(['React'])

  return (
    <>
      <CChipInput value={values} onChange={setValues} placeholder="Controlled chip input" />
      <p className="mt-3 mb-0 small fg-2">Current value: {values.join(', ') || '-'}</p>
    </>
  )
}

Use it when the values belong to something else: form state, a query you persist, or a list shared with another control.

Accessibility

The React Chip Input component follows WAI-ARIA patterns to ensure the chip input is fully usable with keyboards and assistive technologies.

  • Associate a <label> with the ghost input for screen readers.
  • Chips are focusable and keyboard navigable.
  • Each remove button the component creates is named with ariaRemoveLabel and the chip’s value, Remove <value> by default. Put {label} where the value goes, or pass a function of the value for another word order.
  • Adding and removing chips is announced through announce() in the live region of the page, or of the modal dialog around the set, one message per update: the values the set did not hold before, or else the values it no longer holds, joined with commas as in Vue, Svelte added, and nothing while the set is hidden; customize the wording with the ariaAddedAnnouncement and ariaRemovedAnnouncement props. A function in place of the word receives the values and returns the whole message, so a page can put the word first, agree it with the number of chips or give a count for a long list, as in (values) => values.length > 3 ? values.length + ' chips added' : values.join(', ') + ' added'.
  • The container is a generic element, so disabled and readOnly are carried by the inner input rather than stamped on it.
  • Use a descriptive aria-label or <label> on the chip input container to communicate its purpose (e.g., “Add skills” or “Recipients”).

The full treatment is on the accessibility page: semantic structure, ARIA attributes, focus management, visual indicators and keyboard support.

API

Check out the documentation below for a comprehensive guide to all the props you can use with the components mentioned here.