Re: design review for the search experience
Loved the new typeahead — the highlight on each result row reads much faster than the underlined variant. One nit: the empty-state copy still says "no matches" instead of "no results".
Pure transformer that splits text into matched and unmatched chunks. Returns a plain HighlightChunk[] — wrap the call in computed() for reactive recomputation.
import { computed } from 'vue'
import { toHighlight } from '@vuetify/v0'
const chunks = computed(() =>
toHighlight(() => props.text, () => props.query, { ignoreCase: true })
)
// chunks.value → [{ text: 'Hello ', match: false }, { text: 'World', match: true }]toHighlight resolves its input through a fixed priority order:
toHighlight is a pure transformer — it reads each input through toValue once and returns a plain HighlightChunk[]. To make the result track upstream changes, wrap the call in computed() (or any reactive scope). The function itself creates no reactivity.
| Behavior | Reactive | Notes |
|---|---|---|
Calling toHighlight(text, query) | One-shot snapshot at call time | |
Wrapping in computed(() => toHighlight(...)) | Re-runs when tracked refs change | |
| Passing refs or getters as arguments | toValue unwraps them on every call | |
| Mutating returned chunks | Treat the array as derived; do not mutate |
Reach for plain values, refs, or getters Every input accepts MaybeRefOrGetter<T>. Pass a literal for static input, a Ref for v-model integration, or a getter (() => props.text) for prop-driven reactivity. Wrap the call in computed() when you want the result to update automatically.
A mail-style search pane that exercises every input shape toHighlight accepts. The search Input.Root drives a single-term query, the saved-filter Toggle.Root chips extend that into a multi-term array, and the Server snippets Switch.Root swaps the body-text path from client-side substring matching to caller-supplied MatchRange[] returned by a mock backend. Every option is a real-world button you’d find on a working inbox, not a “demo mode” toggle.
The MessageRow sub-component owns its toHighlight calls so the parent list stays dumb — pass it the resolved terms array and the serverMode flag and it decides which shape to feed the transformer. Pulling the per-row work into a child also means the same row component drops into any list with a query in scope.
Single query — type into the search box with no chips active. terms is a one-element string[]. toHighlight accepts that just as happily as a bare string, so this and “Multiple” share a code path. ignoreCase: true, matchAll: true are the typical defaults for search UIs; drop either flag when stricter behavior is warranted (legal text, identifier lookups).
Multiple queries — toggle on one or more saved filters. Each filter chip appends its label to terms, the Input value joins them, and toHighlight searches each term independently. Overlapping or adjacent spans collapse automatically — ['budget', 'budgets'] against “budget vs budgets line item” yields two clean highlights, not four overlapping ones. Derive the array from chips, a tokenizer, an API suggestion list, or a comma-split text input — anything that produces string[].
Pre-computed ranges — flip Server snippets on. The body text now ignores query entirely and renders from [start, end] pairs supplied by snippets() in messages.ts — a stand-in for a real search backend (Algolia, Elasticsearch, your own indexer) that returns character offsets alongside matched documents. The whole-word matcher used by snippets() highlights different spans than client-side substring matching, so the visual difference between the two modes is real, not cosmetic. The matched chunks render with an underline so the source of truth is obvious at a glance.
MatchRange is exported as readonly [number, number] where end is exclusive — the same convention as String.prototype.slice. Caller-supplied ranges are sorted and merged before chunking, so unordered or overlapping input is normalized for you.
| File | Role |
|---|---|
messages.ts | Records plus a snippets() function that fakes a backend’s offset response |
MessageRow.vue | Renders one row; owns both toHighlight calls (subject + body) |
inbox.vue | Entry: Input.Root search, Toggle.Root filters, Switch.Root mode |
| Priority | Source | Condition |
|---|---|---|
| 1 | matches | non-empty array |
| 2 | query | string or string[] |
| 3 | No-match fallback | neither provided |
Re: design review for the search experience
Loved the new typeahead — the highlight on each result row reads much faster than the underlined variant. One nit: the empty-state copy still says "no matches" instead of "no results".
Weekly product sync notes
Recap: launched the new search bar, started the onboarding redesign, paused the budget dashboard work pending review. Open questions on the search ranking tweak are tracked in the planning doc.
New review request on vuetifyjs/0#222
J-Sek requested your review on the toHighlight transformer PR. The branch adds a pure splitter and updates the create-filter live-search example to drop the v-html highlight path.
Wrap matched chunks in the native <mark> element. It carries the implicit ARIA role mark and is announced by screen readers as highlighted or marked text. No additional ARIA attributes are needed on the wrapper element.
WCAG 1.4.3 (Contrast — Minimum) applies to highlighted text. Ensure sufficient contrast between the mark background color and the surrounding text.
Yes. The source text string is sliced at match boundaries, so the original characters (including casing, punctuation, and whitespace) are always preserved in the output chunks. ignoreCase affects only the matching logic, not the returned text.
Yes. The matches option accepts MatchRange[] — [start, end] pairs. Once createFilter exposes positional data, pass the result directly and skip the query path.
Overlapping or adjacent spans are merged before the chunks array is produced. ['foo', 'oba'] against 'foobar' yields [{ text: 'fooba', match: true }, { text: 'r', match: false }] rather than two separate matches.
Yes. Ranges passed via the matches option are sorted by start index and merged on overlap or adjacency before chunking. Pass [[4, 6], [0, 2]] or [[0, 4], [2, 6]] and the output is the same as if you had supplied the canonical sorted, non-overlapping form.
The function returns a single [{ text: sourceText, match: false }] chunk — the full string with no highlights. Safe to iterate without any guard.
Yes. toHighlight is a pure function with no DOM access and no reactive state. It is safe to call during SSR.