Installation
npx @park-ui/cli add tags-inputAdd Component
Copy the code snippet below into you components folder.
'use client'
import { TagsInput, useTagsInputContext } from '@ark-ui/react/tags-input'
import { XIcon } from 'lucide-react'
import type { ComponentProps } from 'react'
import { createStyleContext } from 'styled-system/jsx'
import { tagsInput } from 'styled-system/recipes'
const { withProvider, withContext } = createStyleContext(tagsInput)
export type RootProps = ComponentProps<typeof Root>
export type ItemProps = ComponentProps<typeof Item>
export const Root = withProvider(TagsInput.Root, 'root')
export const RootProvider = withProvider(TagsInput.RootProvider, 'root')
export const ClearTrigger = withContext(TagsInput.ClearTrigger, 'clearTrigger', {
defaultProps: { children: <XIcon /> },
})
export const Control = withContext(TagsInput.Control, 'control')
export const HiddenInput = TagsInput.HiddenInput
export const Input = withContext(TagsInput.Input, 'input')
export const Item = withContext(TagsInput.Item, 'item')
export const ItemDeleteTrigger = withContext(TagsInput.ItemDeleteTrigger, 'itemDeleteTrigger', {
defaultProps: { children: <XIcon /> },
})
export const ItemInput = withContext(TagsInput.ItemInput, 'itemInput')
export const ItemPreview = withContext(TagsInput.ItemPreview, 'itemPreview')
export const ItemText = withContext(TagsInput.ItemText, 'itemText')
export const Label = withContext(TagsInput.Label, 'label')
export { TagsInputContext as Context } from '@ark-ui/react/tags-input'
export interface TagsInputItemsProps extends Omit<ItemProps, 'value' | 'index'> {}
export const Items = (props: TagsInputItemsProps) => {
const context = useTagsInputContext()
return context.value.map((item, index) => (
<Item key={index} index={index} value={item} {...props}>
<ItemPreview>
<ItemText>{item}</ItemText>
<ItemDeleteTrigger />
</ItemPreview>
<ItemInput />
</Item>
))
}
Integrate Recipe
Integrate this recipe in to your Panda config.
import { tagsInputAnatomy } from '@ark-ui/react/tags-input'
import { defineSlotRecipe } from '@pandacss/dev'
export const tagsInput = defineSlotRecipe({
className: 'tags-input',
slots: tagsInputAnatomy.keys(),
base: {
root: {
display: 'flex',
flexDirection: 'column',
gap: '1.5',
width: 'full',
},
label: {
textStyle: 'label',
},
control: {
'--focus-color': 'colors.colorPalette.solid.bg',
'--error-color': 'colors.error',
'--input-height': 'var(--tags-input-height)',
minH: 'var(--tags-input-height)',
px: 'var(--tags-input-px)',
alignItems: 'center',
borderRadius: 'l2',
display: 'flex',
flexWrap: 'wrap',
pos: 'relative',
transitionDuration: 'normal',
transitionProperty: 'border-color, box-shadow',
_disabled: {
opacity: '0.5',
},
_invalid: {
borderColor: 'var(--error-color)',
},
},
clearTrigger: {
boxSize: 'calc(var(--tags-input-item-height) / 1.5)',
alignItems: 'center',
borderRadius: 'l1',
color: 'fg.muted',
display: 'flex',
focusRingWidth: '2px',
focusVisibleRing: 'inside',
justifyContent: 'center',
cursor: { base: 'button', _disabled: 'initial' },
_icon: {
boxSize: '5',
},
},
input: {
px: 'calc(var(--tags-input-item-px) / 1.25)',
height: 'var(--tags-input-item-height)',
flex: '1',
minWidth: '20',
outline: 'none',
_readOnly: {
display: 'none',
},
},
itemInput: {
px: 'var(--tags-input-item-px)',
height: 'var(--tags-input-item-height)',
lineHeight: '1',
minWidth: '2ch',
outline: 'none',
verticalAlign: 'middle',
},
itemDeleteTrigger: {
display: 'flex',
borderRadius: 'l1',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
_hover: {
bg: 'colorPalette.plain.bg.hover',
},
},
itemPreview: {
height: 'var(--tags-input-item-height)',
px: 'var(--tags-input-item-px)',
alignItems: 'center',
borderRadius: 'l1',
display: 'inline-flex',
userSelect: 'none',
},
itemText: {
lineClamp: '1',
},
},
defaultVariants: {
size: 'md',
variant: 'outline',
},
variants: {
variant: {
outline: {
control: {
borderWidth: '1px',
_focus: {
outlineWidth: '1px',
outlineStyle: 'solid',
outlineColor: 'var(--focus-color)',
borderColor: 'var(--focus-color)',
_invalid: {
outlineColor: 'var(--error-color)',
borderColor: 'var(--error-color)',
},
},
},
itemPreview: {
bg: 'colorPalette.subtle.bg',
color: 'colorPalette.subtle.fg',
_highlighted: {
bg: 'colorPalette.subtle.bg.hover',
},
},
},
subtle: {
control: {
bg: 'gray.subtle.bg',
color: 'gray.subtle.fg',
borderWidth: '1px',
borderColor: 'transparent',
_focus: {
outlineWidth: '1px',
outlineStyle: 'solid',
outlineColor: 'var(--focus-color)',
borderColor: 'var(--focus-color)',
_invalid: {
outlineColor: 'var(--error-color)',
borderColor: 'var(--error-color)',
},
},
},
itemPreview: {
bg: 'gray.surface.bg',
borderWidth: '1px',
_highlighted: {
bg: 'gray.surface.bg.hover',
borderColor: 'gray.surface.border.hover',
},
},
},
surface: {
control: {
bg: 'gray.surface.bg',
borderWidth: '1px',
borderColor: 'gray.surface.border',
_focus: {
outlineWidth: '1px',
outlineStyle: 'solid',
outlineColor: 'var(--focus-color)',
borderColor: 'var(--focus-color)',
_invalid: {
outlineColor: 'var(--error-color)',
borderColor: 'var(--error-color)',
},
},
},
itemPreview: {
bg: 'colorPalette.subtle.bg',
color: 'colorPalette.subtle.fg',
_highlighted: {
bg: 'colorPalette.subtle.bg.hover',
},
},
},
},
size: {
xs: {
root: {
'--tags-input-height': 'sizes.8',
'--tags-input-px': 'spacing.1.5',
'--tags-input-item-height': 'sizes.5',
'--tags-input-item-px': 'spacing.1',
_icon: { boxSize: '3' },
textStyle: 'xs',
},
control: { gap: '1' },
itemPreview: { gap: '1' },
itemDeleteTrigger: { boxSize: '3.5', me: '-1px' },
},
sm: {
root: {
'--tags-input-height': 'sizes.9',
'--tags-input-px': 'spacing.1.5',
'--tags-input-item-height': 'sizes.6',
'--tags-input-item-px': 'spacing.1.5',
_icon: { boxSize: '3.5' },
textStyle: 'sm',
},
control: { gap: '1' },
itemPreview: { gap: '1' },
itemDeleteTrigger: { boxSize: '4.5', me: '-0.5' },
},
md: {
root: {
'--tags-input-height': 'sizes.10',
'--tags-input-px': 'spacing.1.5',
'--tags-input-item-height': 'sizes.7',
'--tags-input-item-px': 'spacing.2',
_icon: { boxSize: '3.5' },
textStyle: 'sm',
},
control: { gap: '1.5' },
itemPreview: { gap: '1' },
itemDeleteTrigger: { boxSize: '5', me: '-1' },
},
lg: {
root: {
'--tags-input-height': 'sizes.11',
'--tags-input-px': 'spacing.1.5',
'--tags-input-item-height': 'sizes.8',
'--tags-input-item-px': 'spacing.2.5',
_icon: { boxSize: '4' },
textStyle: 'md',
},
control: { gap: '1.5' },
itemPreview: { gap: '1' },
itemDeleteTrigger: { boxSize: '6', me: '-1.5' },
},
},
},
})
Usage
import { TagsInput } from '@/components/ui'
<TagsInput.Root>
<TagsInput.Label />
<TagsInput.Control>
<TagsInput.Item>
<TagsInput.ItemPreview>
<TagsInput.ItemText />
<TagsInput.ItemDeleteTrigger />
</TagsInput.ItemPreview>
<TagsInput.ItemInput />
</TagsInput.Item>
<TagsInput.Input />
</TagsInput.Control>
</TagsInput.Root>
Shortcuts
The TagsInput component also provides a set of shortcuts for common use cases.
TagsInputItems
The TagsInputItems shortcut renders all tag items automatically based on the current value.
<TagsInput.Context>
{({ value }) =>
value.map((tag, index) => (
<TagsInput.Item key={index} index={index} value={tag}>
<TagsInput.ItemPreview>
<TagsInput.ItemText>{tag}</TagsInput.ItemText>
<TagsInput.ItemDeleteTrigger />
</TagsInput.ItemPreview>
<TagsInput.ItemInput />
</TagsInput.Item>
))
}
</TagsInput.Context>
This might be more concise, if you don't need to customize the items:
<TagsInput.Items />
Examples
Sizes
Use the size prop to adjust the size of the tags input.
Variants
Use the variant prop to change the visual style of the tags input.
Props
Root
| Prop | Default | Type |
|---|---|---|
variant | 'outline' | 'outline' | 'subtle' | 'surface' |
size | 'md' | 'xs' | 'sm' | 'md' | 'lg' |
addOnPaste | false | booleanWhether to add a tag when you paste values into the tag input |
delimiter | \,\ | string | RegExpThe character that serves has: - event key to trigger the addition of a new tag - character used to split tags when pasting into the input |
editable | true | booleanWhether a tag can be edited after creation, by pressing `Enter` or double clicking. |
max | Infinity | numberThe max number of tags |
allowOverflow | booleanWhether to allow tags to exceed max. In this case, we'll attach `data-invalid` to the root | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
autoFocus | booleanWhether the input should be auto-focused | |
blurBehavior | 'clear' | 'add'The behavior of the tags input when the input is blurred - `"add"`: add the input value as a new tag - `"clear"`: clear the input value | |
defaultInputValue | stringThe initial tag input value when rendered. Use when you don't need to control the tag input value. | |
defaultValue | string[]The initial tag value when rendered. Use when you don't need to control the tag value. | |
disabled | booleanWhether the tags input should be disabled | |
form | stringThe associate form of the underlying input element. | |
id | stringThe unique identifier of the machine. | |
ids | Partial<{
root: string
input: string
hiddenInput: string
clearBtn: string
label: string
control: string
item: (opts: ItemProps) => string
itemDeleteTrigger: (opts: ItemProps) => string
itemInput: (opts: ItemProps) => string
}>The ids of the elements in the tags input. Useful for composition. | |
inputValue | stringThe controlled tag input's value | |
invalid | booleanWhether the tags input is invalid | |
maxLength | numberThe max length of the input. | |
name | stringThe name attribute for the input. Useful for form submissions | |
onFocusOutside | (event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the component | |
onHighlightChange | (details: HighlightChangeDetails) => voidCallback fired when a tag is highlighted by pointer or keyboard navigation | |
onInputValueChange | (details: InputValueChangeDetails) => voidCallback fired when the input value is updated | |
onInteractOutside | (event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component | |
onPointerDownOutside | (event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the component | |
onValueChange | (details: ValueChangeDetails) => voidCallback fired when the tag values is updated | |
onValueInvalid | (details: ValidityChangeDetails) => voidCallback fired when the max tag count is reached or the `validateTag` function returns `false` | |
readOnly | booleanWhether the tags input should be read-only | |
required | booleanWhether the tags input is required | |
translations | IntlTranslationsSpecifies the localized strings that identifies the accessibility elements and their states | |
validate | (details: ValidateArgs) => booleanReturns a boolean that determines whether a tag can be added. Useful for preventing duplicates or invalid tag values. | |
value | string[]The controlled tag value |
Item
| Prop | Default | Type |
|---|---|---|
index* | string | number | |
value* | string | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
disabled | boolean |