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

Installation#

With the CLI

$npx lanterncn add item

This 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

  1. Install the dependencies

    npm install class-variance-authority radix-ui
  2. Copy the source into your project

    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 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,
    };
  3. Update the import paths to match your project

    The source imports cn from @/lib/utils and 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#

MB
moss_builder

Signed your guestbook two hours ago.

Invite3 pending
LSTKRB
Build crew

Share edit access to turtle.farm.

Last change 09:42Synced