Zum Inhalt springen

Element

Eine flexible Zeile mit Feldern für Medien, Titel, Beschreibung und Aktionen für Benachrichtigungsfeeds, Einstellungslisten und Dateizeilen – im Neobrutalismus-Stil.

import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

Das Item ist eine Flex-Zeile mit benannten Slots – Media, Content, Actions – für das „Liste von Dingen“-Muster, bei dem jede App früher oder später landet. Darunter liegt kein Headless-Primitive, nur gestylte Elemente (plus Base UIs useRender für die render-Prop), veredelt nach dem neobrutalistischen Rezept: ein 2px-Rahmen – sichtbar in der outline-Variante – und ein harter, versetzter Fokusring.

Typische Anwendungsfälle:

  • Benachrichtigungs- und Aktivitäts-Feeds – Avatar, Nachricht, Zeitstempel und pro Zeile ein Schließen- oder Aktions-Button.
  • Einstellungs- und Integrationslisten – Icon, Name, Statuszeile, mit einem Switch oder Button am Zeilenende.
  • Dateibrowser und Suchergebnisse – Thumbnail oder Icon plus Metadaten, die ganze Zeile per render ein Link.

Die Komponente Item ist ein unkomplizierter Flex-Container, der nahezu jede Art von Inhalt aufnehmen kann. Verwende sie, um einen Titel, eine Beschreibung und Aktionen anzuzeigen. Gruppiere sie mit der Komponente ItemGroup, um eine Liste von Elementen zu erstellen.

Installation

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

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

components/ui/item.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"

import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"

function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      role="list"
      data-slot="item-group"
      className={cn(
        "group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2",
        className
      )}
      {...props}
    />
  )
}

function ItemSeparator({
  className,
  ...props
}: React.ComponentProps<typeof Separator>) {
  return (
    <Separator
      data-slot="item-separator"
      orientation="horizontal"
      className={cn("my-2", className)}
      {...props}
    />
  )
}

const itemVariants = cva(
  "group/item flex w-full flex-wrap items-center rounded border-2 text-sm transition-colors duration-100 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary [a]:transition-colors [a]:hover:bg-accent",
  {
    variants: {
      variant: {
        default: "border-transparent",
        outline: "border-border",
        muted: "border-transparent bg-muted/50",
      },
      size: {
        default: "gap-2.5 px-3 py-2.5",
        sm: "gap-2.5 px-3 py-2.5",
        xs: "gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

function Item({
  className,
  variant = "default",
  size = "default",
  asChild = false,
  ...props
}: React.ComponentProps<"div"> &
  VariantProps<typeof itemVariants> & { asChild?: boolean }) {
  const Comp = asChild ? Slot.Root : "div"
  return (
    <Comp
      data-slot="item"
      data-variant={variant}
      data-size={size}
      className={cn(itemVariants({ variant, size, className }))}
      {...props}
    />
  )
}

const itemMediaVariants = cva(
  "flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none",
  {
    variants: {
      variant: {
        default: "bg-transparent",
        icon: "[&_svg:not([class*='size-'])]:size-4",
        image:
          "size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

function ItemMedia({
  className,
  variant = "default",
  ...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
  return (
    <div
      data-slot="item-media"
      data-variant={variant}
      className={cn(itemMediaVariants({ variant, className }))}
      {...props}
    />
  )
}

function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="item-content"
      className={cn(
        "flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none",
        className
      )}
      {...props}
    />
  )
}

function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="item-title"
      className={cn(
        "line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4",
        className
      )}
      {...props}
    />
  )
}

function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="item-description"
      className={cn(
        "line-clamp-2 text-left text-sm leading-normal font-normal text-muted-foreground group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
        className
      )}
      {...props}
    />
  )
}

function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="item-actions"
      className={cn("flex items-center gap-2", className)}
      {...props}
    />
  )
}

function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="item-header"
      className={cn(
        "flex basis-full items-center justify-between gap-2",
        className
      )}
      {...props}
    />
  )
}

function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="item-footer"
      className={cn(
        "flex basis-full items-center justify-between gap-2",
        className
      )}
      {...props}
    />
  )
}

export {
  Item,
  ItemMedia,
  ItemContent,
  ItemActions,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
  ItemDescription,
  ItemHeader,
  ItemFooter,
}

Passe die Importpfade an dein Projekt-Setup an.

Verwendung

import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"
<Item>
  <ItemMedia variant="icon">
    <Icon />
  </ItemMedia>
  <ItemContent>
    <ItemTitle>Title</ItemTitle>
    <ItemDescription>Description</ItemDescription>
  </ItemContent>
  <ItemActions>
    <Button>Action</Button>
  </ItemActions>
</Item>

Komposition

