Build a tag input
mediumUI Components+190 XP
Build an input that turns whatever you type into a removable tag when you press Enter.
Pressing Backspace while the input is empty removes the most recently added tag, rather than doing nothing.
Examples
// Type "react", press Enter → tags: ["react"], input cleared.
// Type "typescript", Enter → tags: ["react","typescript"].
// Backspace on empty input → tags: ["react"].
// Click × on "react" → tags: [].Backspace-on-empty is the keyboard shortcut everyone expects but no one implements.
function handleKeyDown(e) {
if (e.key === "Enter") {
const trimmed = value.trim();
if (trimmed) {
setTags((current) => [...current, trimmed]);
setValue("");
}
} else if (e.key === "Backspace" && value === "" && tags.length) {
setTags((current) => current.slice(0, -1));
}
}Trim before pushing; the empty check prevents stray whitespace tags.
What it has to do
- A `<input id="tag-input">` for typing new tags.
- Each tag renders as `<span data-testid="tag">text</span>` inside a container `data-testid="tag-list"`, in the order added.
- Pressing Enter with non-empty input text adds it as a new tag and clears the input.
- Pressing Enter with only whitespace, or an empty input, adds nothing.
- Each tag has a `<button data-testid="remove-tag">` that removes just that tag.
- Pressing Backspace while the input is empty removes the last tag in the list.
Notes
- Preventing default on Enter inside a `<form>` matters — otherwise the form submits. Not required by the test, but every real tag input lives inside one.
- Consider deduplicating case-insensitively on insert — the spec doesn't test it, but email/skills fields almost always want it.
- Backspace on empty must only pop; don't also prevent default or the browser's own text-navigation behaviour breaks when the input isn't empty.
- For accessibility, each chip should expose the remove button with an `aria-label` like "Remove react" — screen readers hearing just "×" don't know what's being removed.
Stuck? The react course covers the ideas this problem is built on.
import { useState } from "react"; export default function App() { const [tags, setTags] = useState([]); const [value, setValue] = useState(""); function handleKeyDown(e) { // TODO: Enter with non-empty (non-whitespace-only) text adds a tag // and clears the input. Backspace with an empty input removes the // last tag. } function removeTag(index) { setTags((current) => current.filter((_, i) => i !== index)); } return ( <div style={{ fontFamily: "sans-serif", padding: 16 }}> <div data-testid="tag-list" style={{ display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 8 }} > {tags.map((tag, index) => ( <span key={index} data-testid="tag" style={{ background: "#eee", padding: "4px 8px", borderRadius: 12 }} > {tag} <button data-testid="remove-tag" onClick={() => removeTag(index)} style={{ marginLeft: 6 }} > × </button> </span> ))} </div> <input id="tag-input" value={value} onChange={(e) => setValue(e.target.value)} onKeyDown={handleKeyDown} placeholder="Type and press Enter..." style={{ padding: 8, width: "100%", boxSizing: "border-box" }} /> </div> ); }
- 1.A `<input id="tag-input">` for typing new tags.
- 2.Each tag renders as `<span data-testid="tag">text</span>` inside a container `data-testid="tag-list"`, in the order added.
- 3.Pressing Enter with non-empty input text adds it as a new tag and clears the input.
- 4.Pressing Enter with only whitespace, or an empty input, adds nothing.
- 5.Each tag has a `<button data-testid="remove-tag">` that removes just that tag.
- 6.Pressing Backspace while the input is empty removes the last tag in the list.
5 automated assertions run against your rendered component. The exact assertion names are hidden so you build to the behaviour, not to the test.
5 tests · +190 XP
Discussion
Sign in to postSign in to join the conversation.Sign in
No comments yet. Be the first to say something.