Przejdź do treści

Item

Elastyczny wiersz, który łączy grafikę, tytuł i opis z akcjami — podstawowy element list, ustawień i wierszy zasobów.

Kod źródłowyRejestr
Połącz repozytorium
import { GitBranchIcon } from "lucide-react"

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

export default function ItemDemo() {
  return (
    <Item variant="outline" className="max-w-sm">
      <ItemMedia variant="icon">
        <GitBranchIcon />
      </ItemMedia>
      <ItemContent>
        <ItemTitle>Połącz repozytorium</ItemTitle>
        <ItemDescription>Wdrażaj po każdym wypchnięciu do gałęzi main.</ItemDescription>
      </ItemContent>
      <ItemActions>
        <Button size="sm" variant="outline">
          Połącz
        </Button>
      </ItemActions>
    </Item>
  )
}

Anatomia

Skrót tygodnia
  1. 1GrafikaIkona, awatar lub obraz, po którym element rozpoznaje się na pierwszy rzut oka.
  2. 2KontenerElastyczny wiersz. Warianty dodają obramowanie lub stonowane wypełnienie; rozmiary ustalają gęstość.
  3. 3TytułNazwa elementu. Długa nazwa zawija się.
  4. 4OpisSzczegóły uzupełniające, przycięte do dwóch linii.
  5. 5AkcjePrzyciski lub menu tego elementu, wyrównane do końca wiersza.

Instalacja

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/item.json

Użycie

import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"
<Item variant="outline">
  <ItemMedia variant="icon">
    <GitBranchIcon />
  </ItemMedia>
  <ItemContent>
    <ItemTitle>Connect a repository</ItemTitle>
    <ItemDescription>Deploy on every push to main.</ItemDescription>
  </ItemContent>
  <ItemActions>
    <Button size="sm" variant="outline">Connect</Button>
  </ItemActions>
</Item>

Przykłady

Warianty

default wtapia się w powierzchnię, na której leży, outline to wyniesiony kafelek, a muted — wpuszczona w tło wnęka.

default
outline
muted
import { ShieldCheckIcon } from "lucide-react"

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

const variants = [
  {
    variant: "default",
    note: "Wtapia się w powierzchnię. Do list w kartach i menu.",
  },
  { variant: "outline", note: "Wyniesiony kafelek do samodzielnego użycia na stronie." },
  {
    variant: "muted",
    note: "Wklęsłe pole na treści drugorzędne lub pogrupowane.",
  },
] as const

export default function ItemVariants() {
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      {variants.map(({ variant, note }) => (
        <Item key={variant} variant={variant}>
          <ItemMedia variant="icon">
            <ShieldCheckIcon />
          </ItemMedia>
          <ItemContent>
            <ItemTitle className="capitalize">{variant}</ItemTitle>
            <ItemDescription>{note}</ItemDescription>
          </ItemContent>
          <ItemActions>
            <Button size="sm" variant="ghost">
              Sprawdź
            </Button>
          </ItemActions>
        </Item>
      ))}
    </div>
  )
}

Rozmiary

default, sm i xs skalują jednocześnie odstępy wewnętrzne, odstępy między częściami i grafikę. Używaj sm w listach wewnątrz kart, a xs w menu.

Quarterly report.pdf
default
Quarterly report.pdf
sm
Quarterly report.pdf
xs
import { FileTextIcon } from "lucide-react"

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

export default function ItemSizes() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      {(["default", "sm", "xs"] as const).map((size) => (
        <Item key={size} variant="outline" size={size}>
          <ItemMedia variant="icon">
            <FileTextIcon />
          </ItemMedia>
          <ItemContent>
            <ItemTitle>Quarterly report.pdf</ItemTitle>
            <ItemDescription>
              {size === "xs" ? "2,4 MB" : "2,4 MB · Zaktualizowano 3 godz. temu"}
            </ItemDescription>
          </ItemContent>
          <span className="ml-auto font-mono text-xs text-muted-foreground">
            {size}
          </span>
        </Item>
      ))}
    </div>
  )
}

Grafika

ItemMedia mieści kafelek z ikoną (variant="icon"), obraz (variant="image") albo cokolwiek innego — Avatar, oznaczenie typu pliku. Gdy element ma opis, grafika wyrównuje się do góry.

Ikona
Blue Hour
AK
Aiko Kimura
import { MusicIcon } from "lucide-react"

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

export default function ItemMediaExample() {
  return (
    <ItemGroup className="max-w-sm gap-2">
      <Item variant="outline" size="sm">
        <ItemMedia variant="icon">
          <MusicIcon />
        </ItemMedia>
        <ItemContent>
          <ItemTitle>Ikona</ItemTitle>
          <ItemDescription>Dla obiektów i akcji.</ItemDescription>
        </ItemContent>
      </Item>
      <Item variant="outline" size="sm">
        <ItemMedia variant="image">
          <div
            role="img"
            aria-label="Okładka albumu Blue Hour"
            className="size-full bg-brand-9"
          />
        </ItemMedia>
        <ItemContent>
          <ItemTitle>Blue Hour</ItemTitle>
          <ItemDescription>
            Obraz — okładki, miniatury, podglądy.
          </ItemDescription>
        </ItemContent>
      </Item>
      <Item variant="outline" size="sm">
        <ItemMedia>
          <Avatar>
            <AvatarFallback>AK</AvatarFallback>
          </Avatar>
        </ItemMedia>
        <ItemContent>
          <ItemTitle>Aiko Kimura</ItemTitle>
          <ItemDescription>Awatar — osoby i zespoły.</ItemDescription>
        </ItemContent>
      </Item>
    </ItemGroup>
  )
}

