Skip to content
Dethink Components

ComponentsAsyncSelect

AsyncSelect

Let users choose from options loaded by your app, with loading and retry states.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

Import the component into your page or component file.

Usage
import { AsyncSelect } from "@dethink/components";

Type to change the app-owned query; the examples filter local arrays to model a server result window.

Basic

Single-value search where the app controls the query and result list.

Show sourceexamples/async-select/basic.tsx
examples/async-select/basic.tsx
"use client";

import { useMemo, useState } from "react";
import { AsyncSelect } from "@dethink/components";

const accountItems = [
  { label: "Acme Operations", value: "acme" },
  { label: "Dethink Labs", value: "dethink" },
  { label: "Northstar Systems", value: "northstar" },
  { label: "Signal Foundry", value: "signal" },
];

export function AsyncSelectBasic() {
  const [query, setQuery] = useState("");
  const items = useMemo(
    () =>
      accountItems.filter((item) =>
        item.label.toLocaleLowerCase().includes(query.toLocaleLowerCase()),
      ),
    [query],
  );

  return (
    <div className="mx-auto max-w-sm">
      <AsyncSelect
        inputValue={query}
        items={items}
        label="Account"
        name="account"
        onInputValueChange={setQuery}
        placeholder="Search accounts"
      />
    </div>
  );
}

Multiple values

Multiple mode keeps selected chip labels visible even when the current result window changes.

Mira Patel
1 option selected

Selected labels stay visible while server results change.

Show sourceexamples/async-select/multiple.tsx
examples/async-select/multiple.tsx
"use client";

import { useState } from "react";
import { AsyncSelect, FieldDescription, Stack } from "@dethink/components";

const ownerItems = [
  { label: "Ari Chen", value: "ari" },
  { label: "Mira Patel", value: "mira" },
  { label: "Noah Smith", value: "noah" },
  { label: "Sana Iqbal", value: "sana" },
];

export function AsyncSelectMultiple() {
  const [query, setQuery] = useState("");
  const [value, setValue] = useState<string[]>(["mira"]);
  const items = ownerItems.filter((item) =>
    item.label.toLocaleLowerCase().includes(query.toLocaleLowerCase()),
  );

  return (
    <div className="mx-auto max-w-sm">
      <Stack gap="3">
        <AsyncSelect
          selectionMode="multiple"
          inputValue={query}
          items={items}
          label="Owners"
          name="owners"
          onInputValueChange={setQuery}
          onValueChange={(nextValue) => setValue(nextValue as string[])}
          selectedItems={ownerItems.filter((item) =>
            value.includes(item.value),
          )}
          value={value}
        />
        <FieldDescription>
          Selected labels stay visible while server results change.
        </FieldDescription>
      </Stack>
    </div>
  );
}

Async states

Loading, empty, error-with-retry, invalid, required, read-only, and disabled are explicit props.

Finding customers...
No customer found.
Choose an account before saving.
No results found.
Show sourceexamples/async-select/states.tsx
examples/async-select/states.tsx
"use client";

import { AsyncSelect } from "@dethink/components";

export function AsyncSelectStates() {
  return (
    <div className="space-y-4">
      <div className="grid gap-4 md:grid-cols-3">
        <AsyncSelect
          inputValue="ac"
          items={[]}
          label="Loading customer"
          loading
          loadingMessage="Finding customers..."
        />
        <AsyncSelect
          inputValue="missing"
          items={[]}
          label="Empty customer"
          emptyMessage="No customer found."
        />
        <AsyncSelect
          error="Customer lookup failed."
          inputValue="acme"
          items={[]}
          label="Errored customer"
          onRetry={() => undefined}
          retryLabel="Try again"
        />
      </div>
      <div className="grid gap-4 md:grid-cols-3">
        <AsyncSelect
          errorMessage="Choose an account before saving."
          invalid
          items={[]}
          label="Required account"
          required
        />
        <AsyncSelect
          readOnly
          defaultValue="acme"
          items={[{ label: "Acme Operations", value: "acme" }]}
          label="Inherited account"
        />
        <AsyncSelect
          disabled
          defaultValue="dethink"
          items={[{ label: "Dethink Labs", value: "dethink" }]}
          label="Locked account"
        />
      </div>
    </div>
  );
}

