UI/UX law

// Perception & Gestalt;

Match Highlighting

In a filtered or autocomplete list, highlight the exact substring of each label that matched the query, styled with the product's own accent rather than the browser's default yellow, and give the active row its own separate highlight, whichever one arrow keys or the mouse currently point at, so Enter or a click always finishes the value on the row the user can see is active.

Mechanism

Why it happens

A filtered list still makes someone read every row to work out why it's there. Recognition beats recall, but only once the reason for the match is visible without rereading the label; a short highlighted span inside "Marrakesh" says instantly that it matched on "mar," wherever that substring sits in the word, in a way a plain label or a merely reordered list can't.

Colour is also a grouping signal: two different kinds of information styled the same way read as one thing (Law of Similarity). A match highlight answers "why did this row match" and an active-row highlight answers "where am I right now" — two different questions — so giving them identical styling collapses two answers into one. Reaching for the browser's default `<mark>` yellow compounds it, since that colour was never chosen to sit inside the product's own palette and reads as a stray system alert rather than a deliberate part of the design.

"Where am I" only has one honest answer at a time, so arrow keys and mouse hover have to move the same active-row pointer rather than each keeping a state of their own; two independent "active" states (one for keyboard, one for mouse) is exactly the kind of self-inconsistency Jakob's Law punishes, since it breaks the one mental model people bring from every other list they've used. Enter and a click are the same commitment from two input methods, so both need to finish the interaction the same way: write the visibly active option into the field and close the list.

Impact

Why it matters

  • Without any highlight, a filtered list looks arbitrary; people have to reread every row instead of scanning for the part that matched
  • The browser/OS default `<mark>` yellow was never tested against the product's palette, and it visually announces itself as a bug report or a find-in-page artefact, not a feature
  • Styling the active row the same way as the match highlight erases the one signal a person has for where they currently are
  • If keyboard focus and mouse hover drive two separate "active" states, moving the mouse after arrowing down (or vice versa) leaves two rows looking selected, or none, and Enter can commit a different row than the one last highlighted
  • Highlighting only a prefix misses genuine matches later in the word, which quietly breaks the same promise the highlight is there to keep

Example

Without vs. with

Without
mar
Marseille · France
Marbella · Spain
Marrakesh · Morocco

Typing "mar" into a city search returns Marseille, Marbella, and Marrakesh as plain text; every row looks arbitrary, and the keyboard-selected row is invisible.

With
mar
Marseille· France
Marbella· Spain
Marrakesh· Morocco

The same three rows wrap only the matched "Mar" in the product's own accent colour, and the keyboard-selected row gets its own full-row highlight instead of reusing that colour.

Checklist

How to apply it

Find the match index yourself (case-insensitive `indexOf`, or a fuzzy matcher) and wrap only the matched substring in a styled span, never the whole label

Style the match with the product's own accent, a tinted background and/or weight change, never the browser's default `mark` yellow

Give the active row its own full-row treatment, a neutral surface tint rather than a coloured one, so it reads as a separate channel from the inline match highlight instead of a second, louder copy of it

Move the active row with ArrowUp/ArrowDown, and let mouse hover move that exact same active-row state, never a second, independent one

Commit the active row on Enter or on click alike; both need to write the chosen value into the field and close the list, not just highlight a row

Recompute highlights on every keystroke without re-rendering the whole list; only the changed spans should repaint

Match substrings anywhere in the label, not only at the start, and stay legible when a query produces multiple non-contiguous matches

Recipe

Code example

Where it shows up

Element areas

FormsNavigationStates