Zum Inhalt springen

Auswahl

Ein Dropdown-Menü zur Auswahl einer Option – Länderfelder, Sortierreihenfolgen, Design-Einstellungen – mit dicken, im Neobrutalismus-Stil gehaltenen Rahmen und starken Schatten.

import {
  Select,
  SelectContent,

Das Select öffnet eine Popup-Liste mit Optionen und zeigt die gewählte in seinem Trigger an – ein gestalteter, formulartauglicher Ersatz für das native <select>. Es basiert auf der Base UI Select-Komponente und folgt dem neobrutalistischen Rezept: dicke Rahmen, harte Schatten und kräftige Typografie.

Typische Anwendungsfälle:

  • Formularfelder mit festen Auswahlwerten – Land, Währung, Zeitzone; überall dort, wo sonst ein natives <select> stünde.
  • Sortier- und Filtersteuerungen – ein Produktraster oder eine Tabelle nach Preis, Datum oder Beliebtheit ordnen.
  • Einstellungsauswahl – Theme, Sprache oder Benachrichtigungsfrequenz im Einstellungsbereich.

Installation

pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/select.json
npx shadcn@latest add https://neobrutalism.com/r/base/select.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/select.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/select.json

Installiere die folgenden Abhängigkeiten:

pnpm add @base-ui/react
npm install @base-ui/react
yarn add @base-ui/react
bun add @base-ui/react

Kopiere den folgenden Code und füge ihn in dein Projekt ein.

components/ui/select.tsx
"use client"

import * as React from "react"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { Select as SelectPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"

function Select({
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
  return <SelectPrimitive.Root data-slot="select" {...props} />
}

function SelectGroup({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
  return (
    <SelectPrimitive.Group
      data-slot="select-group"
      className={cn("scroll-my-1 p-1", className)}
      {...props}
    />
  )
}

function SelectValue({
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
  return <SelectPrimitive.Value data-slot="select-value" {...props} />
}

function SelectTrigger({
  className,
  size = "default",
  children,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
  size?: "sm" | "default"
}) {
  return (
    <SelectPrimitive.Trigger
      data-slot="select-trigger"
      data-size={size}
      className={cn(
        "flex w-fit items-center justify-between gap-1.5 rounded border-2 bg-input py-2 pr-2 pl-2.5 text-sm whitespace-nowrap shadow-sm transition-colors outline-none select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      {children}
      <SelectPrimitive.Icon asChild>
        <ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
      </SelectPrimitive.Icon>
    </SelectPrimitive.Trigger>
  )
}

function SelectContent({
  className,
  children,
  position = "item-aligned",
  align = "center",
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
  return (
    <SelectPrimitive.Portal>
      <SelectPrimitive.Content
        data-slot="select-content"
        data-align-trigger={position === "item-aligned"}
        className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded border-2 bg-popover text-popover-foreground shadow-md duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
        position={position}
        align={align}
        {...props}
      >
        <SelectScrollUpButton />
        <SelectPrimitive.Viewport
          data-position={position}
          className={cn(
            "data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
            position === "popper" && ""
          )}
        >
          {children}
        </SelectPrimitive.Viewport>
        <SelectScrollDownButton />
      </SelectPrimitive.Content>
    </SelectPrimitive.Portal>
  )
}

function SelectLabel({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
  return (
    <SelectPrimitive.Label
      data-slot="select-label"
      className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
      {...props}
    />
  )
}

function SelectItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
  return (
    <SelectPrimitive.Item
      data-slot="select-item"
      className={cn(
        "relative flex w-full cursor-default items-center gap-1.5 rounded-sm py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
        className
      )}
      {...props}
    >
      <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
        <SelectPrimitive.ItemIndicator>
          <CheckIcon className="pointer-events-none" />
        </SelectPrimitive.ItemIndicator>
      </span>
      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
    </SelectPrimitive.Item>
  )
}

function SelectSeparator({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
  return (
    <SelectPrimitive.Separator
      data-slot="select-separator"
      className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
      {...props}
    />
  )
}

function SelectScrollUpButton({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
  return (
    <SelectPrimitive.ScrollUpButton
      data-slot="select-scroll-up-button"
      className={cn(
        "z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      <ChevronUpIcon />
    </SelectPrimitive.ScrollUpButton>
  )
}

function SelectScrollDownButton({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
  return (
    <SelectPrimitive.ScrollDownButton
      data-slot="select-scroll-down-button"
      className={cn(
        "z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      <ChevronDownIcon />
    </SelectPrimitive.ScrollDownButton>
  )
}

export {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectScrollDownButton,
  SelectScrollUpButton,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
}

Passe die Importpfade an dein Projekt-Setup an.

Verwendung

import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
const items = [
  { label: "Light", value: "light" },
  { label: "Dark", value: "dark" },
  { label: "System", value: "system" },
]
 
<Select items={items}>
  <SelectTrigger className="w-[180px]">
    <SelectValue placeholder="Theme" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      {items.map((item) => (
        <SelectItem key={item.value} value={item.value}>
          {item.label}
        </SelectItem>
      ))}
    </SelectGroup>
  </SelectContent>
</Select>

Komposition

Verwende die folgende Komposition, um ein Select aufzubauen:

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

Beispiele

Element am Trigger ausrichten

Verwende alignItemWithTrigger an SelectContent, um zu steuern, ob das ausgewählte Element am Trigger ausgerichtet wird. Bei true (Standard) wird das Popup so positioniert, dass das ausgewählte Element über dem Trigger erscheint. Bei false wird das Popup an der Kante des Triggers ausgerichtet.

"use client"

import * as React from "react"

Gruppen

Verwende SelectGroup, SelectLabel und SelectSeparator, um Elemente zu ordnen.

import {
  Select,
  SelectContent,

Scrollbar

Ein Select mit vielen Elementen, das scrollbar ist.

import {
  Select,
  SelectContent,

Deaktiviert

import {
  Select,
  SelectContent,

Ungültig

Füge das Attribut data-invalid zur Komponente Field und das Attribut aria-invalid zur Komponente SelectTrigger hinzu, um einen Fehlerzustand anzuzeigen.

<Field data-invalid>
  <FieldLabel>Fruit</FieldLabel>
  <SelectTrigger aria-invalid>
    <SelectValue />
  </SelectTrigger>
</Field>
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import {
  Select,

RTL

Um RTL-Unterstützung in Neobrutalism zu aktivieren, sieh dir die RTL-Konfigurationsanleitung an.

"use client"

import * as React from "react"

Barrierefreiheit

Das Select implementiert das WAI-ARIA-Pattern „Combobox“ in seiner Select-only-Form: Der Trigger wird als eingeklappte Combobox mit aria-expanded angekündigt, das Popup rendert als listbox mit option-Einträgen, und aria-selected verfolgt den aktuellen Wert – alles vom Primitive verdrahtet.

Tastaturinteraktionen:

TasteAktion
Space / EnterÖffnet das Select; wählt bei offenem Popup den hervorgehobenen Eintrag aus
ArrowDown / ArrowUpÖffnet das geschlossene Select; hebt bei offenem Popup den nächsten / vorherigen Eintrag hervor
EscSchließt das Popup und gibt den Fokus an den Trigger zurück
Zeichen tippenTypeahead – springt zum ersten passenden Eintrag

API-Referenz

Sieh dir die Dokumentation von Base UI Select an.