shadimage
cubby-ui
uiautocompletesource: ok · hit

Autocomplete

A autocomplete component.

npx shadcn add "https://www.cubby-ui.dev/r/autocomplete.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@hugeicons/react@hugeicons/core-free-icons

Registry dependencies

@cubby-ui/scroll-area@cubby-ui/elevated

Files1 · source cached 10/13/2026, 2:12:20 AM

registry/default/autocomplete/autocomplete.tsx
import * as React from "react";
import { Autocomplete as BaseAutocomplete } from "@base-ui/react/autocomplete";
import { cn } from "@/lib/utils";
import {
  elevatedSurface,
  type SurfaceLevel,
} from "@/registry/default/lib/elevated";
import {
  ScrollArea,
  type ScrollAreaProps,
} from "@/registry/default/scroll-area/scroll-area";

import { HugeiconsIcon } from "@hugeicons/react";
import { Cancel01Icon, ChevronDownIcon } from "@hugeicons/core-free-icons";
const AutocompleteRoot = BaseAutocomplete.Root;

function AutocompleteInput({
  className,
  showTrigger = false,
  showClear = false,
  variant = "default",
  ...props
}: BaseAutocomplete.Input.Props & {
  showTrigger?: boolean;
  showClear?: boolean;
  variant?: "default" | "elevated";
}) {
  return (
    <BaseAutocomplete.InputGroup
      data-slot="autocomplete-input-group"
      className={cn(
        "relative",
        // Room for the clear and trigger buttons while they're there (the
        // clear button only shows with text). On the group, with the input,
        // a direct child, as the target: `has-*:**:` variants compile to
        // `:has()` inside `:is(… *)`, which makes Chrome restyle the whole
        // page on any DOM insertion.
        "[&:has([data-slot=autocomplete-clear])>[data-slot=autocomplete-input]]:pr-7",
        "[&:has([data-slot=autocomplete-trigger])>[data-slot=autocomplete-input]]:pr-7",
        "[&:has([data-slot=autocomplete-clear]):has([data-slot=autocomplete-trigger])>[data-slot=autocomplete-input]]:pr-13",
      )}
    >
      <BaseAutocomplete.Input
        data-slot="autocomplete-input"
        className={cn(
          "placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-10 w-full min-w-0 rounded-lg border bg-clip-padding px-3 text-base font-normal disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-60 sm:h-9 md:text-sm",
          variant === "default" ? "bg-input" : "bg-input-elevated",
          "file:text-foreground file:inline-flex file:h-7 file:rounded-md file:border-0 file:bg-transparent file:text-sm file:font-medium",
          "focus-visible:outline-ring/50 outline-0 outline-offset-0 outline-transparent transition-[outline-width,outline-offset,outline-color] duration-100 ease-out outline-solid focus-visible:outline-2 focus-visible:outline-offset-2",
          "aria-invalid:outline-destructive/50 aria-invalid:outline-2 aria-invalid:outline-offset-2 aria-invalid:outline-solid",
          className,
        )}
        {...props}
      />
      {(showClear || showTrigger) && (
        <div className="absolute inset-y-0 right-3 flex items-center gap-2">
          {showClear && <AutocompleteClear />}
          {showTrigger && <AutocompleteTrigger />}
        </div>
      )}
    </BaseAutocomplete.InputGroup>
  );
}

function AutocompleteTrigger({
  className,
  children,
  ...props
}: BaseAutocomplete.Trigger.Props) {
  return (
    <BaseAutocomplete.Trigger
      data-slot="autocomplete-trigger"
      aria-label="Open popup"
      className={cn(
        "inline-flex size-4 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-60",
        "focus-visible:ring-ring/70 outline-none focus-visible:ring-2",
        className,
      )}
      {...props}
    >
      {children ?? (
        <HugeiconsIcon
          icon={ChevronDownIcon}
          className="h-4 w-4"
          strokeWidth={2}
        />
      )}
    </BaseAutocomplete.Trigger>
  );
}

function AutocompleteIcon({
  className,
  ...props
}: React.ComponentProps<typeof BaseAutocomplete.Icon>) {
  return (
    <BaseAutocomplete.Icon
      data-slot="autocomplete-icon"
      className={cn("ml-2 h-4 w-4 shrink-0 opacity-50", className)}
      {...props}
    />
  );
}

function AutocompleteClear({
  className,
  children,
  ...props
}: BaseAutocomplete.Clear.Props) {
  return (
    <BaseAutocomplete.Clear
      data-slot="autocomplete-clear"
      aria-label="Clear selection"
      className={cn(
        "inline-flex h-4 w-4 cursor-pointer items-center justify-center rounded-sm opacity-70 transition-[opacity,scale,transform,translate] hover:opacity-100 disabled:pointer-events-none",
        "focus-visible:ring-ring/70 duration-100 outline-none focus-visible:ring-2",
        "data-ending-style:translate-x-1 data-ending-style:opacity-0 data-starting-style:translate-x-1 data-starting-style:opacity-0",
        className,
      )}
      {...props}
    >
      {children ?? (
        <HugeiconsIcon
          icon={Cancel01Icon}
          className="h-4 w-4"
          strokeWidth={2}
        />
      )}
    </BaseAutocomplete.Clear>
  );
}

