Display
Item.
A flexible row for lists of things: media, title, description and actions, with outline and muted variants.
Deepstone
deepstone.hub:25565
Skylands
skylands.hub:25565
Turtle Farm
turtle.farm:25565
import { Power, RotateCw, Server } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemSeparator,
ItemTitle,
} from "@/components/ui/item";
import { StatusDot } from "@/components/ui/status-dot";
const servers = [
{ name: "Deepstone", host: "deepstone.hub:25565", players: "12 / 40", tone: "online" as const },
{ name: "Skylands", host: "skylands.hub:25565", players: "3 / 20", tone: "busy" as const },
{ name: "Turtle Farm", host: "turtle.farm:25565", players: "Offline", tone: "offline" as const },
];
export default function ItemDemo() {
return (
<ItemGroup className="w-full max-w-lg rounded-lg border bg-card">
{servers.map((server, index) => (
<div key={server.name} role="listitem">
{index > 0 && <ItemSeparator />}
<Item>
<ItemMedia variant="icon">
<Server />
</ItemMedia>
<ItemContent>
<ItemTitle>
{server.name}
<StatusDot tone={server.tone} pulse={server.tone !== "offline"} />
</ItemTitle>
<ItemDescription className="font-mono text-xs">{server.host}</ItemDescription>
</ItemContent>
<ItemActions>
<Badge variant="outline" className="hidden sm:inline-flex">
{server.players}
</Badge>
{server.tone === "offline" ? (
<Button size="icon-sm" variant="outline" aria-label={`Start ${server.name}`}>
<Power />
</Button>
) : (
<Button size="icon-sm" variant="ghost" aria-label={`Restart ${server.name}`}>
<RotateCw />
</Button>
)}
</ItemActions>
</Item>
</div>
))}
</ItemGroup>
);
}Installation#
With the CLI
$npx lanterncn add itemThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/item. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install class-variance-authority radix-uiCopy the source into your project
components/ui/item.tsximport * 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 flex-col", className)} {...props} />; } function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) { return <Separator data-slot="item-separator" orientation="horizontal" className={cn("my-0", className)} {...props} />; } const itemVariants = cva( "group/item flex flex-wrap items-center rounded-md border border-transparent text-sm transition-[background-color,border-color,box-shadow] duration-100 outline-none focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-ring/25 [a]:transition-colors [a]:hover:bg-secondary [a]:hover:border-input", { variants: { variant: { default: "bg-transparent", outline: "border-border", muted: "bg-secondary/60", }, size: { default: "gap-4 p-4", sm: "gap-2.5 px-4 py-3", }, }, 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: "size-9 rounded-md border border-border bg-accent text-[#b7ca9e] [&_svg:not([class*='size-'])]:size-4", image: "size-10 overflow-hidden rounded-md border border-border [&_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 min-w-0 flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none", className)} {...props} /> ); } function ItemTitle({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="item-title" className={cn("flex w-fit items-center gap-2 text-sm leading-snug font-medium", className)} {...props} /> ); } function ItemDescription({ className, ...props }: React.ComponentProps<"p">) { return ( <p data-slot="item-description" className={cn( "line-clamp-2 text-[13px] leading-normal font-normal text-balance text-muted-foreground", "[&>a]:text-primary [&>a]:underline-offset-4 [&>a:hover]:underline", 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 font-mono text-[10px] tracking-[0.2em] text-muted-foreground uppercase", 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, };Update the import paths to match your project
The source imports
cnfrom@/lib/utilsand uses separator.
Usage#
import {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
} from "@/components/ui/item";<Item variant="outline">
<ItemMedia variant="icon">
<Server />
</ItemMedia>
<ItemContent>
<ItemTitle>Deepstone</ItemTitle>
<ItemDescription>deepstone.hub:25565</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="outline">Join</Button>
</ItemActions>
</Item>Use asChild to render an Item as a link; it picks up a hover state automatically.
ItemGroup has role="list". Wrap each Item in an element with role="listitem", or pass role="listitem" to the Item.
Examples#
As links
import { BookOpen, ChevronRight, ExternalLink } from "lucide-react";
import { Item, ItemActions, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "@/components/ui/item";
export default function ItemLink() {
return (
<div className="flex w-full max-w-md flex-col gap-3">
<Item asChild variant="outline">
<a href="#">
<ItemMedia variant="icon">
<BookOpen />
</ItemMedia>
<ItemContent>
<ItemTitle>Turtle API guide</ItemTitle>
<ItemDescription>Move, dig and refuel from a script.</ItemDescription>
</ItemContent>
<ItemActions>
<ChevronRight className="size-4 text-muted-foreground" />
</ItemActions>
</a>
</Item>
<Item asChild variant="outline" size="sm">
<a href="#" target="_blank" rel="noreferrer">
<ItemContent>
<ItemTitle>Open guestbook.hub</ItemTitle>
</ItemContent>
<ItemActions>
<ExternalLink className="size-4 text-primary" />
</ItemActions>
</a>
</Item>
</div>
);
}Avatars, header and footer
MB
moss_builder
Signed your guestbook two hours ago.
Invite3 pending
LSTKRB
Build crew
Share edit access to turtle.farm.
Last change 09:42Synced
import { Plus } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemFooter,
ItemHeader,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
export default function ItemAvatar() {
return (
<div className="flex w-full max-w-md flex-col gap-4">
<Item variant="outline">
<ItemMedia>
<Avatar>
<AvatarFallback>MB</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemTitle>moss_builder</ItemTitle>
<ItemDescription>Signed your guestbook two hours ago.</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="outline">
Reply
</Button>
</ItemActions>
</Item>
<Item variant="muted">
<ItemHeader>
<span>Invite</span>
<span>3 pending</span>
</ItemHeader>
<ItemMedia>
<div className="flex -space-x-2">
{["LS", "TK", "RB"].map((initials) => (
<Avatar key={initials} className="size-8 ring-2 ring-background">
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
))}
</div>
</ItemMedia>
<ItemContent>
<ItemTitle>Build crew</ItemTitle>
<ItemDescription>Share edit access to turtle.farm.</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="icon-sm" variant="outline" aria-label="Invite a player">
<Plus />
</Button>
</ItemActions>
<ItemFooter className="font-mono text-[11px] text-muted-foreground">
<span>Last change 09:42</span>
<span className="text-success">Synced</span>
</ItemFooter>
</Item>
</div>
);
}