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
Enteror 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
nameis 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:
CChipInputrenders the inline text input automatically, and a hidden<textarea>that carries the chip values separated by commas. Whennameis 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, orInvalid value.when there is none, unless the component isdisabledorreadOnly.- A validation that reports the hidden field invalid: a submit,
checkValidity()orreportValidity(), whichCFormand 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-invalidstate of the hidden field, and withvalidate="valid"also its:user-validstate.
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.
| prop | who owns the chips | |
|---|---|---|
| Uncontrolled | defaultValue, or the CChip children | the chip input, from that starting point |
| Controlled | value | your 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
ariaRemoveLabeland 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 inVue, Svelte added, and nothing while the set is hidden; customize the wording with theariaAddedAnnouncementandariaRemovedAnnouncementprops. 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
disabledandreadOnlyare carried by the inner input rather than stamped on it. - Use a descriptive
aria-labelor<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.