Lista

ItemGroup zamienia elementy w listę: grupa dostaje role="list", a każdy zwykły element automatycznie staje się elementem listy. ItemSeparator rysuje cienkie linie między wierszami, a czytniki ekranu go pomijają.

MC
Maya Chen
Właściciel
JO
Jonah Okafor
Administrator
LP
Lena Park
Członek
TR
Tomás Rivera
Zaproszony
import * as React from "react"
import { MoreHorizontalIcon } from "lucide-react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"

const people = [
  { name: "Maya Chen", email: "maya@northwind.dev", role: "Właściciel" },
  { name: "Jonah Okafor", email: "jonah@northwind.dev", role: "Administrator" },
  { name: "Lena Park", email: "lena@northwind.dev", role: "Członek" },
  { name: "Tomás Rivera", email: "tomas@northwind.dev", role: "Zaproszony" },
]

export default function ItemList() {
  return (
    <div className="w-full max-w-md rounded-xl border bg-card p-1 shadow-raised">
      <ItemGroup aria-label="Członkowie przestrzeni roboczej" className="gap-0">
        {people.map((person, index) => (
          <React.Fragment key={person.email}>
            {index > 0 && <ItemSeparator className="my-0" />}
            <Item size="sm">
              <ItemMedia>
                <Avatar>
                  <AvatarFallback>
                    {person.name
                      .split(" ")
                      .map((part) => part[0])
                      .join("")}
                  </AvatarFallback>
                </Avatar>
              </ItemMedia>
              {/* min-w-0 lets long emails truncate instead of pushing the actions to a new line. */}
              <ItemContent className="min-w-0 gap-0.5">
                <ItemTitle>{person.name}</ItemTitle>
                <ItemDescription className="truncate">
                  {person.email}
                </ItemDescription>
              </ItemContent>
              <ItemActions>
                {person.role === "Zaproszony" ? (
                  <Badge variant="outline">Zaproszony</Badge>
                ) : (
                  <span className="text-xs text-muted-foreground">
                    {person.role}
                  </span>
                )}
                <DropdownMenu>
                  <DropdownMenuTrigger
                    render={
                      <Button
                        variant="ghost"
                        size="icon-sm"
                        aria-label={`Zarządzaj: ${person.name}`}
                      />
                    }
                  >
                    <MoreHorizontalIcon />
                  </DropdownMenuTrigger>
                  <DropdownMenuContent align="end" className="w-44">
                    <DropdownMenuGroup>
                      <DropdownMenuItem>Zmień rolę</DropdownMenuItem>
                      <DropdownMenuItem>Pokaż aktywność</DropdownMenuItem>
                    </DropdownMenuGroup>
                    <DropdownMenuSeparator />
                    <DropdownMenuGroup>
                      <DropdownMenuItem variant="destructive">
                        Usuń
                      </DropdownMenuItem>
                    </DropdownMenuGroup>
                  </DropdownMenuContent>
                </DropdownMenu>
              </ItemActions>
            </Item>
          </React.Fragment>
        ))}
      </ItemGroup>
    </div>
  )
}

Gdy cały wiersz prowadzi w inne miejsce, wyrenderuj element jako link przez render. Style najechania i fokusu działają automatycznie. Zestaw linków owiń w <nav> z etykietą, a nie w ItemGroup — link nie może być jednocześnie elementem listy.

import {
  BookOpenIcon,
  ChevronRightIcon,
  HistoryIcon,
  LayoutTemplateIcon,
} from "lucide-react"
import Link from "next/link"

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

const resources = [
  {
    title: "Dokumentacja",
    description: "Przewodniki, dokumentacja API i gotowe przepisy.",
    href: "/pl/docs",
    icon: BookOpenIcon,
  },
  {
    title: "Przykłady",
    description: "Kompletne ekrany zbudowane wyłącznie z prfct.",
    href: "/pl/examples",
    icon: LayoutTemplateIcon,
  },
  {
    title: "Lista zmian",
    description: "Co się zmieniło w każdej wersji.",
    href: "/pl/docs/resources/changelog",
    icon: HistoryIcon,
  },
]

export default function ItemLink() {
  return (
    <nav aria-label="Zasoby" className="flex w-full max-w-md flex-col gap-2">
      {resources.map(({ title, description, href, icon: Icon }) => (
        <Item key={title} variant="outline" render={<Link href={href} />}>
          <ItemMedia variant="icon">
            <Icon />
          </ItemMedia>
          <ItemContent>
            <ItemTitle>{title}</ItemTitle>
            <ItemDescription>{description}</ItemDescription>
          </ItemContent>
          <ItemActions>
            <ChevronRightIcon
              className="size-4 text-muted-foreground"
              aria-hidden="true"
            />
          </ItemActions>
        </Item>
      ))}
    </nav>
  )
}
<Item variant="outline" render={<a href="/docs" />}>
  …