function AutocompleteValue({ ...props }: BaseAutocomplete.Value.Props) {
  return <BaseAutocomplete.Value data-slot="autocomplete-value" {...props} />;
}

function AutocompletePortal({ ...props }: BaseAutocomplete.Portal.Props) {
  return <BaseAutocomplete.Portal data-slot="autocomplete-portal" {...props} />;
}

function AutocompleteBackdrop({
  className,
  ...props
}: BaseAutocomplete.Backdrop.Props) {
  return (
    <BaseAutocomplete.Backdrop
      data-slot="autocomplete-backdrop"
      className={cn(
        "fixed inset-0 z-30 bg-black/50 data-ending-style:opacity-0 data-starting-style:opacity-0",
        className,
      )}
      {...props}
    />
  );
}

function AutocompletePositioner({
  className,
  ...props
}: BaseAutocomplete.Positioner.Props) {
  return (
    <BaseAutocomplete.Positioner
      data-slot="autocomplete-positioner"
      sideOffset={4}
      className={cn("", className)}
      {...props}
    />
  );
}

function AutocompletePopup({
  className,
  level = 3,
  shadowLevel = 3,
  ...props
}: BaseAutocomplete.Popup.Props & {
  /** Surface elevation level for the popup bg (1-8). Bump when nesting inside a Dialog. Defaults to 3. */
  level?: SurfaceLevel;
  /** Shadow weight (1-8). Pinned to 3 by default so the autocomplete reads the same regardless of nesting depth. */
  shadowLevel?: SurfaceLevel;
}) {
  return (
    <BaseAutocomplete.Popup
      data-slot="autocomplete-popup"
      data-level={level}
      className={cn(
        "text-popover-foreground ease-out-expo flex max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) flex-col overflow-clip overscroll-contain rounded-xl transition-[transform,scale,opacity] duration-100 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0",
        // Use elevatedSurface (rim on ::after) because group labels can be
        // sticky and would otherwise hide the rim where they sit.
        elevatedSurface(level, shadowLevel),
        className,
      )}
      {...props}
    />
  );
}

function AutocompleteArrow({
  className,
  ...props
}: BaseAutocomplete.Arrow.Props) {
  return (
    <BaseAutocomplete.Arrow
      data-slot="autocomplete-arrow"
      className={cn(
        "data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180",
        className,
      )}
      {...props}
    >
      <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
        <path
          d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V9H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
          className="fill-(--popup-surface,var(--popover))"
        />
        <path
          d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
          className="fill-border/70"
        />
      </svg>
    </BaseAutocomplete.Arrow>
  );
}

function AutocompleteStatus({
  className,
  ...props
}: BaseAutocomplete.Status.Props) {
  return (
    <BaseAutocomplete.Status
      data-slot="autocomplete-status"
      className={cn(
        "text-muted-foreground px-3 py-2.5 text-sm leading-5 empty:m-0 empty:p-0",
        className,
      )}
      {...props}
    />
  );
}

function AutocompleteEmpty({
  className,
  ...props
}: BaseAutocomplete.Empty.Props) {
  return (
    <BaseAutocomplete.Empty
      data-slot="autocomplete-empty"
      className={cn(
        "text-muted-foreground px-3 py-2.5 text-sm empty:m-0 empty:p-0",
        className,
      )}
      {...props}
    />
  );
}

function AutocompleteList({
  className,
  nativeScroll = false,
  fadeEdges = true,
  scrollbarGutter = false,
  persistScrollbar,
  hideScrollbar,
  ...props
}: BaseAutocomplete.List.Props &
  Pick<
    ScrollAreaProps,
    | "nativeScroll"
    | "fadeEdges"
    | "scrollbarGutter"
    | "persistScrollbar"
    | "hideScrollbar"
  >) {
  return (
    <ScrollArea
      nativeScroll={nativeScroll}
      fadeEdges={fadeEdges}
      scrollbarGutter={scrollbarGutter}
      persistScrollbar={persistScrollbar}
      hideScrollbar={hideScrollbar}
      className={cn("max-h-80", className)}
    >
      <BaseAutocomplete.List
        data-slot="autocomplete-list"
        className="rounded-xl"
        {...props}
      />
    </ScrollArea>
  );
}