Theme, RTL, and wrapping

Multiple async selection using compact dark tokens, RTL direction, and narrow chip wrapping.

Ari ChenMira PatelSana Iqbal
3 options selected
Show sourceexamples/async-select/theme-and-wrapping.tsx
examples/async-select/theme-and-wrapping.tsx
"use client";

import { AsyncSelect, DethinkProvider } from "@dethink/components";

const ownerItems = [
  { label: "Ari Chen", value: "ari" },
  { label: "Mira Patel", value: "mira" },
  { label: "Noah Smith", value: "noah" },
  { label: "Sana Iqbal", value: "sana" },
];

export function AsyncSelectThemeAndWrapping() {
  return (
    <DethinkProvider theme="dark" density="compact" dir="rtl">
      <div className="border-border bg-background mx-auto max-w-72 rounded-lg border p-4">
        <AsyncSelect
          selectionMode="multiple"
          defaultValue={["ari", "mira", "sana"] as string[]}
          items={ownerItems}
          label="مالكو الحساب"
          name="accountOwners"
        />
      </div>
    </DethinkProvider>
  );
}

Examples that combine components for common tasks.

Server filter

A form-ready server lookup with min-query guidance and a second field using a stable result.

Type three characters before querying the server.
Show sourceexamples/async-select/recipe-server-filter.tsx
examples/async-select/recipe-server-filter.tsx
"use client";

import { useMemo, useState } from "react";
import { AsyncSelect, Form, Stack } from "@dethink/components";

const modelItems = [
  { label: "Fast summarizer", value: "fast-summarizer" },
  { label: "Reasoning planner", value: "reasoning-planner" },
  { label: "Vision analyst", value: "vision-analyst" },
  { label: "Customer support bot", value: "support-bot" },
];

export function AsyncSelectRecipeServerFilter() {
  const [query, setQuery] = useState("");
  const items = useMemo(
    () =>
      query.trim().length < 3
        ? []
        : modelItems.filter((item) =>
            item.label.toLocaleLowerCase().includes(query.toLocaleLowerCase()),
          ),
    [query],
  );

  return (
    <Form action="/models" method="get" className="mx-auto max-w-sm">
      <Stack gap="4">
        <AsyncSelect
          inputValue={query}
          items={items}
          label="Model"
          minQueryLength={3}
          minQueryMessage="Type three characters before querying the server."
          name="model"
          onInputValueChange={setQuery}
          placeholder="Search models"
        />
        <AsyncSelect
          inputValue="ari"
          items={[{ label: "Ari Chen", value: "ari" }]}
          label="Fallback owner"
          name="fallbackOwner"
        />
      </Stack>
    </Form>
  );
}

AsyncSelect accepts a shared item shape: { value, label?, textValue? }.

AsyncSelect props
PropWhat it doesDefault
selectionMode"single" | "multiple"Whether the component renders Combobox or MultiSelect behavior."single"
value / defaultValue / onValueChangestring | null | string[]Controlled or uncontrolled selected value shape.Not set
inputValue / defaultInputValue / onInputValueChangestring / string / (text) => voidApp-owned query state. AsyncSelect never fetches; your app owns the server call.""
items / selectedItemsIterable<AsyncSelectItemData>Current result window and optional selected item data for stable labels.Not set
loading / error / onRetryboolean / ReactNode / () => voidExplicit async status flags and retry action.false / — / —
emptyMessage / loadingMessage / minQueryMessageReactNodeMessages rendered in the status region.built-in copy
minQueryLengthnumberMinimum query length before results should be shown.0
disabledKeysIterable<string>Result values that cannot be selected.Not set
label / description / errorMessageReactNodeThe field label, help text, and error message.Not set
namestringHidden input name for native forms; multiple mode submits repeated values.Not set