1 GitHub star = 1 new component.

Star the repo
Formsinputchips

Chip/Tag Input

Tag/chip input component with keyboard navigation.

Live Preview

Usage

Copy and paste this code into your React component:

tsx
import { ChipInput } from 'react-component-kit-ui/forms';

<ChipInput label="Product Tags" initialChips={["New", "Sale"]} placeholder="Add tag..." />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/ChipInput.tsx + ChipInput.css:

ChipInput.tsx
'use client';

import { useEffect, useRef } from 'react';
import './ChipInput.css';

/**
 * ChipInput โ€” An interactive input for managing a list of tags or chips.
 * 
 * @param {string} label - Input labeling title.
 * @param {string[]} initialChips - Optional array of strings to pre-populate.
 * @param {string} name - HTML name for form submission.
 * @param {string} placeholder - Placeholder text for the input area.
 */
export interface ChipInputProps {
  label?: string;
  initialChips?: string[];
  name?: string;
  placeholder?: string;
}

export function ChipInput({ label, initialChips = [], name, placeholder = "Add tag..." }: ChipInputProps) {

  // Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
  const mounted = useRef(false);
  useEffect(() => {
    if (mounted.current) return;
    mounted.current = true;
    // Simplified client-side logic for adding/removing chips
      document.querySelectorAll('[data-chip-input]').forEach(container => {
        const list = container.querySelector('[data-chip-list]');
        const input = container.querySelector('[data-chip-field]') as HTMLInputElement;
        const name = "tags"; // Should ideally be passed from props but hidden inputs solve this for now
    
        const addChip = (value: string) => {
          if (!value.trim() || !list) return;
          const chip = document.createElement('span');
          chip.className = 'chip';
          chip.innerHTML = `${value}<button type="button" aria-label="Remove ${value}" data-remove-chip>ร—</button>`;
          
          const hidden = document.createElement('input');
          hidden.type = 'hidden';
          hidden.name = 'chips[]';
          hidden.value = value;
          chip.appendChild(hidden);
          
          list.appendChild(chip);
          input.value = '';
          input.placeholder = '';
        };
    
        input?.addEventListener('keydown', (e) => {
          if (e.key === 'Enter') {
            e.preventDefault();
            addChip(input.value);
          }
          if (e.key === 'Backspace' && !input.value && list?.lastElementChild) {
            list.removeChild(list.lastElementChild);
            if (list.children.length === 0) input.placeholder = "Add tag...";
          }
        });
    
        container.addEventListener('click', (e) => {
          const target = e.target as HTMLElement;
          if (target.hasAttribute('data-remove-chip')) {
            target.closest('.chip')?.remove();
            if (list?.children.length === 0) input.placeholder = "Add tag...";
          }
        });
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <div className="chip-input-container" data-rk-36ed34="">
        {label && <label className="chip-input-label" data-rk-36ed34="">{label}</label>}
        <div className="chip-input" data-chip-input data-rk-36ed34="">
          <div className="chip-input__list" data-chip-list data-rk-36ed34="">
            {initialChips.map((chip, __i: number) => (
              <span key={__i} className="chip" data-rk-36ed34="">
                {chip}
                <button type="button" aria-label={`Remove ${chip}`} data-remove-chip data-rk-36ed34="">ร—</button>
                <input type="hidden" name={`${name}[]`} defaultValue={chip} data-rk-36ed34="" />
              </span>
            ))}
          </div>
          <input type="text" className="chip-input__field" placeholder={initialChips.length > 0 ? "" : placeholder} data-chip-field data-rk-36ed34="" />
        </div>
      </div>
    </>
  );
}

export default ChipInput;
ChipInput.css
/* ChipInput โ€” ported from astro-component-kit (forms/ChipInput.astro) */

.chip-input-container:where([data-rk-36ed34]) { display: flex; flex-direction: column; gap: var(--sp-2, 0.5rem); width: 100%; }
.chip-input-label:where([data-rk-36ed34]) { font-size: 0.85rem; font-weight: 600; color: var(--c-text-2, #94a3b8); margin-left: 0.5rem; }

.chip-input:where([data-rk-36ed34]) { 
  display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0.5rem; 
  background: var(--c-bg-elev, rgba(0,0,0,0.1)); 
  border: 1px solid var(--c-border, rgba(255,255,255,0.1)); 
  border-radius: var(--r-md, 10px); 
  transition: border-color 0.2s;
}
.chip-input:focus-within:where([data-rk-36ed34]) { border-color: var(--c-primary, #6366f1); }

.chip-input__list:where([data-rk-36ed34]) { display: flex; flex-wrap: wrap; gap: 0.5rem; }

.chip:where([data-rk-36ed34]) { 
  background: var(--c-primary, #6366f1); 
  color: #fff; 
  padding: 0.2rem 0.6rem; 
  border-radius: var(--r-sm, 6px); 
  font-size: 0.8rem; 
  display: flex; align-items: center; gap: 0.4rem; 
}
.chip:where([data-rk-36ed34]) button:where([data-rk-36ed34]) { 
  background: none; border: none; color: #fff; cursor: pointer; 
  font-size: 1.1rem; line-height: 1; padding: 0; 
  opacity: 0.8; transition: opacity 0.2s;
}
.chip:where([data-rk-36ed34]) button:hover:where([data-rk-36ed34]) { opacity: 1; }

.chip-input__field:where([data-rk-36ed34]) { 
  background: none; border: none; outline: none; 
  color: var(--c-text-1, #fff); flex: 1; min-width: 100px;
  padding: 0.2rem; font-family: inherit; font-size: 0.9rem;
}

Quick Info

Category
Forms
Filename
ChipInput.tsx
Dependencies
None โ€” pure React + CSS
Tags
inputchips