Live Component

Online — Ready Filter: all Results: —

Simulate Scenarios

Click a scenario, then type in the search box to trigger it.

600ms

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]);
  }
}