function AutocompleteVirtualizedList({
  className,
  children,
  scrollRef,
  totalSize,
  emptyMessage = "No results found.",
  fadeEdges = "y",
  nativeScroll = false,
  ...props
}: Omit<React.ComponentProps<"div">, "ref"> &
  Pick<ScrollAreaProps, "fadeEdges" | "nativeScroll"> & {
    scrollRef: (element: HTMLDivElement | null) => void;
    totalSize: number;
    emptyMessage?: React.ReactNode;
  }) {
  return (
    <>
      <BaseAutocomplete.Empty
        data-slot="autocomplete-empty"
        className="text-muted-foreground px-3 py-2.5 text-sm empty:m-0 empty:p-0"
      >
        {emptyMessage}
      </BaseAutocomplete.Empty>
      <BaseAutocomplete.List
        data-slot="autocomplete-list"
        className="w-full flex-1 overflow-hidden rounded-xl p-0 outline-hidden empty:m-0 empty:p-0"
      >
        <ScrollArea
          viewportRef={scrollRef}
          viewportClassName={cn("scroll-py-2", className)}
          fadeEdges={fadeEdges}
          nativeScroll={nativeScroll}
          className="h-auto max-h-80 w-full"
          {...props}
        >
          {/* Virtual placeholder for total height */}
          <div
            role="presentation"
            className="relative w-full"
            style={{ height: totalSize }}
          >
            {children}
          </div>
        </ScrollArea>
      </BaseAutocomplete.List>
    </>
  );
}

function AutocompleteCollection({
  ...props
}: BaseAutocomplete.Collection.Props) {
  return (
    <BaseAutocomplete.Collection
      data-slot="autocomplete-collection"
      {...props}
    />
  );
}

function AutocompleteRow({ className, ...props }: BaseAutocomplete.Row.Props) {
  return (
    <BaseAutocomplete.Row
      data-slot="autocomplete-row"
      className={cn("flex", className)}
      {...props}
    />
  );
}

function AutocompleteItem({
  className,
  ref,
  ...props
}: BaseAutocomplete.Item.Props & {
  ref?: React.Ref<HTMLDivElement>;
}) {
  return (
    <BaseAutocomplete.Item
      ref={ref}
      data-slot="autocomplete-item"
      className={cn(
        "data-highlighted:text-accent-foreground data-highlighted:bg-surface-hover relative flex cursor-default items-center rounded-md px-2.5 py-2 text-sm outline-none select-none data-disabled:pointer-events-none data-disabled:opacity-60",
        // Spacing from list edges
        "mx-1 first:mt-1 last:mb-1",
        className,
      )}
      {...props}
    />
  );
}

function AutocompleteGroup({
  className,
  ...props
}: BaseAutocomplete.Group.Props) {
  return (
    <BaseAutocomplete.Group
      data-slot="autocomplete-group"
      className={cn("text-foreground block", className)}
      {...props}
    />
  );
}

function AutocompleteGroupLabel({
  className,
  ...props
}: BaseAutocomplete.GroupLabel.Props) {
  return (
    <BaseAutocomplete.GroupLabel
      data-slot="autocomplete-group-label"
      className={cn(
        "text-muted-foreground bg-(--popup-surface,var(--popover)) px-3.5 py-1.5 pt-2.5 text-xs font-semibold",
        className,
      )}
      {...props}
    />
  );
}

function AutocompleteSeparator({
  className,
  ...props
}: BaseAutocomplete.Separator.Props) {
  return (
    <BaseAutocomplete.Separator
      data-slot="autocomplete-separator"
      className={cn("bg-border mx-1 my-1 h-px min-h-px", className)}
      {...props}
    />
  );
}

export const Autocomplete = {
  Root: AutocompleteRoot,
  Input: AutocompleteInput,
  Trigger: AutocompleteTrigger,
  Icon: AutocompleteIcon,
  Clear: AutocompleteClear,
  Value: AutocompleteValue,
  Portal: AutocompletePortal,
  Backdrop: AutocompleteBackdrop,
  Positioner: AutocompletePositioner,
  Popup: AutocompletePopup,
  Arrow: AutocompleteArrow,
  Status: AutocompleteStatus,
  Empty: AutocompleteEmpty,
  List: AutocompleteList,
  VirtualizedList: AutocompleteVirtualizedList,
  Collection: AutocompleteCollection,
  Row: AutocompleteRow,
  Item: AutocompleteItem,
  Group: AutocompleteGroup,
  GroupLabel: AutocompleteGroupLabel,
  Separator: AutocompleteSeparator,
};

const useAutocompleteFilter = BaseAutocomplete.useFilter;
const useAutocompleteFilteredItems = BaseAutocomplete.useFilteredItems;

export {
  AutocompleteRoot,
  AutocompleteInput,
  AutocompleteTrigger,
  AutocompleteIcon,
  AutocompleteClear,
  AutocompleteValue,
  AutocompletePortal,
  AutocompleteBackdrop,
  AutocompletePositioner,
  AutocompletePopup,
  AutocompleteArrow,
  AutocompleteStatus,
  AutocompleteEmpty,
  AutocompleteList,
  AutocompleteVirtualizedList,
  AutocompleteCollection,
  AutocompleteRow,
  AutocompleteItem,
  AutocompleteGroup,
  AutocompleteGroupLabel,
  AutocompleteSeparator,
  useAutocompleteFilter,
  useAutocompleteFilteredItems,
};

More from cubby-ui

Browse all components in this library →