Overlays & menus
Spotlight Search
Command overlay with a moving highlight
detent-spotlight-search
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 3
- Wave
- phase-3-overlays
- Group
- Overlays & menus
8.5 KB gzip for the class module · inlines the shared spring
Variants
⌘K / Ctrl+K toggles the overlay (enable-global-shortcut is opt-in). Arrow keys wrap, Enter executes the
active row, Escape closes and returns focus to the trigger.
Nothing selected yet.
Recorded states
openrecorded state; no setterclosedrecorded state; no setterexpandedrecorded state; no setteractiverecorded state; no settercompletedrecorded state; no setter
Usage
Import
import '/components/spotlight-search/define.js';
// or register it yourself
import { DetentSpotlightSearch } from '/components/spotlight-search/spotlight-search.js';
Attributes
enable-global-shortcutopenplaceholder
Properties
itemsonCloseonNavigateDocsonOpenChangeonSelectonSelectComponentrecentSearches
Slots
No slots are recorded for this component.
Events
detent-open-change— {}detent-select— {}detent-close— {}detent-select-component— {}detent-navigate-docs— {}
Example
Runnable example: site/examples/spotlight-search.ts. The snippet below is generated from the catalogued attributes.
<detent-spotlight-search
enable-global-shortcut
open
placeholder="Search components, actions..."
></detent-spotlight-search>
Property-only values
const element = document.querySelector('detent-spotlight-search');
if (element !== null) {
// onOpenChange: (open: boolean) => void
element.onOpenChange = () => {};
// items: SpotlightSearchItem[]
element.items = [];
// onSelect: (item: SpotlightSearchItem) => void
element.onSelect = () => {};
}
Source
The pinned upstream source for this row:
src/components/ui/SpotlightSearch.tsxsha256 90273f7b922e…
Native files
The native module and its artifacts:
src/components/spotlight-search/spotlight-search.tssrc/components/spotlight-search/styles.csssrc/components/spotlight-search/define.tsdocs/components/spotlight-search.mdsite/examples/spotlight-search.tstests/browser/spotlight-search.spec.tsevidence/components/spotlight-search.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
enable-global-shortcut | — | — | — | Recorded in the catalog as an attribute. |
open | open | boolean | false | Presence semantics: present means true, absent means false. |
placeholder | placeholder | string | 'Search components, actions...' | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
items | SpotlightSearchItem[] | [...] | Property-only: assign from script; not accepted as an attribute. |
onClose | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
onNavigateDocs | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
onOpenChange | (open: boolean) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
onSelect | (item: SpotlightSearchItem) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
onSelectComponent | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
recentSearches | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-open-change | onOpenChange | (open: boolean) => void | Composed, bubbles; fires only for a user action. |
detent-select | onSelect | (item: SpotlightSearchItem) => void | Composed, bubbles; fires only for a user action. |
detent-close | onClose | () => void | Composed, bubbles; fires only for a user action. |
detent-select-component | onSelectComponent | (id: string) => void | Composed, bubbles; fires only for a user action. |
detent-navigate-docs | onNavigateDocs | (topicId?: string) => void | Composed, bubbles; fires only for a user action. |
Slots
No slots are recorded for this component.
CSS customisation
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- The placeholder default is the source literal. 'Search components, guides, documentation...' (source) rather than 'Search components, actions...' (meta file), following PC-3.6.
- items defaults to the four documentation rows. Upstream also injected the whole generated catalog index and a window.open GitHub action; the catalog index is site data and a window.open side effect is not a component behaviour, so the native default is the four documentation rows and consumers supply the rest.
- One close sequence per selection. Upstream called onOpenChange(false)/onClose() twice for a component row (once inside the item's action, once after it); the native port emits exactly one detent-open-change plus one detent-close (PC-6.6).
- Top layer instead of a fixed overlay. <dialog>.showModal() replaces fixed inset-0 divs, so no ancestor stacking or overflow can clip it and the page behind is inert (PC-9.8, PC-12.4).
- The ESC hint is a real button. Upstream used a clickable <kbd>; a non-interactive element is not a control, so the native port uses a <button> whose label contains its visible text (PC-12.1, WCAG 2.5.3).
- Categorised badges and kbd badges are rendered. The recorded features require categorised results with kbd badges; upstream's final markup rendered the title only. icon (a React node) is dropped (PC-6.3).
- aria-activedescendant, no roving focus. Non-focusable options with one combobox input, which is the pattern the recorded accessibility entry describes.
- The highlight is measured, not layoutId. A single absolutely positioned element driven by the shared spring helper replaces Framer Motion's shared-layout projection; the move writes transform and height on one element per frame.
- recentSearches is stored but not rendered, exactly as upstream (declared in its props and never read).
- enable-global-shortcut defaults to false, as the source declares; the recorded ⌘K / Ctrl+K feature is therefore opt-in.
- className is replaced by documented parts and custom properties.
What it does
It opens as a real modal dialog in the browser's top layer with a dimmed, blurred backdrop.
Upstream behaviour
- Global keyboard listener for ⌘K / Ctrl+K and Escape dismissal
- Animated active highlight tracking item selection with layoutId spring physics
- Categorized search results with contextual icons and technical kbd badges
- Dimmed backdrop with subtle blur preserving focus on command box
- Full arrow key navigation and Enter selection execution
Differences from upstream
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- The placeholder default is the source literal. 'Search components, guides, documentation...' (source) rather than 'Search components, actions...' (meta file), following PC-3.6.
- items defaults to the four documentation rows. Upstream also injected the whole generated catalog index and a window.open GitHub action; the catalog index is site data and a window.open side effect is not a component behaviour, so the native default is the four documentation rows and consumers supply the rest.
- One close sequence per selection. Upstream called onOpenChange(false)/onClose() twice for a component row (once inside the item's action, once after it); the native port emits exactly one detent-open-change plus one detent-close (PC-6.6).
- Top layer instead of a fixed overlay. <dialog>.showModal() replaces fixed inset-0 divs, so no ancestor stacking or overflow can clip it and the page behind is inert (PC-9.8, PC-12.4).
- The ESC hint is a real button. Upstream used a clickable <kbd>; a non-interactive element is not a control, so the native port uses a <button> whose label contains its visible text (PC-12.1, WCAG 2.5.3).
- Categorised badges and kbd badges are rendered. The recorded features require categorised results with kbd badges; upstream's final markup rendered the title only. icon (a React node) is dropped (PC-6.3).
- aria-activedescendant, no roving focus. Non-focusable options with one combobox input, which is the pattern the recorded accessibility entry describes.
- The highlight is measured, not layoutId. A single absolutely positioned element driven by the shared spring helper replaces Framer Motion's shared-layout projection; the move writes transform and height on one element per frame.
- recentSearches is stored but not rendered, exactly as upstream (declared in its props and never read).
- enable-global-shortcut defaults to false, as the source declares; the recorded ⌘K / Ctrl+K feature is therefore opt-in.
- className is replaced by documented parts and custom properties.