The Item component is a straightforward flex container that can house nearly any type of content. Use it to display a title, description, and actions. Group it with the ItemGroup component to create a list of items.
You can pretty much achieve the same result with the div element and some classes, but I've built this so many times that I decided to create a component for it. Now I use it all the time.
A simple item with title and description.
<script lang="ts">
import * as Item from "$lib/components/ui/item/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import BadgeCheckIcon from "@lucide/svelte/icons/badge-check";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
</script>
<div class="flex w-full max-w-md flex-col gap-6">
<Item.Root variant="outline">
<Item.Content>
<Item.Title>Basic Item</Item.Title>
<Item.Description
>A simple item with title and description.</Item.Description
>
</Item.Content>
<Item.Actions>
<Button variant="outline" size="sm">Action</Button>
</Item.Actions>
</Item.Root>
<Item.Root variant="outline" size="sm">
{#snippet child({ props })}
<a href="/docs" {...props}>
<Item.Media>
<BadgeCheckIcon class="size-5" />
</Item.Media>
<Item.Content>
<Item.Title>Your profile has been verified.</Item.Title>
</Item.Content>
<Item.Actions>
<ChevronRightIcon class="size-4" />
</Item.Actions>
</a>
{/snippet}
</Item.Root>
</div> Installation
pnpm dlx shadcn-svelte@latest add item Copy and paste the following code into your project.
import Root from './item.svelte';
import Group from './item-group.svelte';
import Separator from './item-separator.svelte';
import Header from './item-header.svelte';
import Footer from './item-footer.svelte';
import Content from './item-content.svelte';
import Title from './item-title.svelte';
import Description from './item-description.svelte';
import Actions from './item-actions.svelte';
import Media from './item-media.svelte';
export {
Root,
Group,
Separator,
Header,
Footer,
Content,
Title,
Description,
Actions,
Media,
//
Root as Item,
Group as ItemGroup,
Separator as ItemSeparator,
Header as ItemHeader,
Footer as ItemFooter,
Content as ItemContent,
Title as ItemTitle,
Description as ItemDescription,
Actions as ItemActions,
Media as ItemMedia
};
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-actions"
class={cn('flex items-center gap-2 text-zinc-400', className)}
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-content"
class={cn(
'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0.5 [&+[data-slot=item-content]]:flex-none',
className
)}
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="item-description"
class={cn(
'line-clamp-2 text-left text-sm leading-relaxed font-normal text-zinc-500 [&>a]:text-[#d0e891] [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-[#d0e891]',
className
)}
{...restProps}
>
{@render children?.()}
</p>
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-footer"
class={cn(
'flex basis-full items-center justify-between gap-2 border-t border-zinc-900 pt-2 text-zinc-500',
className
)}
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
role="list"
data-slot="item-group"
class={cn(
'group/item-group flex w-full flex-col gap-px has-data-[size=sm]:gap-px has-data-[size=xs]:gap-px',
className
)}
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-header"
class={cn('flex basis-full items-center justify-between gap-2 text-zinc-100', className)}
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts" module>
import { tv, type VariantProps } from 'tailwind-variants';
export const itemMediaVariants = tv({
base: 'flex shrink-0 items-center justify-center gap-2 text-zinc-400 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: "text-[#d0e891] [&_svg:not([class*='size-'])]:size-4",
image:
'size-10 overflow-hidden rounded-none border border-zinc-800 bg-zinc-950 group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 group-data-[size=xs]/item:rounded-none [&_img]:size-full [&_img]:object-cover'
}
},
defaultVariants: {
variant: 'default'
}
});
export type ItemMediaVariant = VariantProps<typeof itemMediaVariants>['variant'];
</script>
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
variant = 'default',
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: ItemMediaVariant } = $props();
</script>
<div
bind:this={ref}
data-slot="item-media"
data-variant={variant}
class={cn(itemMediaVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts">
import { Separator } from '$UI$/separator/index.js';
import { cn } from '$UTILS$.js';
import type { ComponentProps } from 'svelte';
let {
ref = $bindable(null),
class: className,
...restProps
}: ComponentProps<typeof Separator> = $props();
</script>
<Separator
bind:ref
data-slot="item-separator"
orientation="horizontal"
class={cn('my-1 bg-zinc-900', className)}
{...restProps}
/>
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-title"
class={cn(
'line-clamp-1 flex w-fit items-center gap-2 font-mono text-xs leading-snug font-semibold tracking-[0.14em] text-zinc-100 uppercase underline-offset-4',
className
)}
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts" module>
import { tv, type VariantProps } from 'tailwind-variants';
export const itemVariants = tv({
base: 'group/item flex w-full flex-wrap items-center rounded-none border bg-background text-sm text-zinc-200 outline-none transition-colors duration-100 [a]:transition-colors [a]:hover:bg-zinc-950 focus-visible:border-zinc-300 focus-visible:ring-2 focus-visible:ring-zinc-300/30',
variants: {
variant: {
default: 'border-zinc-900',
outline: 'border-zinc-800',
muted: 'border-zinc-900 bg-zinc-950/80'
},
size: {
default: 'gap-3 px-4 py-3',
sm: 'gap-3 px-3.5 py-2.5',
xs: 'gap-2.5 px-3 py-2 in-data-[slot=dropdown-menu-content]:p-0'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
export type ItemSize = VariantProps<typeof itemVariants>['size'];
export type ItemVariant = VariantProps<typeof itemVariants>['variant'];
</script>
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
class: className,
child,
variant,
size,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
variant?: ItemVariant;
size?: ItemSize;
} = $props();
const mergedProps = $derived({
class: cn(itemVariants({ variant, size }), className),
'data-slot': 'item',
'data-variant': variant,
'data-size': size,
...restProps
});
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div bind:this={ref} {...mergedProps}>
{@render mergedProps.children?.()}
</div>
{/if}
Usage
<script lang="ts">
import * as Item from '$lib/components/ui/item/index.js';
</script> <Item.Root>
<Item.Header>Item Header</Item.Header>
<Item.Media />
<Item.Content>
<Item.Title>Item</Item.Title>
<Item.Description>Item</Item.Description>
</Item.Content>
<Item.Actions />
<Item.Footer>Item Footer</Item.Footer>
</Item.Root> Item vs Field
Use Field if you need to display a form input such as a checkbox, input, radio, or select.
If you only need to display content such as a title, description, and actions, use Item.
Examples
Variants
Standard styling with subtle background and borders.
Outlined style with clear borders and transparent background.
Subdued appearance with muted colors for secondary content.
<script lang="ts">
import * as Item from "$lib/components/ui/item/index.js";
import { Button } from "$lib/components/ui/button/index.js";
</script>
<div class="flex flex-col gap-6">
<Item.Root>
<Item.Content>
<Item.Title>Default Variant</Item.Title>
<Item.Description
>Standard styling with subtle background and borders.</Item.Description
>
</Item.Content>
<Item.Actions>
<Button variant="outline" size="sm">Open</Button>
</Item.Actions>
</Item.Root>
<Item.Root variant="outline">
<Item.Content>
<Item.Title>Outline Variant</Item.Title>
<Item.Description>
Outlined style with clear borders and transparent background.
</Item.Description>
</Item.Content>
<Item.Actions>
<Button variant="outline" size="sm">Open</Button>
</Item.Actions>
</Item.Root>
<Item.Root variant="muted">
<Item.Content>
<Item.Title>Muted Variant</Item.Title>
<Item.Description>
Subdued appearance with muted colors for secondary content.
</Item.Description>
</Item.Content>
<Item.Actions>
<Button variant="outline" size="sm">Open</Button>
</Item.Actions>
</Item.Root>
</div> Size
The Item component has different sizes for different use cases. For example, you can use the sm size for a compact item or the default size for a standard item.
A simple item with title and description.
<script lang="ts">
import * as Item from "$lib/components/ui/item/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import BadgeCheckIcon from "@lucide/svelte/icons/badge-check";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
</script>
<div class="flex w-full max-w-md flex-col gap-6">
<Item.Root variant="outline">
<Item.Content>
<Item.Title>Basic Item</Item.Title>
<Item.Description
>A simple item with title and description.</Item.Description
>
</Item.Content>
<Item.Actions>
<Button variant="outline" size="sm">Action</Button>
</Item.Actions>
</Item.Root>
<Item.Root variant="outline" size="sm">
{#snippet child({ props })}
<a href="/docs" {...props}>
<Item.Media>
<BadgeCheckIcon class="size-5" />
</Item.Media>
<Item.Content>
<Item.Title>Your profile has been verified.</Item.Title>
</Item.Content>
<Item.Actions>
<ChevronRightIcon class="size-4" />
</Item.Actions>
</a>
{/snippet}
</Item.Root>
</div> Icon
New login detected from unknown device.
<script lang="ts">
import * as Item from "$lib/components/ui/item/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import ShieldAlertIcon from "@lucide/svelte/icons/shield-alert";
</script>
<div class="flex w-full max-w-lg flex-col gap-6">
<Item.Root variant="outline">
<Item.Media variant="icon">
<ShieldAlertIcon />
</Item.Media>
<Item.Content>
<Item.Title>Security Alert</Item.Title>
<Item.Description
>New login detected from unknown device.</Item.Description
>
</Item.Content>
<Item.Actions>
<Button size="sm" variant="outline">Review</Button>
</Item.Actions>
</Item.Root>
</div> Avatar
Last seen 5 months ago
Invite your team to collaborate on this project.
<script lang="ts">
import * as Item from "$lib/components/ui/item/index.js";
import * as Avatar from "$lib/components/ui/avatar/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import Plus from "@lucide/svelte/icons/plus";
</script>
<div class="flex w-full max-w-lg flex-col gap-6">
<Item.Root variant="outline">
<Item.Media>
<Avatar.Root class="size-10">
<Avatar.Image src="https://github.com/evilrabbit.png" />
<Avatar.Fallback>ER</Avatar.Fallback>
</Avatar.Root>
</Item.Media>
<Item.Content>
<Item.Title>Evil Rabbit</Item.Title>
<Item.Description>Last seen 5 months ago</Item.Description>
</Item.Content>
<Item.Actions>
<Button
size="icon"
variant="outline"
class="rounded-full"
aria-label="Invite"
>
<Plus />
</Button>
</Item.Actions>
</Item.Root>
<Item.Root variant="outline">
<Item.Media>
<div
class="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale"
>
<Avatar.Root class="hidden sm:flex">
<Avatar.Image src="https://github.com/shadcn.png" alt="@shadcn" />
<Avatar.Fallback>CN</Avatar.Fallback>
</Avatar.Root>
<Avatar.Root class="hidden sm:flex">
<Avatar.Image
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<Avatar.Fallback>LR</Avatar.Fallback>
</Avatar.Root>
<Avatar.Root>
<Avatar.Image
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<Avatar.Fallback>ER</Avatar.Fallback>
</Avatar.Root>
</div>
</Item.Media>
<Item.Content>
<Item.Title>No Team Members</Item.Title>
<Item.Description
>Invite your team to collaborate on this project.</Item.Description
>
</Item.Content>
<Item.Actions>
<Button size="sm" variant="outline">Invite</Button>
</Item.Actions>
</Item.Root>
</div> Image
<script lang="ts">
import * as Item from "$lib/components/ui/item/index.js";
const music = [
{
title: "Midnight City Lights",
artist: "Neon Dreams",
album: "Electric Nights",
duration: "3:45"
},
{
title: "Coffee Shop Conversations",
artist: "The Morning Brew",
album: "Urban Stories",
duration: "4:05"
},
{
title: "Digital Rain",
artist: "Cyber Symphony",
album: "Binary Beats",
duration: "3:30"
}
];
</script>
<div class="flex w-full max-w-md flex-col gap-6">
<div class="flex w-full max-w-md flex-col gap-4">
{#each music as song (song)}
<Item.Root variant="outline">
{#snippet child({ props })}
<a href="/docs" {...props}>
<Item.Media variant="image">
<img
src={`https://avatar.vercel.sh/${song.title}`}
alt={song.title}
width="32"
height="32"
class="size-8 rounded object-cover grayscale"
/>
</Item.Media>
<Item.Content>
<Item.Title class="line-clamp-1">
{song.title} -
<span class="text-muted-foreground">{song.album}</span>
</Item.Title>
<Item.Description>{song.artist}</Item.Description>
</Item.Content>
<Item.Content class="flex-none text-center">
<Item.Description>{song.duration}</Item.Description>
</Item.Content>
</a>
{/snippet}
</Item.Root>
{/each}
</div>
</div> Group
<script lang="ts">
import * as Item from "$lib/components/ui/item/index.js";
import * as Avatar from "$lib/components/ui/avatar/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import Plus from "@lucide/svelte/icons/plus";
const people = [
{
username: "shadcn",
avatar: "https://github.com/shadcn.png",
email: "[email protected]"
},
{
username: "maxleiter",
avatar: "https://github.com/maxleiter.png",
email: "[email protected]"
},
{
username: "evilrabbit",
avatar: "https://github.com/evilrabbit.png",
email: "[email protected]"
}
];
</script>
<div class="flex w-full max-w-md flex-col gap-6">
<Item.Group>
{#each people as person, index (person.username)}
<Item.Root>
<Item.Media>
<Avatar.Root>
<Avatar.Image src={person.avatar} class="grayscale" />
<Avatar.Fallback>{person.username.charAt(0)}</Avatar.Fallback>
</Avatar.Root>
</Item.Media>
<Item.Content class="gap-1">
<Item.Title>{person.username}</Item.Title>
<Item.Description>{person.email}</Item.Description>
</Item.Content>
<Item.Actions>
<Button variant="ghost" size="icon" class="rounded-full">
<Plus />
</Button>
</Item.Actions>
</Item.Root>
{#if index !== people.length - 1}
<Item.Separator />
{/if}
{/each}
</Item.Group>
</div> Header
Everyday tasks and UI generation.
Advanced thinking or reasoning.
Open Source model for everyone.
<script lang="ts">
import * as Item from "$lib/components/ui/item/index.js";
const models = [
{
name: "v0-1.5-sm",
description: "Everyday tasks and UI generation.",
image:
"https://images.unsplash.com/photo-1650804068570-7fb2e3dbf888?q=80&w=640&auto=format&fit=crop",
credit: "Valeria Reverdo on Unsplash"
},
{
name: "v0-1.5-lg",
description: "Advanced thinking or reasoning.",
image:
"https://images.unsplash.com/photo-1610280777472-54133d004c8c?q=80&w=640&auto=format&fit=crop",
credit: "Michael Oeser on Unsplash"
},
{
name: "v0-2.0-mini",
description: "Open Source model for everyone.",
image:
"https://images.unsplash.com/photo-1602146057681-08560aee8cde?q=80&w=640&auto=format&fit=crop",
credit: "Cherry Laithang on Unsplash"
}
];
</script>
<div class="flex w-full max-w-xl flex-col gap-6">
<Item.Group class="grid grid-cols-3 gap-4">
{#each models as model (model.name)}
<Item.Root variant="outline">
<Item.Header>
<img
src={model.image}
alt={model.name}
width="128"
height="128"
class="aspect-square w-full rounded-sm object-cover"
/>
</Item.Header>
<Item.Content>
<Item.Title>{model.name}</Item.Title>
<Item.Description>{model.description}</Item.Description>
</Item.Content>
</Item.Root>
{/each}
</Item.Group>
</div> Link
To render an item as a link, use the the child snippet. The hover and focus states will be applied to the anchor element.
<script lang="ts">
import * as Item from "$lib/components/ui/item/index.js";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import ExternalLinkIcon from "@lucide/svelte/icons/external-link";
</script>
<div class="flex w-full max-w-md flex-col gap-4">
<Item.Root>
{#snippet child({ props })}
<a href="/docs" {...props}>
<Item.Content>
<Item.Title>Visit our documentation</Item.Title>
<Item.Description
>Learn how to get started with our components.</Item.Description
>
</Item.Content>
<Item.Actions>
<ChevronRightIcon class="size-4" />
</Item.Actions>
</a>
{/snippet}
</Item.Root>
<Item.Root variant="outline">
{#snippet child({ props })}
<a
href="https://kura.gfdc.dev/docs"
target="_blank"
rel="noopener noreferrer"
{...props}
>
<Item.Content>
<Item.Title>External resource</Item.Title>
<Item.Description
>Opens in a new tab with security attributes.</Item.Description
>
</Item.Content>
<Item.Actions>
<ExternalLinkIcon class="size-4" />
</Item.Actions>
</a>
{/snippet}
</Item.Root>
</div> Dropdown
<script lang="ts">
import * as Item from "$lib/components/ui/item/index.js";
import * as Avatar from "$lib/components/ui/avatar/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import ChevronDown from "@lucide/svelte/icons/chevron-down";
const people = [
{
username: "shadcn",
avatar: "https://github.com/shadcn.png",
email: "[email protected]"
},
{
username: "maxleiter",
avatar: "https://github.com/maxleiter.png",
email: "[email protected]"
},
{
username: "evilrabbit",
avatar: "https://github.com/evilrabbit.png",
email: "[email protected]"
}
];
</script>
<div class="flex min-h-64 w-full max-w-md flex-col items-center gap-6">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline" size="sm" class="w-fit">
Select <ChevronDown />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-72 [--radius:0.65rem]" align="end">
{#each people as person (person.username)}
<DropdownMenu.Item class="p-0">
<Item.Root size="sm" class="w-full p-2">
<Item.Media>
<Avatar.Root class="size-8">
<Avatar.Image src={person.avatar} class="grayscale" />
<Avatar.Fallback>{person.username.charAt(0)}</Avatar.Fallback>
</Avatar.Root>
</Item.Media>
<Item.Content class="gap-0.5">
<Item.Title>{person.username}</Item.Title>
<Item.Description>{person.email}</Item.Description>
</Item.Content>
</Item.Root>
</DropdownMenu.Item>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>