</Item>

ItemHeader i ItemFooter zajmują całą szerokość nad wierszem i pod nim — na postęp, metadane lub akcje drugorzędne.

Przesyłanie 1 z 368%
design-tokens-v4.zip
x
import { FileArchiveIcon, XIcon } from "lucide-react"

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

export default function ItemHeaderFooter() {
  return (
    <Item variant="outline" className="max-w-sm">
      <ItemHeader>
        <span className="text-xs font-medium text-muted-foreground">
          Przesyłanie 1 z 3
        </span>
        <span className="text-xs text-muted-foreground tabular-nums">68%</span>
      </ItemHeader>
      <ItemMedia variant="icon">
        <FileArchiveIcon />
      </ItemMedia>
      <ItemContent>
        <ItemTitle>design-tokens-v4.zip</ItemTitle>
        <ItemDescription>12,8 MB z 18,9 MB · zostało 6 s</ItemDescription>
      </ItemContent>
      <ItemActions>
        <Button size="icon-sm" variant="ghost" aria-label="Anuluj przesyłanie">
          <XIcon />
        </Button>
      </ItemActions>
      <ItemFooter>
        <Progress
          value={68}
          aria-label="Postęp przesyłania"
          className="w-full"
          size="sm"
        />
      </ItemFooter>
    </Item>
  )
}

Wytyczne

Kiedy używać

  • W wierszach, które opisują jeden obiekt z opcjonalną akcją: członka zespołu, plik, integrację, ustawienie.
  • W krótkich listach, w których każdy wiersz łączy grafikę, tekst i kontrolki.

Kiedy nie używać

  • Przy wielu wierszach porównywanych w tych samych kolumnach — użyj komponentu Table.
  • Przy pojedynczym, rozbudowanym obiekcie — wybierz Card.
  • W menu nawigacyjnych z samymi ikonami — sięgnij po Sidebar lub Dropdown Menu.

Jedna główna akcja na wiersz

Slack
DobrzeJedna widoczna akcja; pozostałe trafiają do menu „Więcej”.
Slack
ŹleRząd równorzędnych przycisków konkuruje z treścią, którą opisuje wiersz.

Treść

  • Tytuły mieść w jednej linii, a opisy w dwóch. Dłuższy tytuł zawinie się do kolejnej linii, a dłuższy opis zostanie przycięty po dwóch liniach.
  • W tytule umieść informację, która najlepiej odróżnia element od innych. Na liście osób to imię i nazwisko, na liście plików — nazwa pliku.

Dostępność

  • Semantyka listy jest automatyczna. Wewnątrz ItemGroup zwykłe elementy są udostępniane jako elementy listy. Elementy wyrenderowane jako coś innego (linki, przyciski) zachowują własne role.
  • Link jest odczytywany jako całość. Dostępną nazwą elementu-linku jest cała jego treść tekstowa. Pisz zwięzłe tytuły i opisy, żeby ta nazwa pozostała użyteczna.
  • Nazywaj akcje z samą ikoną. Przyciski menu „Więcej” potrzebują etykiety, która wskazuje wiersz: „Zarządzaj: Maya Chen”.
  • Grafika dekoracyjna. Ikony w ItemMedia są dekoracyjne — znaczenie niesie tytuł. Obrazy, które przekazują informację, potrzebują tekstu alt albo role="img" z etykietą.

Dokumentacja API

Item

Zbudowany na useRender z Base UI, więc przyjmuje prop render.

PropTypDomyślnie
variant

Przezroczysty, wyniesiony kafelek lub wpuszczona wnęka.

"default" | "outline" | "muted""default"
size

Odstępy wewnętrzne, odstępy między częściami i rozmiar grafiki.

"default" | "sm" | "xs""default"
render

Renderuje inny element — zwykle link — ze stylami Item.

ReactElement | (props, state) => ReactElementBrak wartości domyślnej

ItemMedia

PropTypDomyślnie
variant

Kafelek z ikoną, przycięty obraz lub slot bez stylów na własną grafikę.

"default" | "icon" | "image""default"

ItemGroup

<div role="list">, który układa elementy jeden pod drugim i informuje je, że są elementami listy. Przy mniejszych elementach odstępy się zmniejszają.

ItemContent, ItemTitle, ItemDescription

Kolumna tekstu. Tytuł dzieli linię z ikonami i odznakami, a gdy jest za długi, zawija się; opisy są przycinane do dwóch linii.

ItemActions

Kontrolki na końcu wiersza.

ItemHeader, ItemFooter

Wiersze na całą szerokość nad głównym wierszem i pod nim.

ItemSeparator

Pozioma linia między elementami; wewnątrz grupy ukryta przed technologiami wspomagającymi.