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

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.

npm version React TypeScript npm downloads Bundle Size

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 taglite

Or use another package manager:

yarn add taglite
pnpm add taglite

taglite 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.js

Paste 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 -> onChange

Combine 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[]) => void

value 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?: string

Sets 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?: ReactNode

The 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?: boolean

maxTags 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) => string

Transforms 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) => void

Validation 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) => void

onTagAdd 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?: boolean

When 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?: ReactNode

clearable 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?: ReactNode

Both 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 React

When 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 dev

Available 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