ui-015

Autocomplete Search Dropdown

£10 or the whole Vault, £99

An autocomplete search dropdown: grouped results filter as you type with matches highlighted, a highlight slides under the arrow keys.

intermediate
5 more details
micro-interactionfuzzy-searchkeyboard-navigationstaggerhover-effect

This element reads better at your own screen size than in the frame below:

Updated

About this component

A search field with a real results dropdown: a static, grouped list that actually filters as you type, with the matched run of characters marked in each label and any heading left with nothing under it dropping out with its group. The surviving rows stagger back in on every keystroke, one highlight slides between them under the arrow keys, Enter fills the field, and a dead end gets an animated empty state rather than a blank panel.

Read the full effect overview

A single self-contained component: one HTML file, one stylesheet, one script, vanilla JavaScript and core GSAP, no framework and no plugins. Ships in light and dark themes, switched with one data-variant attribute.

Autocomplete Search Dropdown - UI component preview

What's included

8 items
  • Substring filter over a static list, with the matched characters wrapped in mark
  • Results grouped under headings, and a group hides itself when it empties
  • Surviving rows stagger back in on every keystroke
  • One highlight slides between rows for arrow keys, hover and click alike
  • Enter fills the field and flashes it in the accent
  • Animated empty state that names the term that found nothing
  • combobox / listbox / option roles with aria-activedescendant
  • Light and dark themes on one token list

Perfect for

4 use cases
  • People, team and role pickers in an admin or settings screen
  • Site and docs search with results split by section
  • Assignee, customer, product and ticket fields
  • Any input where a plain datalist looks unfinished

How it works

3 sections

Filtering and marking

Each keystroke walks the rows, tests the query against the row's data-label and toggles the hidden attribute, which the stylesheet restates as display:none because the row rule sets display:flex. The label is then re-rendered from data-label with the matched run wrapped in a mark element, so repeated filtering never compounds earlier markup, and each group's heading hides itself when nothing under it survived.

The sliding highlight

One absolutely positioned bar keeps left:0 and top:0 in CSS and takes its whole box from the active row's offsetLeft, offsetTop, offsetWidth and offsetHeight, so no CSS inset can double up with the measured offset. Rows are positioned only to paint over the bar, which leaves the scroll box as the single offset parent every measurement is taken against.

Keyboard model

Focus never leaves the input. Arrow keys move an index over the visible rows only, wrapping at both ends, and the input carries aria-activedescendant so assistive technology follows the same row the highlight is under. Enter copies the active row's label into the field and re-filters, and the field flashes in the accent the current theme defines.

Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this UI element's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid UI element

Purchase to unlock the code.

Buying Autocomplete Search Dropdown opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£10 Standard license, unlimited projects

Browse UI elements

Or the whole library: the Vault, £99, one payment.

Documentation

What's Included

  • index.html: the demo page and the markup to copy

  • assets/style.css: component styles, plain CSS custom properties for colours

  • assets/script.js: readable, commented source with an onReady guard

  • Substring filter over a static list, matched characters wrapped in <mark>

  • Results grouped under headings; a group hides itself when it empties

  • Surviving rows stagger back in on every keystroke

  • One highlight slides between rows for arrow keys, hover and click alike

  • Enter fills the field and flashes it in the accent

  • Animated empty state that names the term that found nothing

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

2. Add to your <body>:

Copy the whole .ac block from index.html. Each row is a plain list item carrying its own text:

Code snippet omitted: it ships with the download.

data-label is the source of truth for both the filter and the re-rendered label, so keep it in step with the visible .ac-label text. .ac-avatar and .ac-meta are optional: a row can be a bare .ac-label if that is all you have, and .ac-avatar--square swaps the circle for a rounded square (people vs teams and roles in the demo). Add or remove <section class="ac-group"> blocks freely; the script reads them on load. Each one needs role="group" and an aria-labelledby pointing at its own heading id (see Accessibility).

3. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

Themes

The component ships in two themes, light and dark: one design at two sets of token values. Set the attribute on <body> in your page (or on any ancestor of .ac), because the surface colours are part of the theme:

