List Box

PreviousNext

React aria components List Box with shadcn characteristics.

Docs
ouiui

Preview

Loading preview…
registry/default/ui/oui-list-box.tsx
"use client";

/**
 * @fileoverview ListBox components for the Oui UI library.
 *
 * This module provides styled ListBoxItem components but intentionally does not
 * provide a styled ListBox wrapper. The Rac.ListBox component is used directly
 * without additional styling for the following reasons:
 *
 * 1. **Context-Dependent Styling**: ListBox containers are often used in different
 *    contexts (standalone lists, dropdowns, sidebars, pagination) where styling
 *    needs vary significantly. A single styled wrapper would be too restrictive.
 *
 * 2. **Minimal Container Role**: The ListBox component primarily handles behavior
 *    (keyboard navigation, selection, accessibility) rather than visual presentation.
 *    Visual styling is typically applied to the items themselves or the parent container.
 *
 * 3. **Flexibility**: By not providing a styled wrapper, users can apply their own
 *    container styles or use the ListBox in unstyled contexts.
 *
 * 4. **Common Usage Patterns**: In most cases, ListBox is used within other
 *    components like Select, ComboBox, or Popover, where the container styling
 *    is handled by the parent component.
 *
 * @example
 * // Standalone list with custom `container` styling
 * <div className="max-h-64 overflow-auto border rounded-md">
 *   <Rac.ListBox aria-label="Options" selectionMode="single">
 *     <ListBoxItem>Option 1</ListBoxItem>
 *     <ListBoxItem>Option 2</ListBoxItem>
 *   </Rac.ListBox>
 * </div>
 *
 * @example
 * // Within a Select dropdown (container styled by Popover)
 * <Select>
 *   <SelectButton>Select an option</SelectButton>
 *   <Popover>
 *     <Rac.ListBox items={options}>
 *       {(item) => <ListBoxItem>{item.name}</ListBoxItem>}
 *     </Rac.ListBox>
 *   </Popover>
 * </Select>
 *
 * @example
 * // Horizontal pagination (custom wrapper for specific use case)
 * <ListBoxExPagination>
 *   <ListBoxItemExPagination>1</ListBoxItemExPagination>
 *   <ListBoxItemExPagination>2</ListBoxItemExPagination>
 * </ListBoxExPagination>
 */
import {
  composeTailwindRenderProps,
  disabledStyles,
  focusVisibleStyles,
} from "@/registry/default/ui/oui-base";
import { Check } from "lucide-react";
import * as Rac from "react-aria-components";
import { twMerge } from "tailwind-merge";

/** Styles derived from shadcn SelectItem.
 * https://github.com/adobe/react-spectrum/issues/7601
 */
export function ListBoxItem<T extends object>({
  className,
  children,
  ...props
}: Rac.ListBoxItemProps<T>) {
  return (
    <Rac.ListBoxItem
      {...props}
      textValue={
        props.textValue ?? (typeof children === "string" ? children : undefined)
      }
      className={composeTailwindRenderProps(className, [
        focusVisibleStyles,
        disabledStyles,
        "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none select-none",
        "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
        "data-focused:bg-accent data-focused:text-accent-foreground",
        "data-hovered:bg-accent data-hovered:text-accent-foreground",
      ])}
    >
      {Rac.composeRenderProps(children, (children, renderProps) => (
        <>
          {renderProps.isSelected && (
            <span className="absolute right-2 flex size-3.5 items-center justify-center">
              <Check className="size-4" />
            </span>
          )}
          {children}
        </>
      ))}
    </Rac.ListBoxItem>
  );
}

/**
 * Dervied from shadcn SelectLabel
 */
export function ListBoxHeader({
  className,
  ...props
}: React.ComponentProps<typeof Rac.Header>) {
  return (
    <Rac.Header
      data-slot="list-box-header"
      className={twMerge(
        "px-2 py-1.5 text-xs text-muted-foreground",
        className,
      )}
      {...props}
    />
  );
}

Installation

npx shadcn@latest add @oui/oui-list-box

Usage

import { OuiListBox } from "@/components/ui/oui-list-box"
<OuiListBox />