Themeable, pluggable command-palette search widget for static sites.
Hawk is a JS-embeddable search widget: npm install + a constructor call (or a <script> tag +
a constructor call), no host markup beyond a trigger element required. It owns the DOM, keyboard
navigation, and result rendering; Pagefind owns indexing, ranking, and
typo-tolerant matching — hawk is Pagefind's own front end, exposing Pagefind's filtering and
search-tuning options directly rather than hiding them behind a narrower API. Visual theme and
reveal animation are swappable plugins rather than hardcoded choices. A TypeFlow-typewriter
animation driver ships alongside a zero-dependency instant driver, so TypeFlow is never a hard
dependency of the core bundle. (Hawk started as an extraction of typeflow's own docs site search
— typeflow is one theme/animation pairing it ships with today, not what it's limited to.)
See architecture/README.md for the design rationale, and architecture/plugin-system.md /
architecture/theme-contract.md / architecture/animation-driver-contract.md for the plugin
interfaces.
- Pagefind-backed search — chunked, lazily-loaded index; scales past a hand-rolled substring-scored in-browser index.
- Dev-controlled tabs: auto-derived category filter pills by default (from Pagefind's
filters()), or an explicittabsconfig where each tab'sfilteris a raw Pagefind filters object (multi-dimension AND, multi-value OR — full Pagefind filtering power, no translation layer) with its own optionalpagefindOptionsoverride. A top-levelpagefindOptionsescape hatch merges into every search call forsort/ranking/languagetuning hawk doesn't otherwise wrap. - Fit / medium / compact density modes (
params.density), switchable via config or an in-modal toggle whose choice persists tolocalStorage. - Two-pane keyboard navigation: Tab moves between the search input, filter tabs, results pane,
and preview pane; arrow keys change selection in the results pane and scroll the preview pane,
depending on which has focus (plus the underlying arrow-key wraparound selection, Enter to
navigate, Escape to close, Ctrl/Cmd+K and
/to open). - Configurable initial state:
defaultTab,defaultPreviewUrl(shows a specific page's content on mount, before any selection), andeagerResults(an empty-query listing on open instead of an empty "Type to search" state, on by default). - Configurable layout:
scale,position,offset,alignment,columns, and a JS-drivenbreakpoint(not a fixed CSS media query) that collapses to a single column and hides the preview pane below the configured width. - Theme plugin system: every color in
hawk.cssis a--hawk-*custom property with a neutral fallback — a theme only needs to redefine tokens (font, match-highlight, and scrollbar tokens included). Ships withhawk-theme-typeflow, the visual identity ported from typeflow's docs search.theme.tokensoffers a JS-only alternative to a CSS file for a few color/font tweaks;disableDefaultMarkdownStylesopts out of the built-in preview-content prose rules entirely. - Animation driver plugin system:
typeTitle/typeContent/rotatePlaceholder/stop. Ships withhawk-animation-instant(default, zero dependencies) andhawk-animation-typeflow(wrapsTypeFlow.type()/TypeFlow.rotate(), including anchor-title scramble-decode and a rotating example-query placeholder in the search input). - Structured inline filter syntax (
inlineFilters: true): typingcategory:Guides responsiveparses out a Pagefind filter and searches only the remaining text. - Result/page metadata: every match carries Pagefind's full
metaobject;meta.sectionrenders as a badge;state.pageCounttracks distinct pages separately from the flattened row count. - Pin system: a pin toggle on every result, persisted to
localStorage, surfaced as a synthetic "Pinned" tab once at least one exists. - A built-in trigger element (
config.trigger,kind: 'minimal' | 'compact' | 'default') for sites that don't want to hand-build trigger markup, plus a text-only "Search powered by Pagefind" attribution (showPoweredBy). - Accessibility/reliability: a real modal focus trap (Tab/Shift+Tab never escape to the page
behind the backdrop), a "search unavailable" state instead of an unhandled rejection if
Pagefind fails to load or a search call rejects, and full i18n via
config.strings.
npm install @staticcanvas/hawk
npm install --save-dev pagefindpagefind is a real dependency (hawk has nothing to search without an index).
@staticcanvas/typeflow is an optional peer dependency — only needed for the typeflow
theme/animation driver instead of the built-in unstyled/instant ones.
Build a Pagefind index against your built site (see scripts/pagefind-index.mjs — content region
must carry data-pagefind-body, chrome should carry data-pagefind-ignore):
node scripts/pagefind-index.mjs --site distMount hawk against it:
import { HawkSearch } from '@staticcanvas/hawk';
import '@staticcanvas/hawk/theme-typeflow/css';
import { HawkThemeTypeflow } from '@staticcanvas/hawk/theme-typeflow';
HawkSearch.registerTheme('typeflow', HawkThemeTypeflow);
const search = new HawkSearch({
index: '/pagefind/',
triggerSelector: '[data-search-trigger]',
theme: 'typeflow',
params: { scale: 1, position: 'top-center', columns: 2, breakpoint: 768, density: 'medium' },
tabs: [
{ label: 'Guides', filter: { category: 'Guides' } },
{ id: 'ref', label: 'Reference', filter: { category: ['Reference', 'API'] } },
],
defaultTab: 'guides',
defaultPreviewUrl: '/getting-started/',
inlineFilters: true,
rotatingPlaceholders: ['Try "tabs"...', 'Try "theming"...'],
enablePins: true,
});Any element matching triggerSelector opens the modal — hawk builds its own DOM, no other markup
required (or skip building a trigger at all with trigger: { container: '#search-slot' }). Call
search.setParams({...}) to change layout at runtime, search.state to read
{ open, query, selectedIndex, matches, pageCount, pins, activeTab, error }, and
search.destroy() to tear it down. See docs/content/api.md for the full
config reference, and the guides for tabs and inline filter
syntax,
density, keyboard navigation, and scrollbars,
theming,
animation plugins and the rotating placeholder, and
pins, the built-in trigger, and attribution.
See architecture/theme-contract.md for the full token list and the contract a theme
implements. Load a theme's CSS separately from its JS (@staticcanvas/hawk/theme-typeflow/css),
matching this org's existing "./css" subpath-export convention.
See architecture/animation-driver-contract.md. Pass animation: false to force the instant
driver even when a typeflow driver is registered; omit animation entirely to prefer typeflow
when registered, falling back to instant otherwise.
Full type declarations in src/hawk.d.ts (core), src/hawk-theme-typeflow.d.ts, and
src/hawk-animation-typeflow.d.ts. Published docs: https://staticcanvas.gitlab.io/hawk/api
| Script | Purpose |
|---|---|
npm run dev |
Vite dev server |
npm run build |
Builds core + optional (theme-typeflow, animation-typeflow) bundles |
npm run dev:typeflow |
Builds hawk and copies the bundle + a preview Pagefind index into the sibling typeflow checkout's docs site, for live prototyping |
npm run pagefind:index |
Wraps pagefind --site <dir> — see scripts/pagefind-index.mjs |
npm test / npm run test:coverage |
Vitest (jsdom) — unit coverage |
npm run test:e2e |
Playwright, real Chromium against a real built docs site + Pagefind index (not a fixture) — builds the docs site with a localhost baseURL override first. Runs in CI as test:e2e (allow_failure: true until it's proven stable there — see .gitlab-ci.yml). |
npm run lint / npm run lint:fix |
ESLint |
npm run docs:build / npm run docs:serve |
Builds and serves hawk's own Hugo docs site |
npm run check:size |
Enforces the gzip bundle size budget |
npm run validate:release |
CI release-artifact validation |
MIT