Verwende die folgende Komposition, um ein Item aufzubauen:

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

Item vs Field

Verwende Field, wenn du eine Formulareingabe wie eine Checkbox, ein Eingabefeld, ein Radio oder ein Select anzeigen möchtest.

Wenn du nur Inhalte wie einen Titel, eine Beschreibung und Aktionen anzeigen möchtest, verwende Item.

Variant

Verwende die Prop variant, um den visuellen Stil des Elements zu ändern.

import { InboxIcon } from "lucide-react"

import {

Size

Verwende die Prop size, um die Größe des Elements zu ändern. Verfügbare Größen sind default, sm und xs.

import { InboxIcon } from "lucide-react"

import {

Beispiele

Icon

Verwende ItemMedia mit variant="icon", um ein Icon anzuzeigen.

import { ShieldAlertIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

Avatar

Du kannst ItemMedia mit variant="avatar" verwenden, um einen Avatar anzuzeigen.

import { Plus } from "lucide-react"

import {

Bild

Verwende ItemMedia mit variant="image", um ein Bild anzuzeigen.

import Image from "next/image"

import {

Gruppe

Verwende ItemGroup, um zusammengehörige Elemente zu gruppieren.

import * as React from "react"
import { PlusIcon } from "lucide-react"

Kopfzeile

Verwende ItemHeader, um eine Kopfzeile über dem Inhalt des Elements hinzuzufügen.

import Image from "next/image"

import {

Verwende die Prop render, um das Element als Link zu rendern. Die Hover- und Fokus-Zustände werden auf das Anker-Element angewendet.

import { ChevronRightIcon, ExternalLinkIcon } from "lucide-react"

import {
<Item render={<a href="/dashboard" />}>
  <ItemMedia variant="icon">
    <HomeIcon />
  </ItemMedia>
  <ItemContent>
    <ItemTitle>Dashboard</ItemTitle>
    <ItemDescription>Overview of your account and activity.</ItemDescription>
  </ItemContent>
</Item>
"use client"

import { ChevronDownIcon } from "lucide-react"

RTL

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

"use client"

import * as React from "react"

Barrierefreiheit

Das Item ist rein präsentational – gestylte Elemente ohne eigene ARIA-Verdrahtung; ItemGroup rendert allerdings role="list" (ergänze role="listitem" auf jedem Item, wenn die Listenstruktur angekündigt werden soll). Hänge bei interaktiven Zeilen keinen Click-Handler an das div – rendere über die render-Prop ein echtes <a> oder <button>, damit natives Fokus- und Tastaturverhalten greift; der sichtbare Fokusring ist bereits gestylt. Kennzeichne dekorative Icon-Media mit aria-hidden und gib Bild-Media einen aussagekräftigen alt-Text.

API-Referenz

Item

Die Hauptkomponente, um Inhalte mit Medien, Titel, Beschreibung und Aktionen anzuzeigen.

PropTypeDefault
variant"default" | "outline" | "muted""default"
size"default" | "sm" | "xs""default"
renderReact.ReactElement

ItemGroup

Ein Container, der zusammengehörige Elemente mit einheitlichem Styling gruppiert.

<ItemGroup>
  <Item />
  <Item />
</ItemGroup>

ItemSeparator

Ein Trenner zwischen Elementen in einer Gruppe.

<ItemGroup>
  <Item />
  <ItemSeparator />
  <Item />
</ItemGroup>

ItemMedia

Verwende ItemMedia, um Medieninhalte wie Icons, Bilder oder Avatare anzuzeigen.

PropTypeDefault
variant"default" | "icon" | "image""default"
<ItemMedia variant="icon">
  <Icon />
</ItemMedia>
<ItemMedia variant="image">
  <img src="..." alt="..." />
</ItemMedia>

ItemContent

Umschließt den Titel und die Beschreibung des Elements.

<ItemContent>
  <ItemTitle>Title</ItemTitle>
  <ItemDescription>Description</ItemDescription>
</ItemContent>

ItemTitle

Zeigt den Titel des Elements an.

<ItemTitle>Item Title</ItemTitle>

ItemDescription

Zeigt die Beschreibung des Elements an.

<ItemDescription>Item description</ItemDescription>

ItemActions

Container für Aktions-Buttons oder andere interaktive Elemente.

<ItemActions>
  <Button>Action</Button>
</ItemActions>

ItemHeader

Zeigt eine Kopfzeile über dem Inhalt des Elements an.

<Item>
  <ItemHeader>Header</ItemHeader>
  <ItemContent>...</ItemContent>
</Item>

ItemFooter

Zeigt eine Fußzeile unter dem Inhalt des Elements an.

<Item>
  <ItemContent>...</ItemContent>
  <ItemFooter>Footer</ItemFooter>
</Item>