About
A composable Astryx combobox engine with search, keyboard navigation, custom results, and anchored dropdown positioning.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Astryx Base Typeahead to your project and fits it to your theme.
Code
How Astryx Base Typeahead is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
'use client';;
import _inject from "@stylexjs/stylex/lib/stylex-inject";
var _inject2 = _inject;
import { useRef, useState } from 'react';
import { BaseTypeahead, createStaticSource } from '@/components/ui/astryx-base-typeahead';
import { Button } from '@astryxdesign/core/Button';
import { Icon } from '@astryxdesign/core/Icon';
import { HStack, VStack } from '@astryxdesign/core/Layout';
import { RadioList, RadioListItem } from '@astryxdesign/core/RadioList';
import { Text } from '@astryxdesign/core/Text';
import '@astryxdesign/core/theme/tokens.stylex';
import { FunnelIcon } from '@heroicons/react/24/outline';
const fields = [{
id: 'status',
label: 'Status',
values: ['Open', 'In review', 'Closed']
}, {
id: 'priority',
label: 'Priority',
values: ['High', 'Medium', 'Low']
}, {
id: 'owner',
label: 'Owner',
values: ['Me', 'My team', 'Anyone']
}];
const source = createStaticSource(fields);
_inject2({
ltr: ".ph8yej3{width:100%}",
priority: 4000
});
_inject2({
ltr: ".p1lr1uin{max-width:420px}",
priority: 4000
});
_inject2({
ltr: ".p10xzikg{background-color:var(--color-background-surface)}",
priority: 3000
});
_inject2({
ltr: ".p14i3s5s{border-color:var(--color-border)}",
priority: 2000
});
_inject2({
ltr: ".ph6dtrn{border-radius:var(--radius-element)}",
priority: 2000
});
_inject2({
ltr: ".p1y0btm7{border-style:solid}",
priority: 2000
});
_inject2({
ltr: ".p1litavf{border-width:var(--border-width)}",
priority: 2000
});
_inject2({
ltr: ".p1vofgu7{padding-block:var(--spacing-1-5)}",
priority: 2000
});
_inject2({
ltr: ".pf314gf{padding-inline:var(--spacing-2)}",
priority: 2000
});
_inject2({
ltr: ".p1uczgqu{outline-color:transparent}",
priority: 3000
});
_inject2({
ltr: ".pjc0agd:has(> input:focus-visible){outline-color:var(--focus-outline-color)}",
priority: 3045
});
_inject2({
ltr: ".p1eqjuhu{outline-offset:var(--focus-outline-offset)}",
priority: 3000
});
_inject2({
ltr: ".paatb59{outline-style:solid}",
priority: 3000
});
_inject2({
ltr: ".p1k57tk5{outline-width:0}",
priority: 3000
});
_inject2({
ltr: ".p19h5306:has(> input:focus-visible){outline-width:var(--focus-outline-width)}",
priority: 3045
});
_inject2({
ltr: ".pu0wf1k{padding-block:var(--spacing-1)}",
priority: 2000
});
_inject2({
ltr: ".prrkdod{padding-inline:var(--spacing-3)}",
priority: 2000
});
_inject2({
ltr: ".p1b2ylru{padding:var(--spacing-3)}",
priority: 1000
});
_inject2({
ltr: ".p1klvvdw{min-width:260px}",
priority: 4000
});
const styles = {
root: {
kzqmXN: "ph8yej3",
ks0D6T: "p1lr1uin",
$$css: true
},
field: {
kWkggS: "p10xzikg",
kVAM5u: "p14i3s5s",
kaIpWk: "ph6dtrn",
ksu8eU: "p1y0btm7",
kMzoRj: "p1litavf",
k8WAf4: "p1vofgu7",
kg3NbH: "pf314gf",
kjBf7l: "p1uczgqu pjc0agd",
kInvED: "p1eqjuhu",
k3XXqK: "paatb59",
kMeerF: "p1k57tk5 p19h5306",
$$css: true
},
menuNote: {
k8WAf4: "pu0wf1k",
kg3NbH: "prrkdod",
$$css: true
},
editor: {
kmVPX3: "p1b2ylru",
k7Eaqz: "p1klvvdw",
$$css: true
}
};
/**
* A filter field built on BaseTypeahead's `menu`. Typing lists the fields
* with a heading and a key hint around the list. Picking a field switches the
* same menu to a dialog that edits its value; a filter's button reopens that
* dialog, and Escape returns focus to the button.
*/
function BaseTypeaheadMenu() {
const fieldRef = useRef(null);
const typeahead = useRef(null);
const [filters, setFilters] = useState([]);
const [editing, setEditing] = useState(null);
const [draft, setDraft] = useState('');
const edit = filter => {
setEditing(filter);
setDraft(filter.value);
typeahead.current?.open();
};
const menu = editing == null ? {
role: 'listbox',
render: ({
listbox,
query
}) => <>
<Text type="supporting" color="secondary" xstyle={styles.menuNote}>
{query === '' ? 'Filter by' : `Fields matching “${query}”`}
</Text>
{listbox}
<Text type="supporting" color="secondary" xstyle={styles.menuNote}>
Arrow keys move, Enter picks
</Text>
</>
} : {
role: 'dialog',
label: `${editing.field.label} filter`,
render: ({
close
}) => <VStack gap={3} xstyle={styles.editor}>
<RadioList label={editing.field.label} value={draft} onChange={setDraft}>
{editing.field.values.map(value => <RadioListItem key={value} label={value} value={value} />)}
</RadioList>
<HStack gap={2} hAlign="end">
<Button label="Cancel" variant="secondary" onClick={close} />
<Button label="Apply" variant="primary" onClick={() => {
const next = {
field: editing.field,
value: draft
};
setFilters(current => [...current.filter(f => f.field.id !== next.field.id), next]);
close();
}} />
</HStack>
</VStack>
};
return <VStack gap={3} xstyle={styles.root}>
<HStack ref={fieldRef} gap={2} vAlign="center" xstyle={styles.field}>
<Icon icon={FunnelIcon} size="sm" color="secondary" />
<BaseTypeahead aria-label="Add a filter" searchSource={source} value={null} onChange={field => {
if (field != null) {
// A field that already has a filter opens on its applied value.
edit(filters.find(filter => filter.field.id === field.id) ?? {
field,
value: field.values[0]
});
}
}} onOpenChange={isOpen => {
if (!isOpen) {
setEditing(null);
}
}} handleRef={typeahead} menu={menu} anchorRef={fieldRef} placeholder="Add a filter…" hasEntriesOnFocus debounceMs={0} />
</HStack>
<HStack gap={2} wrap="wrap">
{filters.length === 0 ? <Text type="supporting" color="secondary">
No filters
</Text> : filters.map(filter => <Button key={filter.field.id} label={`${filter.field.label}: ${filter.value}`} variant="secondary" size="sm" onClick={() => edit(filter)} />)}
</HStack>
</VStack>;
}
export default function BaseTypeaheadMenuDemo() {
return (
<div
data-astryx-theme="neutral"
data-theme="light"
style={{colorScheme: 'light', display: 'contents'}}>
<BaseTypeaheadMenu />
</div>
);
}














