Detent web components
By Shane Logsdon

Overlays & menus

Spotlight Search

Implemented

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

  • open recorded state; no setter
  • closed recorded state; no setter
  • expanded recorded state; no setter
  • active recorded state; no setter
  • completed recorded 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-shortcut
  • open
  • placeholder

Properties

  • items
  • onClose
  • onNavigateDocs
  • onOpenChange
  • onSelect
  • onSelectComponent
  • recentSearches

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.

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.tsx sha256 90273f7b922e…

Native files

The native module and its artifacts:

  • src/components/spotlight-search/spotlight-search.ts
  • src/components/spotlight-search/styles.css
  • src/components/spotlight-search/define.ts
  • docs/components/spotlight-search.md
  • site/examples/spotlight-search.ts
  • tests/browser/spotlight-search.spec.ts
  • evidence/components/spotlight-search.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
enable-global-shortcut———Recorded in the catalog as an attribute.
openopenbooleanfalsePresence semantics: present means true, absent means false.
placeholderplaceholderstring'Search components, actions...'Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
itemsSpotlightSearchItem[][...]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) => voidundefinedProperty-only: assign from script; not accepted as an attribute.
onSelect(item: SpotlightSearchItem) => voidundefinedProperty-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

Events
EventUpstream propPayload typeNotes
detent-open-changeonOpenChange(open: boolean) => voidComposed, bubbles; fires only for a user action.
detent-selectonSelect(item: SpotlightSearchItem) => voidComposed, bubbles; fires only for a user action.
detent-closeonClose() => voidComposed, bubbles; fires only for a user action.
detent-select-componentonSelectComponent(id: string) => voidComposed, bubbles; fires only for a user action.
detent-navigate-docsonNavigateDocs(topicId?: string) => voidComposed, 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.
Previous: Spotlight Card Next: SpringSelect

Docs

Components

Project