Interest Selector — Free HTML CSS JS Snippet

Interest Selector · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Pill chip toggle: .selected class switches border to indigo + tint background
button elements: native keyboard focus, Enter/Space, and ARIA semantics for free
MIN = 3 configurable minimum: disable/enable Continue button in real time
Live count text: "0 selected" → "3 selected" updates on every chip click
"Select at least 3" hint: shows below count when minimum not met
Continue button disabled attribute: grey styling + not-allowed cursor via CSS
Emoji icons in chips: colored icons without any icon library
flex-wrap grid: chips reflow naturally at any container width

About this UI Snippet

Interest Selector — Pill Chip Multi-Select for Onboarding and Personalisation

Screenshot of the Interest Selector snippet rendered live

An interest selector is a multi-select chip grid — related to the chip filter and tag input — used in onboarding flows, newsletter preference centres, and content personalisation screens to learn what a user cares about. This snippet provides 16 topic chips in a flex-wrap grid, a minimum selection validation (3 required), a live count display, a disabled Continue button that enables on reaching the minimum, and a "Select at least 3" hint that hides once the minimum is met.

The pill chip toggle pattern

Each .chip is a button element styled as a rounded pill. toggle(chip) adds or removes the .selected class, which switches the border from grey (#e2e8f0) to indigo (#6366f1) and applies a light indigo background tint. Using button elements (not divs or inputs) gives keyboard focus, Enter/Space activation, and correct ARIA semantics for free.

Minimum selection validation

update() counts .chip.selected elements and compares against MIN = 3. If below minimum, the Continue button is disabled via disabled attribute (which CSS styles as grey with not-allowed cursor). The "Select at least 3" note appears below the count text. Once the minimum is met, both the button enables and the note hides. This pattern is cleaner than showing a validation error on submit — it gives real-time visual guidance.

The live count display

#countText shows "0 selected", "1 selected", "3 selected" etc. A zero state shows "None selected". This gives immediate feedback on each chip click, making the requirement clear and progress visible — particularly important on mobile where users may not see the minimum count hint.

Emoji icons in chip buttons

The emoji inside each chip uses a span with font-size: 16px. Emoji render as colored icons across all platforms without any icon library. The chip layout is flex with a 6px gap between the emoji and the label text, keeping them tightly coupled as a single visual unit.

Collecting the selected values

To read which topics are selected, query all .chip.selected elements and extract their data-id attributes: const selected = [...document.querySelectorAll(".chip.selected")].map(el => el.dataset.id). This gives an array like ["design", "react", "devtools"]. On Continue, POST this array to your API: fetch("/api/onboarding/interests", { method: "POST", body: JSON.stringify({ interests: selected }), headers: { "Content-Type": "application/json" } }). On the server, store the array in the user's profile and use it to seed the personalised content feed.

Search and filtering for large topic lists

When the topic list grows beyond 20–30 chips, add a search box above the grid. On each keystroke, filter visible chips by checking if the chip's label text includes the search query (case-insensitive): chips.forEach(chip => { chip.style.display = chip.querySelector(".chip-label").textContent.toLowerCase().includes(q) ? "" : "none"; }). This keeps the underlying selection state intact while narrowing the visible choices. For very large lists (100+ topics), consider grouping chips into collapsible category sections with a "Show more" toggle per group.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not have to trace the toggle and validation logic by hand to know exactly what it is doing. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why update() re-queries document.querySelectorAll(".chip.selected") on every single click instead of maintaining a running counter, and whether that matters at 16 chips versus 200. The same assistant is useful for optimizing it, for example suggesting a Set-based selection model that avoids a full DOM query on every toggle, or profiling whether a large topic grid should be virtualized. It is just as good for extending the pattern, such as adding a configurable MAX alongside the existing MIN, grouping chips into collapsible categories for a much longer topic list, or persisting selections to localStorage so a returning user's chips are pre-selected. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an "interest selector" onboarding step in plain HTML, CSS, and JavaScript using only DOM APIs and CSS class toggles — no state library, no framework.

Requirements:
- A grid of pill-shaped chip buttons (flex-wrap, not CSS grid) that reflows naturally at any container width, each button carrying a machine-readable identifier in a data attribute separate from its visible emoji-plus-label content.
- Clicking a chip toggles a "selected" class on that chip via classList.toggle, which restyles its border and background — no other chip's state is affected.
- A MIN constant (e.g. 3) defines the minimum number of chips that must be selected. After every toggle, recompute the count of selected chips, update a live "N selected" text element, enable or disable a Continue button's disabled attribute based on whether the count meets MIN, and show or hide a "select at least N" hint element to match.
- The Continue button must use the native disabled attribute (styled distinctly via CSS, e.g. grey background and not-allowed cursor) rather than just a visual-only disabled look, so it is genuinely non-interactive below the minimum.
- On Continue, collect the data attribute values of every currently selected chip into a plain array (not the display text) so the result is stable even if labels are renamed or translated later.
- Use real button elements for the chips so they are keyboard-focusable and activate on Enter and Space without extra JavaScript.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Click chips to select or deselect topicsClick any topic chip to toggle it selected (indigo border and tint). Click again to deselect. The count updates immediately and the Continue button enables when 3 or more are selected.
  2. 2
    Change the minimum requirementEdit var MIN = 3 in the JS to any number. The count display and validation update accordingly.
  3. 3
    Add or remove topicsDuplicate a .chip button in the HTML. Update the emoji in .chip-icon and the label in .chip-label. Set a unique data-id for the selected values array.
  4. 4
    Pre-select topics for returning usersOn page load, read the user's saved interests and add the .selected class to matching chips: savedInterests.forEach(id => document.querySelector('.chip[data-id="' + id + '"]').classList.add("selected")). Then call update().
  5. 5
    Handle the Continue actionReplace the alert() in onContinue() with your navigation logic: save the selected array to an API endpoint (POST /api/onboarding/interests) and advance to the next step.
  6. 6
    Export for your frameworkClick "JSX" for a React component with useState for selected set and useMemo for count. Click "Vue" for a Vue 3 SFC with reactive Set.

Real-world uses

Common Use Cases

Onboarding flow topic preference step
Use as step 2 or 3 in an onboarding wizard. On Continue, POST the selected interests array to your API (e.g., POST /api/users/preferences with { interests: ["dev", "react", "ux"] }). Use the array to personalise the home feed, recommended courses, or initial content filters.
Newsletter preference centre category selection
Let subscribers choose which content categories they want to receive. Each chip maps to an email list segment. On continue, update the subscriber's list memberships in Mailchimp, ConvertKit, or your ESP via their segment API.
Job board skill tag multi-selector for profiles
Use in a developer profile creation flow to select tech skills, tools, and domains. Each chip maps to a skill tag stored in the user profile. The selected array drives job recommendation matching and search filter defaults.
E-commerce product category preference onboarding
Let new shoppers pick categories they are interested in (Electronics, Fashion, Home, Sports). Use the selection to show a personalised homepage product grid. Persist in a cookie for anonymous users or in the user profile for logged-in users.
Study minimum-selection form validation without a library
The snippet demonstrates real-time form validation using a disabled attribute toggle — no validation library, form library, or error message system needed. The button's disabled state and the hint text provide dual feedback signals, reducing the need for intrusive error messages on submit.
Survey question multi-select with configurable minimum
Adapt for multi-answer survey questions: "Which features do you use most?" (min 2) or "Which tools do you use?" (min 1). Change the MIN variable and the minimum note text. The selected values feed into your survey results API.

Got questions?

Frequently Asked Questions

In toggle(chip), check the count before adding .selected: if (!chip.classList.contains("selected") && document.querySelectorAll(".chip.selected").length >= MAX) return; This prevents selection above the cap. Show a "Max N selected" note below the count when the limit is hit.

After loading the page, fetch the user's saved interests and apply: savedInterests.forEach(id => { const chip = document.querySelector('.chip[data-id="' + id + '"]'); if (chip) chip.classList.add("selected"); }); call update() after to refresh the count and button state.

Use const [selected, setSelected] = useState(new Set(["dev", "react", "ux"])). Toggle: setSelected(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }). Derive count: selected.size. Disable continue: selected.size < 3. Render each chip as a button with an onClick that calls the toggle function, and apply a conditional className that adds the active/selected styles when selected.has(chip.id) is true. Pass the selected set to the Continue handler as Array.from(selected) to convert it to a plain array for API serialisation. For animation, add a short CSS scale transform on the .selected class toggle so chips visually "pop" when selected — a 100ms scale(1.05) then back to scale(1) via a CSS transition creates a satisfying tactile feel that encourages engagement in onboarding flows.

A single MIN variable drives the whole gate. update() counts document.querySelectorAll(".chip.selected").length, writes the live "N selected" counter, disables #continueBtn while count < MIN, and shows the #minNote hint. Change MIN to 1 for an optional-feeling picker or 5 for a stronger personalisation signal, and update the subtitle copy ("Pick at least 3 topics…") to match — the enforcement and the promise should always agree.

Every chip carries its machine value in data-id, so onContinue() collects them with Array.from(document.querySelectorAll(".chip.selected")).map(c => c.dataset.id) — giving you a clean array like ["design", "dev", "ai"] regardless of the visible labels or emoji. Replace the demo alert with a fetch POST to your onboarding endpoint or stash the array in localStorage for the next step. Keeping display labels separate from data-id values means you can rename or translate chips without breaking stored preferences.