Live Component
Simulate Scenarios
Click a scenario, then type in the search box to trigger it.
Property Playground
Tweak any attribute in real time — the component above updates immediately.
Placeholder
placeholder
No-results text
no-results-text
Hint text
hint
Min chars
min-chars
Debounce
debounce-ms
Page size
page-size
Highlight matches
highlight-matches
on
Disabled
disabled
off
Keyboard Navigation
↓ ↑
Navigate results / move into listbox from input
Enter
Select highlighted result
Esc
Close dropdown, return focus to input
Home End
Jump to first / last result
Tab
Close dropdown, continue tab order
← →
Navigate filter chips
Event Log
— events will appear here as you interact —
Usage (Host Integration Pattern)
<!-- 1. Load from npm build or CDN -->
<script type="module" src="./banking-search.js"></script>
<!-- 2. Declare in your HTML -->
<banking-search
id="search"
placeholder="Search accounts, customers…"
min-chars="2"
debounce-ms="300"
highlight-matches
></banking-search>
<!-- 3. Wire it up -->
<script type="module">
const el = document.getElementById('search');
let controller;
el.filters = [
{ id: 'all', label: 'All' },
{ id: 'accounts', label: 'Accounts' },
{ id: 'transactions', label: 'Transactions' },
{ id: 'customers', label: 'Customers' },
];
el.addEventListener('bs:search', async ({ detail }) => {
controller?.abort();
controller = new AbortController();
el.setAttribute('loading', '');
el.removeAttribute('error');
try {
const data = await fetchResults(detail.term, detail.filters, controller.signal);
el.results = data;
} catch (err) {
if (err.name !== 'AbortError') el.setAttribute('error', 'network');
} finally {
el.removeAttribute('loading');
}
});
el.addEventListener('bs:select', ({ detail }) => router.navigate(detail.item.url));
el.addEventListener('bs:retry', ({ detail }) => el.dispatchEvent(
new CustomEvent('bs:search', { detail, bubbles: true, composed: true })
));
</script>
import 'banking-search';
import { useEffect, useRef } from 'react';
export function SearchBar({ onSelect }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
let controller;
el.filters = [
{ id: 'all', label: 'All' },
{ id: 'accounts', label: 'Accounts' },
{ id: 'transactions', label: 'Transactions' },
];
const onSearch = async ({ detail }) => {
controller?.abort();
controller = new AbortController();
el.setAttribute('loading', '');
try {
el.results = await fetchResults(detail.term, detail.filters, controller.signal);
} catch (err) {
if (err.name !== 'AbortError') el.setAttribute('error', 'network');
} finally {
el.removeAttribute('loading');
}
};
el.addEventListener('bs:search', onSearch);
el.addEventListener('bs:select', onSelect);
return () => {
el.removeEventListener('bs:search', onSearch);
el.removeEventListener('bs:select', onSelect);
};
}, [onSelect]);
// JSX — attribute names stay kebab-case for custom elements
return (
<banking-search
ref={ref}
placeholder="Search accounts, customers…"
min-chars="2"
highlight-matches
/>
);
}
<!-- SearchBar.vue -->
<template>
<banking-search
ref="searchEl"
placeholder="Search accounts, customers…"
min-chars="2"
highlight-matches
@bs:search="onSearch"
@bs:select="onSelect"
/>
</template>
<script setup>
import 'banking-search';
import { ref, onMounted } from 'vue';
const searchEl = ref(null);
let controller;
onMounted(() => {
searchEl.value.filters = [
{ id: 'all', label: 'All' },
{ id: 'accounts', label: 'Accounts' },
{ id: 'transactions', label: 'Transactions' },
];
});
async function onSearch({ detail }) {
controller?.abort();
controller = new AbortController();
const el = searchEl.value;
el.setAttribute('loading', '');
try {
el.results = await fetchResults(detail.term, detail.filters, controller.signal);
} catch (err) {
if (err.name !== 'AbortError') el.setAttribute('error', 'network');
} finally {
el.removeAttribute('loading');
}
}
function onSelect({ detail }) {
router.push(detail.item.url);
}
</script>
// app.module.ts — allow unknown custom elements
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })
export class AppModule {}
// search-bar.component.ts
import 'banking-search';
import { Component, ViewChild, ElementRef, OnInit } from '@angular/core';
@Component({
selector: 'app-search-bar',
template: `
<banking-search
#searchEl
placeholder="Search accounts, customers…"
min-chars="2"
highlight-matches
(bs:search)="onSearch($event)"
(bs:select)="onSelect($event)"
></banking-search>
`
})
export class SearchBarComponent implements OnInit {
@ViewChild('searchEl') searchEl!: ElementRef;
private controller?: AbortController;
ngOnInit() {
this.searchEl.nativeElement.filters = [
{ id: 'all', label: 'All' },
{ id: 'accounts', label: 'Accounts' },
{ id: 'transactions', label: 'Transactions' },
];
}
async onSearch({ detail }: CustomEvent) {
this.controller?.abort();
this.controller = new AbortController();
const el = this.searchEl.nativeElement;
el.setAttribute('loading', '');
try {
el.results = await this.api.search(detail.term, detail.filters, this.controller.signal);
} catch (err: any) {
if (err.name !== 'AbortError') el.setAttribute('error', 'network');
} finally {
el.removeAttribute('loading');
}
}
onSelect({ detail }: CustomEvent) {
this.router.navigate([detail.item.url]);
}
}