stream/ui
Browse the library

Select

A native select in the brutal frame, so phones get their own picker and keyboards work as expected.

stableSelect.tsx

Examples

Basic

select/basic.tsx
import { Select } from "@streamuilib/react";

export default function SelectBasic() {
  return (
    <div className="w-full max-w-xs">
      <label htmlFor="select-region" className="mb-2 block font-bold">
        Region
      </label>
      <Select id="select-region" defaultValue="bom">
        <option value="iad">US East (Virginia)</option>
        <option value="fra">Europe (Frankfurt)</option>
        <option value="bom">Asia (Mumbai)</option>
        <option value="syd">Oceania (Sydney)</option>
      </Select>
    </div>
  );
}

Invalid and disabled

Pick a role to send the invite.

select/states.tsx
import { Select } from "@streamuilib/react";

export default function SelectStates() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-6">
      <div>
        <label htmlFor="select-role" className="mb-2 block font-bold">
          Role
        </label>
        <Select id="select-role" defaultValue="" invalid aria-describedby="select-role-error">
          <option value="" disabled>
            Choose a role…
          </option>
          <option value="admin">Admin</option>
          <option value="member">Member</option>
        </Select>
        <p id="select-role-error" className="mt-2 text-sm font-semibold">
          Pick a role to send the invite.
        </p>
      </div>
      <div>
        <label htmlFor="select-plan" className="mb-2 block font-bold">
          Plan
        </label>
        <Select id="select-plan" disabled>
          <option>Enterprise (managed)</option>
        </Select>
      </div>
    </div>
  );
}

Installation

bun add @streamuilib/react
usage.tsx
import { Select } from "@streamuilib/react";

<label htmlFor="region">Region</label>
<Select id="region">
  <option value="bom">Asia (Mumbai)</option>
</Select>

API

PropTypeDefaultDescription
invalidbooleanfalseSets aria-invalid and tints the field pink.
frameClassNamestring—Classes for the outer frame.
classNamestring—Classes for the <select> itself.
children<option> elements—Options and optgroups, exactly as in native HTML.
...propsComponentProps<"select">—Every native select attribute.

Accessibility

  • It's the platform's own <select>: typeahead, arrow keys, and the native picker on touch devices all work.
  • Use a disabled empty first option as a placeholder, and mark the field invalid if the form is submitted without a choice.

Source

packages/react/src/Select.tsx
import type { ComponentProps } from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "./cn";
import { fieldFrame } from "./field";

export type SelectProps = ComponentProps<"select"> & {
  /** Marks the field invalid (sets `aria-invalid`) and tints it. */
  invalid?: boolean;
  /** Classes for the outer frame. `className` goes to the `<select>`. */
  frameClassName?: string;
};

/**
 * A native <select> in the brutal frame. Native means the platform's own
 * picker on phones, typeahead, and full keyboard and screen reader support.
 */
export function Select({ invalid, frameClassName, className, children, ...props }: SelectProps) {
  return (
    <div className={fieldFrame({ invalid, className: cn("relative items-center", frameClassName) })}>
      <select
        aria-invalid={invalid || undefined}
        className={cn(
          "h-11 w-full min-w-0 cursor-pointer appearance-none bg-transparent pr-10 pl-3 focus:outline-none disabled:cursor-not-allowed",
          className,
        )}
        {...props}
      >
        {children}
      </select>
      <ChevronDown aria-hidden className="pointer-events-none absolute right-3 size-4.5 text-ink" strokeWidth={2.5} />
    </div>
  );
}