taglite
v0.2.0
Published
A lightweight, dependency-free React tag input component.
Readme
taglite
A lightweight, dependency-free React tag input with a focused API and flexible interactions.
Type a tag, press Enter or comma, and keep going.
taglite is a controlled React component for collecting, validating, normalizing, and removing tags. It has zero runtime dependencies, ships with TypeScript types, and supports keyboard-first workflows without imposing an autocomplete or form framework.
Why taglite?
- Small by default — zero runtime dependencies and no animation or icon libraries.
- Controlled and predictable — the parent owns the tag array through value and onChange.
- Flexible input rules — custom separators, duplicate handling, normalization, validation, limits, and paste parsing.
- Ready for real interfaces — built-in themes, RTL support, read-only and disabled states, native input attributes, and forwarded refs.
- Easy to extend — custom tag, remove, and clear icons plus lifecycle callbacks for additions, removals, invalid tags, and clearing.
Built-in themes
The component includes eight theme values: default (a backward-compatible alias of light), light, dark, cupcake, emerald, corporate, retro, and dracula.
Installation
npm install tagliteOr use another package manager:
yarn add taglite
pnpm add taglitetaglite supports React >=18 and includes its own public TypeScript declarations.
Quick start
import { useState } from 'react'
import { SimpleTagInput } from 'taglite'
import 'taglite/style.css'
export default function Example() {
const [tags, setTags] = useState<string[]>([])
return (
<SimpleTagInput
value={tags}
onChange={setTags}
placeholder="Add a technology..."
/>
)
}The component is controlled through value and onChange; the tag array is never stored internally. The text currently being typed is temporary UI state managed by the component.
Common interactions
Add tags with the keyboard
By default, pressing Enter or , converts the current input into a tag. Empty values are ignored.
<SimpleTagInput
value={tags}
onChange={setTags}
separators={['Enter', ',']}
/>React + Enter -> React
Next.js + , -> Next.jsPaste multiple tags
Comma-separated, newline-separated, and mixed input can be pasted in one operation:
React, Next.js
TypeScript, Tailwind CSS['React', 'Next.js', 'TypeScript', 'Tailwind CSS']The same processing pipeline applies to pasted values:
normalize -> validate -> duplicate check -> maxTags -> onChangeCombine features
import { useState } from 'react'
import { SimpleTagInput } from 'taglite'
export default function Example() {
const [tags, setTags] = useState<string[]>([])
return (
<SimpleTagInput
value={tags}
onChange={setTags}
placeholder="Add a technology..."
hintText="Enter, comma, or paste multiple tags"
separators={['Enter', ',']}
maxTags={8}
allowDuplicates={false}
normalizeTag={tag => tag.trim().toLowerCase()}
validateTag={tag =>
tag.length >= 2
? true
: 'Tag must contain at least 2 characters'
}
onInvalidTag={(tag, reason) => {
console.log('Invalid tag:', tag, reason)
}}
onTagAdd={(tag, index) => {
console.log('Added:', tag, index)
}}
onTagRemove={(tag, index) => {
console.log('Removed:', tag, index)
}}
acceptOnBlur
clearable
onClear={() => console.log('All tags cleared')}
theme="dark"
/>
)
}API reference
Core props
| Prop | Type | Default | Description | | --- | --- | --- | --- | | value | string[] | required | Controlled tag list | | onChange | (tags: string[]) => void | required | Called when tags change | | direction | 'ltr' or 'rtl' | 'ltr' | Text direction | | theme | SimpleTagInputTheme | 'light' | Built-in visual theme | | accentColor | string | — | Custom accent color while preserving the selected theme | | placeholder | string | 'Add a new tag...' | Input placeholder | | hintText | ReactNode | 'Press Enter to add a tag' | Focus helper text | | separators | string[] | ['Enter', ','] | Keys that create tags | | maxTags | number | — | Maximum number of tags | | allowDuplicates | boolean | false | Allow duplicate tags | | normalizeTag | (tag: string) => string | — | Normalizes tags before validation | | validateTag | (tag: string) => boolean or string | — | Validates tags | | onInvalidTag | (tag, reason?) => void | — | Called for validation failures | | onTagAdd | (tag, index) => void | — | Called after a tag is added | | onTagRemove | (tag, index) => void | — | Called after a tag is removed | | acceptOnBlur | boolean | false | Add current input on blur | | clearable | boolean | false | Show clear-all button | | clearIcon | ReactNode | built-in SVG | Custom clear icon | | onClear | () => void | — | Called after clearing all tags | | tagIcon | ReactNode | built-in SVG | Custom tag icon | | removeIcon | ReactNode | built-in SVG | Custom remove icon | | removeButtonProps | button attributes | — | Additional remove-button attributes | | readOnly | native input prop | false | Prevent tag editing | | disabled | native input prop | false | Disable interaction | | ref | Ref<HTMLInputElement> | — | Ref to the native input |
SimpleTagInput also accepts standard InputHTMLAttributes<HTMLInputElement> props unless they conflict with the controlled value and tag-level onChange API.
value and onChange
value: string[]
onChange: (tags: string[]) => voidvalue is the source of truth. onChange is called whenever the list changes, and the component does not mutate the existing array.
const [tags, setTags] = useState<string[]>([
'React',
'Next.js',
])
<SimpleTagInput
value={tags}
onChange={setTags}
/>direction
direction?: 'ltr' | 'rtl'Controls text direction. Use rtl for Persian, Arabic, Hebrew, and other right-to-left interfaces.
<SimpleTagInput
direction="rtl"
value={tags}
onChange={setTags}
/>theme
theme?:
| 'default'
| 'light'
| 'dark'
| 'cupcake'
| 'emerald'
| 'corporate'
| 'retro'
| 'dracula'default is a backward-compatible alias of light. The default theme is light.
<SimpleTagInput
theme="dracula"
value={tags}
onChange={setTags}
/>accentColor
accentColor?: stringSets a custom accent color for the component while preserving the selected theme as the base design.
The color is used to automatically derive related UI colors such as focus states, borders, tags, hover states, and action colors.
<SimpleTagInput
value={tags}
onChange={setTags}
accentColor="#7B61E8"
/>You can use any valid CSS color value, including hex, RGB, RGBA, and HSL:
<SimpleTagInput
value={tags}
onChange={setTags}
accentColor="rgb(123, 97, 232)"
/>accentColor can also be combined with any built-in theme. In this case, the selected theme remains the base design while accentColor overrides its accent colors.
<SimpleTagInput
value={tags}
onChange={setTags}
theme="dark"
accentColor="#7B61E8"
/>For example, the combination above uses the dark theme with a custom purple accent.
placeholder and hintText
placeholder?: string
hintText?: ReactNodeThe placeholder appears inside the input. hintText is rendered in the helper area while the component is focused.
<SimpleTagInput
placeholder="Add technology..."
hintText="Press Enter or comma to add a tag"
value={tags}
onChange={setTags}
/>hintText also accepts JSX:
<SimpleTagInput
hintText={<span>Add your technology tags</span>}
value={tags}
onChange={setTags}
/>separators
separators?: string[]Defines which KeyboardEvent.key values create a tag. The default is ['Enter', ','].
<SimpleTagInput separators={['Enter']} value={tags} onChange={setTags} />
<SimpleTagInput separators={['Enter', 'Tab']} value={tags} onChange={setTags} />
<SimpleTagInput separators={['Enter', ';']} value={tags} onChange={setTags} />maxTags and allowDuplicates
maxTags?: number
allowDuplicates?: booleanmaxTags ignores additional tags after the limit is reached; existing tags are never removed automatically. allowDuplicates defaults to false and controls whether an already-present tag can be added again.
<SimpleTagInput
value={tags}
onChange={setTags}
maxTags={5}
allowDuplicates={false}
/>With duplicates disabled, adding React, React, and React produces one tag. With allowDuplicates enabled, all three can be added.
normalizeTag
normalizeTag?: (tag: string) => stringTransforms a tag before validation and duplicate checking. This is useful for trimming or normalizing case:
<SimpleTagInput
value={tags}
onChange={setTags}
normalizeTag={tag => tag.trim().toLowerCase()}
/>Input REACT becomes react. With allowDuplicates={false}, React, react, and REACT are treated as the same tag when using tag => tag.toLowerCase().
validateTag and onInvalidTag
validateTag?: (tag: string) => boolean | string
onInvalidTag?: (tag: string, reason?: string) => voidValidation runs after normalization. Return true to accept a tag, false to reject it without a reason, or a string to reject it and provide that reason.
<SimpleTagInput
value={tags}
onChange={setTags}
validateTag={tag =>
tag.length >= 3
? true
: 'Tag must contain at least 3 characters'
}
onInvalidTag={(tag, reason) => {
console.log(tag, reason)
}}
/>onTagAdd and onTagRemove
onTagAdd?: (tag: string, index: number) => void
onTagRemove?: (tag: string, index: number) => voidonTagAdd receives the final normalized tag and its resulting index. It is not called for empty, invalid, duplicate, or max-limit-rejected tags.
onTagRemove is triggered by clicking a tag's remove button or pressing Backspace while the input is empty. Its index is the tag's index before removal.
<SimpleTagInput
value={tags}
onChange={setTags}
onTagAdd={(tag, index) => console.log('Added:', tag, index)}
onTagRemove={(tag, index) => console.log('Removed:', tag, index)}
/>acceptOnBlur
acceptOnBlur?: booleanWhen enabled, the component attempts to add the current input when focus leaves it. The same normalization, validation, duplicate, and maxTags rules apply.
<SimpleTagInput
value={tags}
onChange={setTags}
acceptOnBlur
/>clearable, onClear, and clearIcon
clearable?: boolean
onClear?: () => void
clearIcon?: ReactNodeclearable shows a clear-all button when at least one tag exists. Clicking it calls onChange([]) and then onClear, if provided. The clear action is disabled in read-only and disabled modes.
<SimpleTagInput
value={tags}
onChange={setTags}
clearable
onClear={() => console.log('All tags cleared')}
clearIcon={<span aria-hidden="true">×</span>}
/>tagIcon and removeIcon
tagIcon?: ReactNode
removeIcon?: ReactNodeBoth props accept any React node. A runtime icon dependency is not required by taglite.
<SimpleTagInput
value={tags}
onChange={setTags}
tagIcon={<span aria-hidden="true">#</span>}
removeIcon={<span aria-hidden="true">×</span>}
/>You can also provide your own SVG:
<SimpleTagInput
value={tags}
onChange={setTags}
tagIcon={
<svg viewBox="0 0 24 24" aria-hidden="true">
{/* ... */}
</svg>
}
/>removeButtonProps
removeButtonProps?: {
className?: string
[key: string]: unknown
}Adds attributes to the remove buttons rendered inside tags. taglite keeps control of the button type, click handler, disabled state, and core behavior.
<SimpleTagInput
value={tags}
onChange={setTags}
removeButtonProps={{
title: 'Remove tag',
className: 'text-red-500',
}}
/>Read-only, disabled, and native input props
readOnly
readOnly is inherited from native input attributes. In read-only mode, new tags cannot be added, existing tags cannot be removed, Backspace and blur do not modify tags, and clear-all is disabled. The input can still be focused.
<SimpleTagInput value={tags} onChange={setTags} readOnly />disabled
In disabled mode, the input, tag actions, clear-all action, and keyboard tag actions are disabled. The component does not force focus onto the disabled input.
<SimpleTagInput value={tags} onChange={setTags} disabled />Native input attributes
SimpleTagInput extends InputHTMLAttributes<HTMLInputElement>, so standard attributes are supported unless they conflict with the controlled tag API.
<SimpleTagInput
value={tags}
onChange={setTags}
name="tags"
id="project-tags"
autoComplete="off"
autoFocus
required
/>The component intentionally controls value and onChange as its tag-list API.
Forwarded ref
The component forwards its ref directly to the underlying native <input> element. This is useful for forms, dialogs, keyboard shortcuts, and programmatic focus management.
import { useRef } from 'react'
import { SimpleTagInput } from 'taglite'
export default function Example() {
const inputRef = useRef<HTMLInputElement>(null)
return (
<>
<SimpleTagInput
ref={inputRef}
value={tags}
onChange={setTags}
/>
<button
type="button"
onClick={() => inputRef.current?.focus()}
>
Focus input
</button>
</>
)
}Common patterns
Technology tags
<SimpleTagInput
value={technologies}
onChange={setTechnologies}
placeholder="Add technology..."
normalizeTag={tag => tag.trim()}
maxTags={10}
/>Product keywords
<SimpleTagInput
value={keywords}
onChange={setKeywords}
placeholder="Add keyword..."
allowDuplicates={false}
/>RTL / Persian
<SimpleTagInput
direction="rtl"
theme="light"
value={tags}
onChange={setTags}
placeholder="برچسب جدید..."
hintText="برای افزودن برچسب Enter را بزنید"
/>Strict validation
<SimpleTagInput
value={tags}
onChange={setTags}
validateTag={tag =>
/^[a-z0-9-]+$/i.test(tag)
? true
: 'Only letters, numbers, and hyphens are allowed'
}
/>Accessibility
The component uses native HTML controls and provides accessible labels for tag removal buttons. The default remove button receives a label based on the tag name, such as:
Remove tag ReactWhen replacing icons with custom React nodes, keep decorative icons aria-hidden when the icon itself does not provide information. For form-level labels, descriptions, and validation messages, use your application's surrounding form or field structure rather than duplicating field abstractions inside SimpleTagInput.
TypeScript
The package exposes its public component and theme types:
import {
SimpleTagInput,
type SimpleTagInputProps,
type SimpleTagInputTheme,
} from 'taglite'const theme: SimpleTagInputTheme = 'dracula'
const props: SimpleTagInputProps = {
value: [],
onChange: tags => {
console.log(tags)
},
theme,
}Performance notes
taglite is designed to stay small and lightweight:
- no runtime dependencies
- inline SVG icons instead of an icon package
- controlled tag state managed by the parent
- memoized tag rendering
- cached theme styles
- simple array operations for normal add/remove flows
- no animation library
- no built-in network or asynchronous logic
For large tag collections, keep the value reference stable when the tags themselves have not changed.
Development
Clone the repository, install dependencies, and start the Vite development server:
npm install
npm run devAvailable scripts:
| Command | Purpose | | --- | --- | | npm run dev | Start the Vite dev server with HMR | | npm run build | Type-check and create the production app bundle | | npm run build:lib | Build the distributable library and declaration files | | npm run lint | Run ESLint across the repository | | npm run preview | Preview the production build locally |
There is currently no automated test runner or npm test script. When behavior grows beyond manual verification, add focused component tests covering tag creation with Enter/comma, duplicate handling, removal, keyboard behavior, focus states, and each supported theme.
License
MIT