Code snippet omitted: it ships with the download.

Nothing else changes: same markup, same script. The demo's toolbar toggle and the ?variant=dark URL parameter only set that attribute.

Both themes define the same short token list at the top of assets/style.css, so re-branding is re-valuing it once per theme:

Token What it colours
--ground, --ground-2 the page behind the component, and the results footer
--raised the field, the panel, the key caps
--hover control hover
--ink, --ink-2, --ink-3 row titles, the role line, group headings and meta
--line, --line-strong hairlines, and the field border
--accent the sliding highlight wash, the focus ring, the match underline, the Enter flash
--accent-ink, --accent-wash text on the accent, and the accent as a solid selected-row ground
--shadow, --radius, --radius-sm, --radius-xs elevation and the corner scale
--field-bg, --field-border, --panel-bg, --avatar-bg the component's own aliases onto the list above

The Enter flash reads --accent and --field-border with getComputedStyle at tween time, so a re-valued theme flashes its own colour with no script change. Nothing in assets/script.js reads the theme name.

Demo furniture. The 44px toolbar strip at the top of index.html, the .stage* rules in assets/style.css and the sr-only page heading are the demo's stage, not part of the component. Delete them and drop .ac straight into your own page.

How It Works

Filtering and marking. Each keystroke walks the rows, tests the query against data-label and toggles the hidden attribute, which the stylesheet restates as display: none because the row rule sets display: flex. The label is then re-rendered from data-label with the matched run wrapped in a <mark>, so repeated filtering never compounds earlier markup, and each group's heading hides itself when nothing under it survived.

The sliding highlight. One absolutely positioned bar keeps left: 0 and top: 0 in CSS and takes its whole box from the active row's offsetLeft, offsetTop, offsetWidth and offsetHeight. Rows are positioned only so they paint over the bar, which leaves the scroll box as the single offset parent every measurement is taken against.

Keyboard model. Focus never leaves the input. Arrow keys move an index over the visible rows only, wrapping at both ends, and the input carries aria-activedescendant so assistive technology follows the row the highlight is under. Enter copies the active row's label into the field and re-filters.

Using It With Your Own Data

The list is static markup on purpose: it works with no build step, no fetch and no framework, and it is what a server-rendered page already has. To drive it from data, render the same <li class="ac-item" data-label="…"> rows before the script runs; nothing in assets/script.js needs to change. For a remote source, replace the body of filter() with your request and rebuild the rows, then call setActive(0).

Customisation

What Where
Row stagger stagger: dur(0.022) in filter()
Highlight travel speed duration: dur(0.28) and power3.out in moveHighlight
Empty-state swing the elastic.out(1, 0.55) tween in showEmpty
Match rule label.indexOf(query) in filter() and renderLabel
Panel height max-height on .ac-results (default 344px)
Colours, radii, shadows the body[data-variant] blocks in assets/style.css

Accessibility

  • The field is a role="combobox" with aria-controls, aria-expanded and aria-autocomplete="list".
  • The panel is role="listbox", each <section class="ac-group"> inside it is role="group" labelled by its heading with aria-labelledby, and the rows are role="option". The <ul class="ac-list"> between them is role="presentation", so the owned structure is the valid listbox > group > option and neither the list nor the group semantics fight each other. Keep those three attributes if you render the rows yourself, and give each group heading a unique id.
  • The active row is published through aria-activedescendant and aria-selected, so focus can stay in the input where typing belongs.
  • Arrow Down / Arrow Up move the active row and wrap at both ends, Enter fills the field, Escape closes the panel; focus stays on the input throughout.
  • :focus-visible rings are kept on the field, the clear button and the demo's theme toggle.
  • The clear button empties the field and restores every row.
  • Without JavaScript the panel and the whole list are visible and readable; only the filtering is lost.
  • prefers-reduced-motion: reduce sets every duration to zero, so rows and the highlight move instantly rather than not at all.

Requirements

  • GSAP 3.12+ (core only)
  • No build step, no framework

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.