Mohammad Shehadeh

Lazy Select

Component 1

About

Autocomplete single-select that defers loading until open and pages through results on scroll. Debounced server-side search with a pluggable lazy paginator hook.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Lazy Select to your project and fits it to your theme.

Code

How Lazy Select is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import * as React from "react";

import {
  LazySelect,
  LazySelectContent,
  LazySelectTrigger,
  useLazySelectOptions,
  type LazyPage,
} from "@/components/ui/lazy-select";

interface User {
  id: string;
  name: string;
}

const PAGE_SIZE = 20;

const FIRST_NAMES = [
  "Ava",
  "Liam",
  "Noah",
  "Mia",
  "Zoe",
  "Ezra",
  "Iris",
  "Omar",
  "Lena",
  "Kai",
];
const LAST_NAMES = [
  "Khan",
  "Reyes",
  "Okafor",
  "Park",
  "Vance",
  "Sato",
  "Diallo",
  "Costa",
];

const ALL_USERS: User[] = Array.from({ length: 137 }, (_, i) => ({
  id: `u-${i + 1}`,
  name: `${FIRST_NAMES[i % 10]} ${LAST_NAMES[i % 8]} #${i + 1}`,
}));

const fetchUsers = async ({
  query,
  page,
}: {
  query: string;
  page: number;
}): Promise<LazyPage<User>> => {
  await new Promise((r) => setTimeout(r, 450));
  const filtered = query
    ? ALL_USERS.filter((u) =>
        u.name.toLowerCase().includes(query.toLowerCase()),
      )
    : ALL_USERS;
  const start = page * PAGE_SIZE;
  const items = filtered.slice(start, start + PAGE_SIZE);
  return { items, hasMore: start + PAGE_SIZE < filtered.length };
};

const mapUser = (u: User) => ({ value: u.id, label: u.name });

export default function LazySelectDemo() {
  const [value, setValue] = React.useState<string | undefined>();
  const [open, setOpen] = React.useState(false);

  // `enabled: open` defers all network work until the dropdown is opened —
  // the "lazy" half. `loadMore` appends the next page on scroll — the
  // "autocomplete + paginate" half.
  const { setQuery, options, loading, loadingMore, hasMore, loadMore } =
    useLazySelectOptions(fetchUsers, mapUser, {
      enabled: open,
    });

  return (
    <div className="flex w-full max-w-md flex-col gap-2">
      <label
        htmlFor="lazy-assignee"
        className="text-sm font-medium text-foreground"
      >
        Assignee · lazy-loaded on open, paged on scroll
      </label>
      <LazySelect
        value={value}
        onValueChange={setValue}
        open={open}
        onOpenChange={setOpen}
        options={options}
        loading={loading}
        loadingMore={loadingMore}
        hasMore={hasMore}
        onSearchChange={setQuery}
        onLoadMore={loadMore}
      >
        <LazySelectTrigger
          id="lazy-assignee"
          placeholder="Select a teammate…"
        />
        <LazySelectContent
          searchPlaceholder="Search teammates…"
          emptyMessage="No teammates match."
          loadingMessage="Fetching teammates…"
          loadingMoreMessage="Loading more…"
          endMessage="End of list"
        />
      </LazySelect>
      <p className="text-xs uppercase text-muted-foreground">
        value = {value ? `"${value}"` : "-"} · loaded = {options.length}
      </p>
    </div>
  );
}

Dependencies

More by Mohammad Shehadeh

View all
  • Feature Problem Cards

    Feature Problem Cards

    84
  • Feature Grid Spotlight Cards

    Feature Grid Spotlight Cards

    70
  • Dithered Shader CTA

    Dithered Shader CTA

    63
  • Ecommerce Category Page

    Ecommerce Category Page

    61
  • Split Login

    Split Login

    60
  • Comparison Table

    Comparison Table

    59
  • FAQ Two Column

    FAQ Two Column

    59
  • Comparison Section

    Comparison Section

    57