HextaUI

Item

Eine Zeile aus Medien, Text und Aktionen für Listen, Einstellungen und Auswahlen, mit gruppierter Fläche und einer Hover-Hervorhebung, die zwischen den Zeilen gleitet.

import {
  IconBell,
  IconDeviceDesktop,
  IconKey,
  IconUserCircle,
} from "@tabler/icons-react"

import {
  Item,
  ItemChevron,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemTitle,
  type ItemMediaTone,
} from "@/components/ui/item"

const settings: {
  href: string
  icon: typeof IconBell
  title: string
  tone: ItemMediaTone
  description: string
}[] = [
  {
    href: "#profile",
    icon: IconUserCircle,
    title: "Profile",
    tone: "gray",
    description: "Name, photo and handle",
  },
  {
    href: "#notifications",
    icon: IconBell,
    title: "Notifications",
    tone: "red",
    description: "Mentions, replies and digests",
  },
  {
    href: "#security",
    icon: IconKey,
    title: "Password and passkeys",
    tone: "green",
    description: "Two passkeys, last used today",
  },
  {
    href: "#sessions",
    icon: IconDeviceDesktop,
    title: "Sessions",
    tone: "blue",
    description: "Signed in on 3 devices",
  },
]

export function ItemDemo() {
  return (
    <ItemGroup variant="grouped" className="max-w-sm">
      {settings.map((setting) => (
        <Item key={setting.href} size="sm" render={<a href={setting.href} />}>
          <ItemMedia variant="icon" tone={setting.tone}>
            <setting.icon />
          </ItemMedia>
          <ItemContent>
            <ItemTitle>{setting.title}</ItemTitle>
            <ItemDescription>{setting.description}</ItemDescription>
          </ItemContent>
          <ItemChevron />
        </Item>
      ))}
    </ItemGroup>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/item.json

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

import {
  Item,
  ItemActions,
  ItemChevron,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"
<ItemGroup variant="grouped">
  <Item render={<a href="/settings/profile" />}>
    <ItemMedia variant="icon">
      <IconUserCircle />
    </ItemMedia>
    <ItemContent>
      <ItemTitle>Profile</ItemTitle>
      <ItemDescription>Name, photo and handle</ItemDescription>
    </ItemContent>
    <ItemChevron />
  </Item>
</ItemGroup>

Gib Icon-Media einen tone für farbige Kacheln wie in den iOS-Einstellungen oder lass ihn weg für eine neutrale Kachel. Ein Eintrag wird interaktiv, wenn er über render als Link, Button oder Label gerendert wird. Nur dann erhält er Hover-, Press- und Fokus-Stile, sodass statische Zeilen nie vorgeben, klickbar zu sein.

ItemGroup
├── Item
│   ├── ItemHeader
│   ├── ItemMedia
│   ├── ItemContent
│   │   ├── ItemTitle
│   │   └── ItemDescription
│   ├── ItemActions
│   ├── ItemChevron
│   └── ItemFooter
└── ItemSeparator

Gruppiert

variant="grouped" legt Items auf eine gemeinsame Fläche. Trenner werden automatisch gezeichnet und zum Text eingerückt, die erste und letzte Zeile besitzen die äußeren Ecken, und die Trenner neben einer gehoverten oder fokussierten Zeile blenden aus.

"use client"

import * as React from "react"
import { IconCheck } from "@tabler/icons-react"

import {
  Item,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemTitle,
} from "@/components/ui/item"

const themes = [
  { id: "system", title: "System", description: "Follow your device" },
  { id: "light", title: "Light", description: "Always light" },
  { id: "dark", title: "Dark", description: "Always dark" },
]

export function ItemPressed() {
  const [theme, setTheme] = React.useState("system")

  return (
    <ItemGroup variant="grouped" className="max-w-sm">
      {themes.map((option) => (
        <Item
          key={option.id}
          size="sm"
          render={<button type="button" />}
          aria-pressed={theme === option.id}
          onClick={() => setTheme(option.id)}
        >
          <ItemContent>
            <ItemTitle>{option.title}</ItemTitle>
            <ItemDescription>{option.description}</ItemDescription>
          </ItemContent>
          {theme === option.id && (
            <IconCheck className="size-4 text-foreground" aria-hidden="true" />
          )}
        </Item>
      ))}
    </ItemGroup>
  )
}

Hover-Hervorhebung

Interaktive Einträge in einer Gruppe teilen sich eine Hervorhebung, die zur Zeile unter dem Zeiger gleitet und deren Ecken übernimmt. Sie folgt nur einer Maus oder einem Stift, nie Touch, und lässt sich mit highlight={false} abschalten. Die Ecken von Bild-Media sind konzentrisch zur Zeile.

import {
  Item,
  ItemChevron,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"

const albums = [
  {
    title: "Night Drive",
    artist: "Lune Avenue",
    src: "https://images.unsplash.com/photo-1514525253161-7a46d19cd819?w=120&h=120&fit=crop",
  },
  {
    title: "Paper Gardens",
    artist: "Mara Lin",
    src: "https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=120&h=120&fit=crop",
  },
  {
    title: "Low Tide",
    artist: "The Harbour",
    src: "https://images.unsplash.com/photo-1511379938547-c1f69419868d?w=120&h=120&fit=crop",
  },
]

export function ItemImage() {
  return (
    <ItemGroup className="max-w-sm">
      {albums.map((album) => (
        <Item key={album.title} size="sm" render={<a href="#" />}>
          <ItemMedia variant="image">
            <img src={album.src} alt="" width={40} height={40} />
          </ItemMedia>
          <ItemContent>
            <ItemTitle>{album.title}</ItemTitle>
            <ItemDescription>{album.artist}</ItemDescription>
          </ItemContent>
          <ItemChevron />
        </Item>
      ))}
    </ItemGroup>
  )
}

Auswählbar

Rendere ein Item als <label> um eine Checkbox, und die ganze Zeile schaltet sie um. Ein aktiviertes Steuerelement oder aria-selected, aria-checked bzw. aria-pressed am Item markiert die Zeile als ausgewählt.

import { Checkbox } from "@/components/ui/checkbox"
import {
  Item,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemTitle,
} from "@/components/ui/item"

const plans = [
  {
    id: "analytics",
    title: "Analytics",
    description: "Page views and funnels",
  },
  { id: "backups", title: "Daily backups", description: "Kept for 30 days" },
  { id: "sso", title: "Single sign-on", description: "SAML and OIDC" },
]

export function ItemSelectable() {
  return (
    <ItemGroup className="max-w-sm">
      {plans.map((plan, index) => (
        <Item key={plan.id} variant="outline" size="sm" render={<label />}>
          <Checkbox
            name="addons"
            value={plan.id}
            defaultChecked={index === 0}
          />
          <ItemContent>
            <ItemTitle>{plan.title}</ItemTitle>
            <ItemDescription>{plan.description}</ItemDescription>
          </ItemContent>
        </Item>
      ))}
    </ItemGroup>
  )
}

Varianten

default hat keine Fläche, outline zeichnet eine feine Linie und muted liegt auf einer weichen Füllung.

import { IconFileText } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"

const variants = ["default", "outline", "muted"] as const

export function ItemVariants() {
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      {variants.map((variant) => (
        <Item key={variant} variant={variant}>
          <ItemMedia variant="icon">
            <IconFileText />
          </ItemMedia>
          <ItemContent>
            <ItemTitle>Quarterly report.pdf</ItemTitle>
            <ItemDescription>2.4 MB · {variant}</ItemDescription>
          </ItemContent>
          <ItemActions>
            <Button variant="outline" size="sm">
              Open
            </Button>
          </ItemActions>
        </Item>
      ))}
    </div>
  )
}

Größen

size skaliert Padding, Abstand, Eckenradius und Media gemeinsam. Eine Gruppe verengt ihren Abstand entsprechend.

import { IconMusic } from "@tabler/icons-react"

import {
  Item,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"

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

export function ItemSizes() {
  return (
    <ItemGroup className="max-w-sm">
      {sizes.map((size) => (
        <Item key={size} size={size} variant="outline">
          <ItemMedia variant="icon">
            <IconMusic />
          </ItemMedia>
          <ItemContent>
            <ItemTitle>Size {size}</ItemTitle>
            <ItemDescription>
              Padding, gap and media scale together.
            </ItemDescription>
          </ItemContent>
        </Item>
      ))}
    </ItemGroup>
  )
}

Avatar

Das Standard-<ItemMedia /> zentriert nur seinen Inhalt, sodass ein Avatar oder eine AvatarGroup direkt hineinpasst.

import { IconPlus } from "@tabler/icons-react"

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
} from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"

const team = [
  { handle: "preetsuthar17", initials: "PS" },
  { handle: "emilkowalski", initials: "EK" },
  { handle: "rauchg", initials: "GR" },
]

export function ItemAvatar() {
  return (
    <div className="flex w-full max-w-lg flex-col gap-6">
      <Item variant="outline">
        <ItemMedia>
          <Avatar size="lg">
            <AvatarImage src="https://github.com/preetsuthar17.png" alt="" />
            <AvatarFallback>PS</AvatarFallback>
          </Avatar>
        </ItemMedia>
        <ItemContent>
          <ItemTitle>Preet Suthar</ItemTitle>
          <ItemDescription>Maintainer of HextaUI</ItemDescription>
        </ItemContent>
        <ItemActions>
          <Button
            variant="outline"
            size="icon-sm"

            aria-label="Invite Preet Suthar"
          >
            <IconPlus />
          </Button>
        </ItemActions>
      </Item>
      <Item variant="outline">
        <ItemMedia>
          <AvatarGroup>
            {team.map((person) => (
              <Avatar key={person.handle}>
                <AvatarImage
                  src={`https://github.com/${person.handle}.png`}
                  alt=""
                />
                <AvatarFallback>{person.initials}</AvatarFallback>
              </Avatar>
            ))}
          </AvatarGroup>
        </ItemMedia>
        <ItemContent>
          <ItemTitle>No team members</ItemTitle>
          <ItemDescription>Invite your team to collaborate.</ItemDescription>
        </ItemContent>
        <ItemActions>
          <Button size="sm" variant="outline">
            Invite
          </Button>
        </ItemActions>
      </Item>
    </div>
  )
}

Personen

Eine gruppierte Liste von Personen. Trennlinien beginnen dort, wo der Text beginnt, egal was im Media-Slot sitzt, sodass sie unter den Namen fluchten.

import { IconPlus } from "@tabler/icons-react"

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"

const people = [
  { handle: "preetsuthar17", name: "Preet Suthar" },
  { handle: "emilkowalski", name: "Emil Kowalski" },
  { handle: "rauchg", name: "Guillermo Rauch" },
  { handle: "leerob", name: "Lee Robinson" },
]

export function ItemPeople() {
  return (
    <ItemGroup variant="grouped" className="max-w-sm">
      {people.map((person) => (
        <Item key={person.handle} size="sm">
          <ItemMedia>
            <Avatar size="lg">
              <AvatarImage
                src={`https://github.com/${person.handle}.png`}
                alt=""
              />
              <AvatarFallback>
                {person.name
                  .split(" ")
                  .map((part) => part[0])
                  .join("")}
              </AvatarFallback>
            </Avatar>
          </ItemMedia>
          <ItemContent>
            <ItemTitle>{person.name}</ItemTitle>
            <ItemDescription>@{person.handle}</ItemDescription>
          </ItemContent>
          <ItemActions>
            <Button
              variant="ghost"
              size="icon-sm"

              aria-label={`Invite ${person.name}`}
            >
              <IconPlus />
            </Button>
          </ItemActions>
        </Item>
      ))}
    </ItemGroup>
  )
}

Aktionen

<ItemActions /> hält Buttons am Ende der Zeile. Halte den Eintrag selbst statisch, wenn er Buttons enthält, damit nie ein Button in einem Link liegt.

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"

const invites = [
  { initials: "MO", name: "Mira Okafor", email: "[email protected]" },
  { initials: "JP", name: "Jun Park", email: "[email protected]" },
]

export function ItemActionsDemo() {
  return (
    <ItemGroup variant="grouped" className="max-w-md">
      {invites.map((invite) => (
        <Item key={invite.email} size="sm">
          <ItemMedia>
            <Avatar>
              <AvatarFallback>{invite.initials}</AvatarFallback>
            </Avatar>
          </ItemMedia>
          <ItemContent>
            <ItemTitle>{invite.name}</ItemTitle>
            <ItemDescription>{invite.email}</ItemDescription>
          </ItemContent>
          <ItemActions>
            <Button variant="ghost" size="sm">
              Decline
            </Button>
            <Button variant="outline" size="sm">
              Accept
            </Button>
          </ItemActions>
        </Item>
      ))}
    </ItemGroup>
  )
}

<ItemHeader /> und <ItemFooter /> nehmen über und unter dem Inhalt eine volle Zeile ein.

import { Badge } from "@/components/ui/badge"
import {
  Item,
  ItemContent,
  ItemDescription,
  ItemFooter,
  ItemHeader,
  ItemTitle,
} from "@/components/ui/item"

export function ItemHeaderFooter() {
  return (
    <Item variant="outline" className="max-w-sm" render={<a href="#" />}>
      <ItemHeader>
        <span className="text-xs text-muted-foreground">hextaui/hextaui</span>
        <Badge>Open</Badge>
      </ItemHeader>
      <ItemContent>
        <ItemTitle>Rework the item component</ItemTitle>
        <ItemDescription>
          Grouped surfaces, a gliding hover highlight and selectable rows.
        </ItemDescription>
      </ItemContent>
      <ItemFooter>
        <span className="text-xs text-muted-foreground">
          #482 · 3 reviewers
        </span>
        <span className="text-xs text-muted-foreground">2h ago</span>
      </ItemFooter>
    </Item>
  )
}

Separator

Setze in einer Standardgruppe selbst einen <ItemSeparator /> zwischen die Einträge.

import {
  Item,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"

export function ItemSeparatorDemo() {
  return (
    <ItemGroup className="max-w-sm">
      <Item size="sm">
        <ItemContent>
          <ItemTitle>Storage</ItemTitle>
          <ItemDescription>18.2 GB of 50 GB used</ItemDescription>
        </ItemContent>
      </Item>
      <ItemSeparator />
      <Item size="sm">
        <ItemContent>
          <ItemTitle>Bandwidth</ItemTitle>
          <ItemDescription>312 GB this month</ItemDescription>
        </ItemContent>
      </Item>
    </ItemGroup>
  )
}

Langer Inhalt

Titel werden auf eine Zeile und Beschreibungen auf zwei Zeilen gekürzt. Ununterbrochene Zeichenketten umbrechen, statt die Zeile zu verbreitern.

import { IconFile } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"

export function ItemLongContent() {
  return (
    <Item variant="outline" className="max-w-sm">
      <ItemMedia variant="icon">
        <IconFile />
      </ItemMedia>
      <ItemContent>
        <ItemTitle>
          final-final-v3-approved-by-legal-and-design-do-not-edit-this-copy.pdf
        </ItemTitle>
        <ItemDescription>
          Uploaded to /projects/2026/client-work/very-long-folder-names/archive
          by [email protected] and shared with
          the whole organisation.
        </ItemDescription>
      </ItemContent>
      <ItemActions>
        <Button variant="outline" size="sm">
          Share
        </Button>
      </ItemActions>
    </Item>
  )
}

Rechts nach links

Padding, Trenner-Einzüge und das Chevron folgen der Leserichtung.

import { IconBell, IconUserCircle } from "@tabler/icons-react"

import {
  Item,
  ItemChevron,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"

export function ItemRtl() {
  return (
    <div dir="rtl" className="w-full max-w-sm">
      <ItemGroup variant="grouped">
        <Item size="sm" render={<a href="#" />}>
          <ItemMedia variant="icon">
            <IconUserCircle />
          </ItemMedia>
          <ItemContent>
            <ItemTitle>الملف الشخصي</ItemTitle>
            <ItemDescription>الاسم والصورة</ItemDescription>
          </ItemContent>
          <ItemChevron />
        </Item>
        <Item size="sm" render={<a href="#" />}>
          <ItemMedia variant="icon">
            <IconBell />
          </ItemMedia>
          <ItemContent>
            <ItemTitle>الإشعارات</ItemTitle>
            <ItemDescription>الإشارات والردود</ItemDescription>
          </ItemContent>
          <ItemChevron />
        </Item>
      </ItemGroup>
    </div>
  )
}
TasteAktion
TabWechselt zum nächsten interaktiven Eintrag, in Quellreihenfolge wie jeder Link oder Button.
EnterFolgt einem Link-Eintrag oder drückt einen Button-Eintrag.
SpaceBetätigt ein Button-Item oder schaltet ein Label-Item um.
  • Eine Gruppe ist eine Liste, wenn ihre Einträge einfache Zeilen sind. Einträge, die als Links oder Buttons gerendert werden, behalten ihre eigene Rolle, und die Gruppe verwirft die Listenrolle, damit das Markup gültig bleibt. Rendere die Gruppe als <ul> und die Einträge als <li>, die Links umschließen, wenn du beides möchtest.
  • <ItemMedia variant="icon" /> und der Chevron sind für Screenreader verborgen. Gib Bildern echten Alt-Text, wenn sie Bedeutung tragen.
  • Das Highlight ist reine Dekoration. Der Tastaturfokus zeigt den Fokusring, und das Highlight verschiebt weder den Fokus noch ändert es die Tab-Reihenfolge.

Jeder Teil rendert standardmäßig ein <div> und akzeptiert render sowie die Attribute seines Elements.

PropTypStandard
variant
"default" | "outline" | "muted""default"
size
"default" | "sm" | "xs""default"
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="item"Items in CSS ansprechen.
data-variantDie aktuelle Variante.
data-sizeDie aktuelle Größe.
data-interactiveVorhanden, wenn das Item als Link, Button oder Label gerendert wird.
data-highlightedVorhanden, solange das Gruppen-Highlight darauf liegt.
--item-radiusEckenradius. Die Media-Ecken leiten sich davon ab. Wird durch size gesetzt.
--item-px / --item-py / --item-gapPadding und Abstand. Wird durch size gesetzt.
--item-media-sizeGröße von Icon- und Bild-Media. Wird über size gesetzt.
--item-insetWo ein gruppierter Trenner beginnt. Wird gemessen, damit er unabhängig vom Media mit ItemContent fluchtet.
PropTypStandard
variantgrouped legt Items mit automatischen Trennern auf eine gemeinsame Fläche.
"default" | "grouped""default"
highlightZeige ein einzelnes Highlight, das beim Hover zwischen interaktiven Items gleitet.
booleantrue
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="item-group"Gruppen in CSS ansprechen.
data-variantDie aktuelle Variante.
data-highlightVorhanden, wenn das Hover-Highlight aktiv ist.
--item-group-radiusEckenradius einer gruppierten Fläche. Der erste und der letzte Eintrag folgen ihm.
data-slot="item-highlight"Das Highlight-Element, mit data-visible und data-pressed.
PropTypStandard
varianticon setzt das Icon auf eine Kachel und blendet es für Screenreader aus. image schneidet quadratisch zu. Beide haben Ecken, die konzentrisch zum Item verlaufen.
"default" | "icon" | "image""default"
toneMit der Icon-Variante füllt es die Kachel mit einer Volltonfarbe und färbt das Icon weiß, wie in den iOS-Einstellungen. Ohne Angabe bleibt die Kachel neutral.
"gray" | "red" | "orange" | "yellow" | "green" | "teal" | "sky" | "blue" | "indigo" | "purple" | "pink"–
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="item-media"Media per CSS ansprechen.
data-variantDie aktuelle Variante.
data-toneDie Farbe der Kachel, wenn tone bei einem Icon gesetzt ist.
--item-media-toneDie Farbe der Kachel. Setze eine beliebige Farbe für einen eigenen Ton.
AttributBeschreibung
data-slot="item-content"Wächst, um die Zeile zu füllen. Ein zweiter Inhaltsblock schmiegt sich an seinen Text.
AttributBeschreibung
data-slot="item-title"Auf eine Zeile begrenzt.
PropTypStandard
render
ReactElement | (props, state) => ReactElement<p>
AttributBeschreibung
data-slot="item-description"Auf zwei Zeilen begrenzt.
AttributBeschreibung
data-slot="item-actions"Die Aktionen in CSS ansprechen.
AttributBeschreibung
data-slot="item-header"Eine Zeile in voller Breite über dem Inhalt.
data-slot="item-footer"Eine Zeile in voller Breite unter dem Inhalt.

Ein nachgestellter Chevron, der sich beim Hovern seines Eintrags in Leserichtung schiebt. Akzeptiert jede Tabler-Icon-Prop.

AttributBeschreibung
data-slot="item-chevron"Das Chevron per CSS ansprechen.
AttributBeschreibung
data-slot="item-separator"Eine Haarlinie mit role="separator".