HextaUI

Select

現在の値の位置で開くリストから、1つ以上の option を選びます。先頭文字検索、グループ、フォームに対応しています。

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const fonts = [
  { value: "inter", label: "Inter" },
  { value: "geist", label: "Geist" },
  { value: "ibm-plex", label: "IBM Plex Sans" },
  { value: "source-serif", label: "Source Serif" },
  { value: "jetbrains", label: "JetBrains Mono" },
]

export function SelectDemo() {
  return (
    <Select items={fonts} defaultValue="geist">
      <SelectTrigger aria-label="Font" className="w-48">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        {fonts.map((font) => (
          <SelectItem key={font.value} value={font.value}>
            {font.label}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/select.json

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
<Select items={fonts} defaultValue="geist">
  <SelectTrigger aria-label="Font">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    {fonts.map((font) => (
      <SelectItem key={font.value} value={font.value}>
        {font.label}
      </SelectItem>
    ))}
  </SelectContent>
</Select>

リストはトリガーの真上に開き、現在の option が値の上に重なるので、視線が位置を見失いません。SelectValue が生の値ではなくラベルを表示するよう、items を渡します。検索が必要な長いリストには Combobox を使います。

Select
├── SelectTrigger
│   └── SelectValue
└── SelectContent
    ├── SelectGroup
    │   ├── SelectLabel
    │   └── SelectItem
    └── SelectSeparator

サイズ

SelectTrigger の size は、Input と Button の高さに合います。

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const sizes = ["sm", "default", "lg"] as const

export function SelectSizes() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      {sizes.map((size) => (
        <Select key={size} defaultValue="week">
          <SelectTrigger size={size} aria-label={`Range (${size})`}>
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="day">Today</SelectItem>
            <SelectItem value="week">This week</SelectItem>
            <SelectItem value="month">This month</SelectItem>
          </SelectContent>
        </Select>
      ))}
    </div>
  )
}

グループと長いリスト

SelectGroup と SelectLabel で option をグループ化します。長いリストは画面に収まり、ホバーするとスクロールする矢印が表示されます。

import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const zones = [
  {
    label: "Americas",
    items: [
      "Los Angeles",
      "Denver",
      "Chicago",
      "New York",
      "Toronto",
      "Mexico City",
      "Bogotá",
      "São Paulo",
      "Buenos Aires",
    ],
  },
  {
    label: "Europe",
    items: ["London", "Lisbon", "Paris", "Berlin", "Stockholm", "Athens"],
  },
  {
    label: "Asia",
    items: ["Dubai", "Mumbai", "Singapore", "Shanghai", "Tokyo", "Seoul"],
  },
]

export function SelectGroups() {
  return (
    <Select defaultValue="Berlin">
      <SelectTrigger aria-label="Time zone" className="w-56">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        {zones.map((zone, index) => (
          <SelectGroup key={zone.label}>
            {index > 0 ? <SelectSeparator /> : null}
            <SelectLabel>{zone.label}</SelectLabel>
            {zone.items.map((city) => (
              <SelectItem key={city} value={city}>
                {city}
              </SelectItem>
            ))}
          </SelectGroup>
        ))}
      </SelectContent>
    </Select>
  )
}

アイコンつき

項目にアイコンを入れ、SelectValue に関数を渡すと、トリガーにも同じアイコンを表示できます。

"use client"

import {
  IconCircleCheck,
  IconCircleDashed,
  IconCircleHalf2,
  IconCircleX,
} from "@tabler/icons-react"

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const statuses = [
  { value: "backlog", label: "Backlog", icon: IconCircleDashed },
  { value: "progress", label: "In progress", icon: IconCircleHalf2 },
  { value: "done", label: "Done", icon: IconCircleCheck },
  { value: "canceled", label: "Canceled", icon: IconCircleX },
]

export function SelectIcons() {
  return (
    <Select defaultValue="progress">
      <SelectTrigger aria-label="Status" className="w-44">
        <SelectValue>
          {(value: string) => {
            const status = statuses.find((item) => item.value === value)
            if (!status) {
              return null
            }
            const Icon = status.icon
            return (
              <>
                <Icon />
                {status.label}
              </>
            )
          }}
        </SelectValue>
      </SelectTrigger>
      <SelectContent>
        {statuses.map((status) => (
          <SelectItem key={status.value} value={status.value}>
            <status.icon />
            {status.label}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  )
}

複数

multiple では、選択中もリストが開いたままで、値は長い選択内容を要約できます。

"use client"

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const labels = ["Bug", "Feature", "Design", "Docs", "Performance"]

export function SelectMultiple() {
  return (
    <Select multiple defaultValue={["Bug", "Design"]}>
      <SelectTrigger aria-label="Labels" className="w-56">
        <SelectValue placeholder="Add labels">
          {(value: string[]) =>
            value.length > 2 ? `${value.length} labels` : value.join(", ")
          }
        </SelectValue>
      </SelectTrigger>
      <SelectContent alignItemWithTrigger={false}>
        {labels.map((label) => (
          <SelectItem key={label} value={label}>
            {label}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  )
}

フォーム内

Field の内側では、トリガーにラベル、説明、必須バリデーションが付きます。

"use client"

import { Form } from "@base-ui/react/form"

import { Button } from "@/components/ui/button"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/components/ui/field"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const roles = [
  { value: "viewer", label: "Viewer" },
  { value: "editor", label: "Editor" },
  { value: "admin", label: "Admin" },
]

export function SelectField() {
  return (
    <Form
      className="flex w-full max-w-xs flex-col items-start gap-4"
      onSubmit={(event) => event.preventDefault()}
    >
      <Field name="role">
        <FieldLabel>Role</FieldLabel>
        <Select items={roles} required>
          <SelectTrigger className="w-full">
            <SelectValue placeholder="Choose a role" />
          </SelectTrigger>
          <SelectContent>
            {roles.map((role) => (
              <SelectItem key={role.value} value={role.value}>
                {role.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <FieldDescription>Admins can invite people.</FieldDescription>
        <FieldError match="valueMissing">Choose a role to continue.</FieldError>
      </Field>
      <Button type="submit" size="sm">
        Invite
      </Button>
    </Form>
  )
}

無効と不正

select 全体または個別の option を無効にし、aria-invalid で無効(invalid)な状態にします。

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

export function SelectStates() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Select defaultValue="pro" disabled>
        <SelectTrigger aria-label="Plan" className="w-36">
          <SelectValue />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="pro">Pro</SelectItem>
        </SelectContent>
      </Select>
      <Select defaultValue="weekly">
        <SelectTrigger aria-label="Digest" className="w-36">
          <SelectValue />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="daily">Daily</SelectItem>
          <SelectItem value="weekly">Weekly</SelectItem>
          <SelectItem value="monthly" disabled>
            Monthly (soon)
          </SelectItem>
        </SelectContent>
      </Select>
      <Select>
        <SelectTrigger aria-label="Region" aria-invalid className="w-36">
          <SelectValue placeholder="Region" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="us">United States</SelectItem>
          <SelectItem value="eu">Europe</SelectItem>
        </SelectContent>
      </Select>
    </div>
  )
}

トリガーの下

alignItemWithTrigger={false} は、メニューのようにトリガーの下にリストを開きます。タッチ入力では自動的にこの動作になります。

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

export function SelectDropdown() {
  return (
    <Select defaultValue="newest">
      <SelectTrigger aria-label="Sort by" className="w-44">
        <SelectValue />
      </SelectTrigger>
      <SelectContent alignItemWithTrigger={false}>
        <SelectItem value="newest">Newest first</SelectItem>
        <SelectItem value="oldest">Oldest first</SelectItem>
        <SelectItem value="popular">Most popular</SelectItem>
      </SelectContent>
    </Select>
  )
}

右から左

トリガー、リスト、チェックは文字の読み方向に従います。

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const cities = [
  { value: "cairo", label: "القاهرة" },
  { value: "riyadh", label: "الرياض" },
  { value: "dubai", label: "دبي" },
]

export function SelectRtl() {
  return (
    <div dir="rtl">
      <Select items={cities} defaultValue="riyadh">
        <SelectTrigger aria-label="المدينة" className="w-44">
          <SelectValue />
        </SelectTrigger>
        <SelectContent>
          {cities.map((city) => (
            <SelectItem key={city.value} value={city.value}>
              {city.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  )
}
キーアクション
SpaceEnter↓↑トリガーからリストを開きます。
↓↑option 間を移動します。
HomeEnd最初または最後の option に移動します。
A–Z入力したテキストで始まる次の option にジャンプします。
EnterSpaceハイライトされている option を選びます。
Escリストを閉じ、トリガーにフォーカスを戻します。
  • FieldLabel または aria-label でトリガーにラベルを付けます。
  • タッチ操作では、指が option に触れないよう、リストはトリガーに重ならず下に開きます。
プロパティ型デフォルト
value
Value | Value[] | null–
defaultValue
Value | Value[] | null–
onValueChange
(value, details) => void–
itemsSelectValue がラベルを表示できるようにします。
Record<string, ReactNode> | { value, label }[]–
multiple
booleanfalse
name
string–
required
booleanfalse
disabled
booleanfalse
readOnly
booleanfalse
open
boolean–
onOpenChange
(open, details) => void–
プロパティ型デフォルト
size
"sm" | "default" | "lg""default"
render
ReactElement | (props, state) => ReactElement<button>
属性説明
data-slot="select-trigger"トリガー。data-size を持ちます。
data-popup-openリストが開いている間付きます。
data-placeholder何も選ばれていない間付きます。
プロパティ型デフォルト
placeholder
ReactNode–
children表示される値をフォーマットします。
ReactNode | (value) => ReactNode–
プロパティ型デフォルト
alignItemWithTrigger現在の option を揃えて、トリガーの上に重ねて開きます。
booleantrue
sideトリガーに揃えていないとき。
"top" | "bottom" | …"bottom"
align
"start" | "center" | "end""start"
sideOffset
number6
属性説明
data-slot="select-content"ポップアップ。
data-side="none"トリガーの上に揃えている間付きます。
プロパティ型デフォルト
value
Value–
disabled
booleanfalse
label先頭文字検索用のテキスト。
string–
属性説明
data-selected選択されている option。
data-highlightedフォーカスされている option。
data-disabledその option は無効です。

使用しているブロック

Select の上に構築されるブロック。