Astryx

Astryx Base Typeahead

Forms

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

Dependencies

More by Astryx

View all
  • Astryx Banner

    Astryx Banner

    56
  • Astryx Avatar

    Astryx Avatar

    28
  • Astryx Aspect Ratio

    Astryx Aspect Ratio

    28
  • Astryx Badge

    Astryx Badge

    25
  • Astryx Button Group

    Astryx Button Group

    23
  • Astryx Toggle Button

    Astryx Toggle Button

    16
  • Astryx Avatar Group

    Astryx Avatar Group

    8
  • Astryx Chat Composer Drawer

    Astryx Chat Composer Drawer

    7

More in Forms

View all
  • Contact Card

    Contact Card

    1.1K
  • Form

    Form

    17
  • Premium Auth

    Premium Auth

    555
  • Authentication Card

    Authentication Card

    51
  • Book A Demo 1

    Book A Demo 1

    87
  • Login 1

    Login 1

    61
  • SignInPage

    SignInPage

    736
  • Form

    Form

    12