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
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.
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