Item
Elastyczny wiersz, który łączy grafikę, tytuł i opis z akcjami — podstawowy element list, ustawień i wierszy zasobów.
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
- 1GrafikaIkona, awatar lub obraz, po którym element rozpoznaje się na pierwszy rzut oka.
- 2KontenerElastyczny wiersz. Warianty dodają obramowanie lub stonowane wypełnienie; rozmiary ustalają gęstość.
- 3TytułNazwa elementu. Długa nazwa zawija się.
- 4OpisSzczegóły uzupełniające, przycięte do dwóch linii.
- 5AkcjePrzyciski lub menu tego elementu, wyrównane do końca wiersza.
Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/item.jsonUż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.
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.
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.
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ą.
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>
)
}Jako link
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>Nagłówek i stopka
ItemHeader i ItemFooter zajmują całą szerokość nad wierszem i pod nim — na postęp, metadane lub akcje drugorzędne.
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
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
ItemGroupzwykł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
ItemMediasą dekoracyjne — znaczenie niesie tytuł. Obrazy, które przekazują informację, potrzebują tekstualtalborole="img"z etykietą.
Dokumentacja API
Item
Zbudowany na useRender z Base UI, więc przyjmuje prop render.
ItemMedia
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.