All Taste components
Inputs
Tag input
Type and press Enter to add tags; each chip can be removed, and invalid ones are flagged.
tags · focused · invalid chip · count
Press Enter or comma to add.3 / 10
meera@kaveri isn’t a full email address.
"use client";
import { useId, useRef, useState } from "react";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
export function TagInput({ label, value, onChange, max = 10, placeholder = "Add a tag…", isValid = () => true }:
{ label: string; value: string[]; onChange: (tags: string[]) => void; max?: number; placeholder?: string; isValid?: (tag: string) => boolean }) {
const id = useId();
const [draft, setDraft] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
function commit() {
const tag = draft.trim().replace(/,+$/, "");
if (tag && !value.includes(tag) && value.length < max) onChange([...value, tag]);
setDraft("");
}
function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter" || e.key === ",") { e.preventDefault(); commit(); }
else if (e.key === "Backspace" && !draft && value.length) onChange(value.slice(0, -1));
}
return (
<div className="grid gap-1.5">
<label htmlFor={id} className="text-[13px] font-medium text-fg">{label}</label>
<div
onClick={() => inputRef.current?.focus()}
className="flex min-h-9 w-full cursor-text flex-wrap items-center gap-1.5 rounded border border-border-strong bg-bg px-1.5 py-[5px] transition focus-within:border-primary focus-within:ring-[3px] focus-within:ring-primary/20"
>
{value.map((tag) => {
const ok = isValid(tag);
return (
<span key={tag} className={cn("inline-flex h-6 max-w-full items-center gap-0.5 rounded-sm pl-2 pr-0.5 text-[13px]", ok ? "bg-surface-2 text-fg" : "bg-danger-soft text-danger-fg")}>
<span className="truncate">{tag}</span>
<button
type="button"
aria-label={"Remove " + tag + (ok ? "" : ", not valid")}
onClick={(e) => { e.stopPropagation(); onChange(value.filter((t) => t !== tag)); }}
className={cn("grid size-[18px] place-items-center rounded-sm transition hover:bg-fg/10", ok ? "text-fg-subtle hover:text-fg" : "text-current")}
>
<X className="size-3" aria-hidden />
</button>
</span>
);
})}
<input
ref={inputRef}
id={id}
value={draft}
placeholder={value.length < max ? placeholder : ""}
disabled={value.length >= max}
aria-describedby={id + "-help"}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={onKeyDown}
onBlur={commit}
className="h-6 min-w-24 flex-1 bg-transparent px-1 text-sm text-fg outline-none placeholder:text-fg-subtle"
/>
</div>
<div className="flex items-baseline justify-between gap-3 text-xs text-fg-subtle">
<p id={id + "-help"}>Press Enter or comma to add.</p>
<p className="tabular-nums">{value.length} / {max}</p>
</div>
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.