Item
A versatile component for displaying content with media, title, description, and actions.
A simple item with title and description.
import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { BadgeCheckIcon, ChevronRightIcon } from '#app/ui/icons.tsx'
import {
Item,import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { BadgeCheckIcon, ChevronRightIcon } from '#app/ui/icons.tsx'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '#app/ui/item.tsx'
const stackStyles = css({
display: 'flex',
width: '100%',
maxWidth: '28rem',
flexDirection: 'column',
gap: '1.5rem',
})
const size5 = css({ width: '1.25rem', height: '1.25rem' })
const size4 = css({ width: '1rem', height: '1rem' })
export function ItemDemo() {
return () => (
<div mix={stackStyles}>
<Item variant="outline">
<ItemContent>
<ItemTitle>Basic Item</ItemTitle>
<ItemDescription>A simple item with title and description.</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline" size="sm">
Action
</Button>
</ItemActions>
</Item>
<Item variant="outline" size="sm" href="#">
<ItemMedia>
<BadgeCheckIcon mix={size5} />
</ItemMedia>
<ItemContent>
<ItemTitle>Your profile has been verified.</ItemTitle>
</ItemContent>
<ItemActions>
<ChevronRightIcon mix={size4} />
</ItemActions>
</Item>
</div>
)
}Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/item.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/item.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/item.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/item.jsonInstall the following dependencies:
$ bun add remix$ npm install remix$ pnpm add remix$ yarn add remixCopy and paste the following code into your project.
import { css, type CSSMixinDescriptor, type Handle, type MixValue, type Props } from 'remix/component'
import { Separator, type SeparatorProps } from './separator.tsx'
export type ItemVariant = 'default' | 'outline' | 'muted'
export type ItemSize = 'default' | 'sm'
export type ItemMediaVariant = 'default' | 'icon' | 'image'
export interface ItemVariantOptions {
variant?: ItemVariant
size?: ItemSize
}
/**
* Renders a `<div>`, or an `<a>` when `href` is given (shadcn's `<Item asChild><a/></Item>`).
* For any other element, apply `itemVariants()` to it.
*/
export interface ItemProps extends Omit<Props<'div'>, 'href' | 'mix'>, ItemVariantOptions {
mix?: MixValue<HTMLElement>
href?: string
target?: string
rel?: string
}
export interface ItemMediaProps extends Props<'div'> {
variant?: ItemMediaVariant
}
const groupStyles = css({ display: 'flex', flexDirection: 'column' })
const separatorStyles = css({ marginBlock: 0 })
const itemBase = css({
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
borderRadius: 'calc(var(--radius) - 2px)',
borderWidth: '1px',
borderColor: 'transparent',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
transitionProperty:
'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '100ms',
outlineStyle: 'none',
boxShadow: 'var(--tw-ring-shadow), var(--tw-shadow)',
'&:focus-visible': {
borderColor: 'var(--ring)',
'--tw-ring-shadow':
'0 0 0 3px var(--tw-ring-color, color-mix(in oklab, var(--ring) 50%, transparent))',
},
})
// `[a]:hover:bg-accent/50` is listed after the variant backgrounds so it wins on links.
const itemVariantStyles: Record<ItemVariant, CSSMixinDescriptor> = {
default: css({
backgroundColor: 'transparent',
'&:is(a):hover': { backgroundColor: 'color-mix(in oklab, var(--accent) 50%, transparent)' },
}),
outline: css({
borderColor: 'var(--border)',
'&:focus-visible': { borderColor: 'var(--ring)' },
'&:is(a):hover': { backgroundColor: 'color-mix(in oklab, var(--accent) 50%, transparent)' },
}),
muted: css({
backgroundColor: 'color-mix(in oklab, var(--muted) 50%, transparent)',
'&:is(a):hover': { backgroundColor: 'color-mix(in oklab, var(--accent) 50%, transparent)' },
}),
}
const itemSizes: Record<ItemSize, CSSMixinDescriptor> = {
default: css({ gap: '1rem', padding: '1rem' }),
sm: css({ gap: '0.625rem', paddingInline: '1rem', paddingBlock: '0.75rem' }),
}
const mediaBase = css({
display: 'flex',
flexShrink: 0,
alignItems: 'center',
justifyContent: 'center',
gap: '0.5rem',
'[data-slot="item"]:has([data-slot="item-description"]) &': {
translate: '0 0.125rem',
alignSelf: 'flex-start',
},
'& svg': { pointerEvents: 'none' },
})
const mediaVariants: Record<ItemMediaVariant, CSSMixinDescriptor> = {
default: css({ backgroundColor: 'transparent' }),
icon: css({
width: '2rem',
height: '2rem',
borderRadius: 'calc(var(--radius) - 4px)',
borderWidth: '1px',
backgroundColor: 'var(--muted)',
'& svg:not([class*="size-"])': { width: '1rem', height: '1rem' },
}),
image: css({
width: '2.5rem',
height: '2.5rem',
overflow: 'hidden',
borderRadius: 'calc(var(--radius) - 4px)',
'& img': { width: '100%', height: '100%', objectFit: 'cover' },
}),
}
const contentStyles = css({
display: 'flex',
flex: '1 1 0%',
flexDirection: 'column',
gap: '0.25rem',
'& + [data-slot="item-content"]': { flex: 'none' },
})
const titleStyles = css({
display: 'flex',
width: 'fit-content',
alignItems: 'center',
gap: '0.5rem',
fontSize: '0.875rem',
lineHeight: 1.375,
fontWeight: 500,
})
const descriptionStyles = css({
overflow: 'hidden',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: '2',
fontSize: '0.875rem',
lineHeight: 1.5,
fontWeight: 400,
textWrap: 'balance',
color: 'var(--muted-foreground)',
'& > a': { textDecorationLine: 'underline', textUnderlineOffset: '4px' },
'& > a:hover': { color: 'var(--primary)' },
})
const actionsStyles = css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })
const headerFooterStyles = css({
display: 'flex',
flexBasis: '100%',
alignItems: 'center',
justifyContent: 'space-between',
gap: '0.5rem',
})
export function ItemGroup(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div role="list" data-slot="item-group" {...props} mix={[groupStyles, mix]} />
}
}
export function ItemSeparator(handle: Handle<SeparatorProps>) {
return () => {
let { mix, ...props } = handle.props
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
{...props}
mix={[separatorStyles, mix]}
/>
)
}
}
/**
* The `itemVariants()` of shadcn/ui. Returns a mix that styles any element as an item (shadcn's
* `asChild`). Also set `data-slot="item"` on that element so `ItemMedia` aligns to descriptions.
*/
export function itemVariants({ variant = 'default', size = 'default' }: ItemVariantOptions = {}) {
return [itemBase, itemVariantStyles[variant], itemSizes[size]]
}
export function Item(handle: Handle<ItemProps>) {
return () => {
let { variant = 'default', size = 'default', href, target, rel, mix, ...props } = handle.props
let styles = [itemVariants({ variant, size }), mix]
if (href !== undefined) {
return (
<a
data-slot="item"
data-variant={variant}
data-size={size}
href={href}
target={target}
rel={rel}
{...(props as Omit<Props<'a'>, 'role'>)}
mix={styles}
/>
)
}
return <div data-slot="item" data-variant={variant} data-size={size} {...props} mix={styles} />
}
}
/** The `itemMediaVariants()` of shadcn/ui, as a mix. */
export function itemMediaVariants({ variant = 'default' }: { variant?: ItemMediaVariant } = {}) {
return [mediaBase, mediaVariants[variant]]
}
export function ItemMedia(handle: Handle<ItemMediaProps>) {
return () => {
let { variant = 'default', mix, ...props } = handle.props
return (
<div
data-slot="item-media"
data-variant={variant}
{...props}
mix={[itemMediaVariants({ variant }), mix]}
/>
)
}
}
export function ItemContent(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="item-content" {...props} mix={[contentStyles, mix]} />
}
}
export function ItemTitle(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="item-title" {...props} mix={[titleStyles, mix]} />
}
}
export function ItemDescription(handle: Handle<Props<'p'>>) {
return () => {
let { mix, ...props } = handle.props
return <p data-slot="item-description" {...props} mix={[descriptionStyles, mix]} />
}
}
export function ItemActions(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="item-actions" {...props} mix={[actionsStyles, mix]} />
}
}
export function ItemHeader(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="item-header" {...props} mix={[headerFooterStyles, mix]} />
}
}
export function ItemFooter(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="item-footer" {...props} mix={[headerFooterStyles, mix]} />
}
}Update the import paths to match your project setup.
Usage
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '#app/ui/item.tsx'<Item>
<ItemMedia variant="icon">
<Icon />
</ItemMedia>
<ItemContent>
<ItemTitle>Title</ItemTitle>
<ItemDescription>Description</ItemDescription>
</ItemContent>
<ItemActions>
<Button>Action</Button>
</ItemActions>
</Item>Examples
Variant
Use the variant prop to change the visual style of the item.
Standard styling with subtle background and borders.
Outlined style with clear borders and transparent background.
Subdued appearance with muted colors for secondary content.
import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import {
Item,
ItemActions,import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemTitle,
} from '#app/ui/item.tsx'
const stackStyles = css({ display: 'flex', flexDirection: 'column', gap: '1.5rem' })
export function ItemVariant() {
return () => (
<div mix={stackStyles}>
<Item>
<ItemContent>
<ItemTitle>Default Variant</ItemTitle>
<ItemDescription>Standard styling with subtle background and borders.</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline" size="sm">
Open
</Button>
</ItemActions>
</Item>
<Item variant="outline">
<ItemContent>
<ItemTitle>Outline Variant</ItemTitle>
<ItemDescription>
Outlined style with clear borders and transparent background.
</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline" size="sm">
Open
</Button>
</ItemActions>
</Item>
<Item variant="muted">
<ItemContent>
<ItemTitle>Muted Variant</ItemTitle>
<ItemDescription>
Subdued appearance with muted colors for secondary content.
</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline" size="sm">
Open
</Button>
</ItemActions>
</Item>
</div>
)
}Size
Use the size prop to change the size of the item. Available sizes are default, sm, and xs.
A simple item with title and description.
import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { BadgeCheckIcon, ChevronRightIcon } from '#app/ui/icons.tsx'
import {
Item,import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { BadgeCheckIcon, ChevronRightIcon } from '#app/ui/icons.tsx'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '#app/ui/item.tsx'
const stackStyles = css({
display: 'flex',
width: '100%',
maxWidth: '28rem',
flexDirection: 'column',
gap: '1.5rem',
})
const size5 = css({ width: '1.25rem', height: '1.25rem' })
const size4 = css({ width: '1rem', height: '1rem' })
export function ItemSize() {
return () => (
<div mix={stackStyles}>
<Item variant="outline">
<ItemContent>
<ItemTitle>Basic Item</ItemTitle>
<ItemDescription>A simple item with title and description.</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline" size="sm">
Action
</Button>
</ItemActions>
</Item>
<Item variant="outline" size="sm" href="#">
<ItemMedia>
<BadgeCheckIcon mix={size5} />
</ItemMedia>
<ItemContent>
<ItemTitle>Your profile has been verified.</ItemTitle>
</ItemContent>
<ItemActions>
<ChevronRightIcon mix={size4} />
</ItemActions>
</Item>
</div>
)
}Icon
Use ItemMedia with variant="icon" to display an icon.
New login detected from unknown device.
import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { ShieldAlertIcon } from '#app/ui/icons.tsx'
import {
Item,import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { ShieldAlertIcon } from '#app/ui/icons.tsx'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '#app/ui/item.tsx'
const stackStyles = css({
display: 'flex',
width: '100%',
maxWidth: '32rem',
flexDirection: 'column',
gap: '1.5rem',
})
export function ItemIcon() {
return () => (
<div mix={stackStyles}>
<Item variant="outline">
<ItemMedia variant="icon">
<ShieldAlertIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Security Alert</ItemTitle>
<ItemDescription>New login detected from unknown device.</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="outline">
Review
</Button>
</ItemActions>
</Item>
</div>
)
}Avatar
You can use ItemMedia with variant="avatar" to display an avatar.
Last seen 5 months ago
Invite your team to collaborate on this project.
import { css } from 'remix/component'
import { Avatar, AvatarFallback, AvatarImage } from '#app/ui/avatar.tsx'
import { Button } from '#app/ui/button.tsx'
import { PlusIcon } from '#app/ui/icons.tsx'
import {import { css } from 'remix/component'
import { Avatar, AvatarFallback, AvatarImage } from '#app/ui/avatar.tsx'
import { Button } from '#app/ui/button.tsx'
import { PlusIcon } from '#app/ui/icons.tsx'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '#app/ui/item.tsx'
const stackStyles = css({
display: 'flex',
width: '100%',
maxWidth: '32rem',
flexDirection: 'column',
gap: '1.5rem',
})
const size10 = css({ width: '2.5rem', height: '2.5rem' })
const roundedFull = css({ borderRadius: '9999px' })
const groupStyles = css({
display: 'flex',
'& > :not(:last-child)': { marginInlineEnd: '-0.5rem' },
})
// `*:data-[slot=avatar]:ring-2 ring-background grayscale`, applied to each avatar so it layers
// after the avatar's own styles.
const groupAvatar = css({ boxShadow: '0 0 0 2px var(--background)', filter: 'grayscale(100%)' })
const hiddenUntilSm = css({ display: 'none', '@media (min-width: 40rem)': { display: 'flex' } })
export function ItemAvatar() {
return () => (
<div mix={stackStyles}>
<Item variant="outline">
<ItemMedia>
<Avatar mix={size10}>
<AvatarImage src="https://github.com/evilrabbit.png" />
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemTitle>Evil Rabbit</ItemTitle>
<ItemDescription>Last seen 5 months ago</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="icon-sm" variant="outline" mix={roundedFull} aria-label="Invite">
<PlusIcon />
</Button>
</ItemActions>
</Item>
<Item variant="outline">
<ItemMedia>
<div mix={groupStyles}>
<Avatar mix={[groupAvatar, hiddenUntilSm]}>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar mix={[groupAvatar, hiddenUntilSm]}>
<AvatarImage src="https://github.com/maxleiter.png" alt="@maxleiter" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar mix={groupAvatar}>
<AvatarImage src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>
</ItemMedia>
<ItemContent>
<ItemTitle>No Team Members</ItemTitle>
<ItemDescription>Invite your team to collaborate on this project.</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="outline">
Invite
</Button>
</ItemActions>
</Item>
</div>
)
}Image
Use ItemMedia with variant="image" to display an image.
import { css } from 'remix/component'
import {
Item,
ItemContent,
ItemDescription,import { css } from 'remix/component'
import {
Item,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from '#app/ui/item.tsx'
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' },
]
const stackStyles = css({
display: 'flex',
width: '100%',
maxWidth: '28rem',
flexDirection: 'column',
gap: '1.5rem',
})
const groupStyles = css({ gap: '1rem' })
const imageStyles = css({ objectFit: 'cover', filter: 'grayscale(100%)' })
const titleStyles = css({
overflow: 'hidden',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: '1',
})
const mutedStyles = css({ color: 'var(--muted-foreground)' })
const durationStyles = css({ flex: 'none', textAlign: 'center' })
export function ItemImage() {
return () => (
<div mix={stackStyles}>
<ItemGroup mix={groupStyles}>
{music.map((song) => (
<Item key={song.title} variant="outline" href="#" role="listitem">
<ItemMedia variant="image">
<img
src={`https://avatar.vercel.sh/${song.title}`}
alt={song.title}
width={32}
height={32}
mix={imageStyles}
/>
</ItemMedia>
<ItemContent>
<ItemTitle mix={titleStyles}>
{song.title} - <span mix={mutedStyles}>{song.album}</span>
</ItemTitle>
<ItemDescription>{song.artist}</ItemDescription>
</ItemContent>
<ItemContent mix={durationStyles}>
<ItemDescription>{song.duration}</ItemDescription>
</ItemContent>
</Item>
))}
</ItemGroup>
</div>
)
}Group
Use ItemGroup to group related items together.
shadcn@vercel.com
maxleiter@vercel.com
evilrabbit@vercel.com
import { css } from 'remix/component'
import { Avatar, AvatarFallback, AvatarImage } from '#app/ui/avatar.tsx'
import { Button } from '#app/ui/button.tsx'
import { PlusIcon } from '#app/ui/icons.tsx'
import {import { css } from 'remix/component'
import { Avatar, AvatarFallback, AvatarImage } from '#app/ui/avatar.tsx'
import { Button } from '#app/ui/button.tsx'
import { PlusIcon } from '#app/ui/icons.tsx'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup as ItemGroupRoot,
ItemMedia,
ItemSeparator,
ItemTitle,
} from '#app/ui/item.tsx'
const people = [
{ username: 'shadcn', avatar: 'https://github.com/shadcn.png', email: 'shadcn@vercel.com' },
{ username: 'maxleiter', avatar: 'https://github.com/maxleiter.png', email: 'maxleiter@vercel.com' },
{
username: 'evilrabbit',
avatar: 'https://github.com/evilrabbit.png',
email: 'evilrabbit@vercel.com',
},
]
const stackStyles = css({
display: 'flex',
width: '100%',
maxWidth: '28rem',
flexDirection: 'column',
gap: '1.5rem',
})
const grayscale = css({ filter: 'grayscale(100%)' })
const contentStyles = css({ gap: '0.25rem' })
const roundedFull = css({ borderRadius: '9999px' })
export function ItemGroup() {
return () => (
<div mix={stackStyles}>
<ItemGroupRoot>
{people.map((person, index) => [
<Item key={person.username}>
<ItemMedia>
<Avatar>
<AvatarImage src={person.avatar} mix={grayscale} />
<AvatarFallback>{person.username.charAt(0)}</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent mix={contentStyles}>
<ItemTitle>{person.username}</ItemTitle>
<ItemDescription>{person.email}</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="ghost" size="icon" mix={roundedFull}>
<PlusIcon />
</Button>
</ItemActions>
</Item>,
index !== people.length - 1 && <ItemSeparator key={`${person.username}-separator`} />,
])}
</ItemGroupRoot>
</div>
)
}Header
Use ItemHeader to add a header above the item content.
Everyday tasks and UI generation.
Advanced thinking or reasoning.
Open Source model for everyone.
import { css } from 'remix/component'
import {
Item,
ItemContent,
ItemDescription,import { css } from 'remix/component'
import {
Item,
ItemContent,
ItemDescription,
ItemGroup,
ItemHeader as ItemHeaderSlot,
ItemTitle,
} from '#app/ui/item.tsx'
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',
},
]
const stackStyles = css({
display: 'flex',
width: '100%',
maxWidth: '36rem',
flexDirection: 'column',
gap: '1.5rem',
})
const gridStyles = css({
display: 'grid',
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
gap: '1rem',
})
const imageStyles = css({
aspectRatio: '1 / 1',
width: '100%',
borderRadius: 'calc(var(--radius) - 4px)',
objectFit: 'cover',
})
export function ItemHeader() {
return () => (
<div mix={stackStyles}>
<ItemGroup mix={gridStyles}>
{models.map((model) => (
<Item key={model.name} variant="outline">
<ItemHeaderSlot>
<img src={model.image} alt={model.name} width={128} height={128} mix={imageStyles} />
</ItemHeaderSlot>
<ItemContent>
<ItemTitle>{model.name}</ItemTitle>
<ItemDescription>{model.description}</ItemDescription>
</ItemContent>
</Item>
))}
</ItemGroup>
</div>
)
}Link
Pass href to render the item as a link. The hover and focus states are applied to the anchor element.
import { css } from 'remix/component'
import { ChevronRightIcon, ExternalLinkIcon } from '#app/ui/icons.tsx'
import {
Item,
ItemActions,import { css } from 'remix/component'
import { ChevronRightIcon, ExternalLinkIcon } from '#app/ui/icons.tsx'
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemTitle,
} from '#app/ui/item.tsx'
const stackStyles = css({
display: 'flex',
width: '100%',
maxWidth: '28rem',
flexDirection: 'column',
gap: '1rem',
})
const size4 = css({ width: '1rem', height: '1rem' })
export function ItemLink() {
return () => (
<div mix={stackStyles}>
<Item href="#">
<ItemContent>
<ItemTitle>Visit our documentation</ItemTitle>
<ItemDescription>Learn how to get started with our components.</ItemDescription>
</ItemContent>
<ItemActions>
<ChevronRightIcon mix={size4} />
</ItemActions>
</Item>
<Item variant="outline" href="#" target="_blank" rel="noopener noreferrer">
<ItemContent>
<ItemTitle>External resource</ItemTitle>
<ItemDescription>Opens in a new tab with security attributes.</ItemDescription>
</ItemContent>
<ItemActions>
<ExternalLinkIcon mix={size4} />
</ItemActions>
</Item>
</div>
)
}Dropdown
shadcn@vercel.com
maxleiter@vercel.com
evilrabbit@vercel.com
import { css } from 'remix/component'
import { Avatar, AvatarFallback, AvatarImage } from '#app/ui/avatar.tsx'
import { buttonVariants } from '#app/ui/button.tsx'
import {
DropdownMenu,import { css } from 'remix/component'
import { Avatar, AvatarFallback, AvatarImage } from '#app/ui/avatar.tsx'
import { buttonVariants } from '#app/ui/button.tsx'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '#app/ui/dropdown-menu.tsx'
import { ChevronDownIcon } from '#app/ui/icons.tsx'
import {
Item,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '#app/ui/item.tsx'
const people = [
{ username: 'shadcn', avatar: 'https://github.com/shadcn.png', email: 'shadcn@vercel.com' },
{ username: 'maxleiter', avatar: 'https://github.com/maxleiter.png', email: 'maxleiter@vercel.com' },
{
username: 'evilrabbit',
avatar: 'https://github.com/evilrabbit.png',
email: 'evilrabbit@vercel.com',
},
]
const containerStyles = css({
display: 'flex',
minHeight: '16rem',
width: '100%',
maxWidth: '28rem',
flexDirection: 'column',
alignItems: 'center',
gap: '1.5rem',
})
const triggerStyles = css({ width: 'fit-content' })
const contentStyles = css({ width: '18rem', '--radius': '0.65rem' })
const menuItemStyles = css({ padding: 0 })
const itemStyles = css({ width: '100%', padding: '0.5rem' })
const avatarStyles = css({ width: '2rem', height: '2rem' })
const grayscale = css({ filter: 'grayscale(100%)' })
const itemContentStyles = css({ gap: '0.125rem' })
export function ItemDropdown() {
return () => (
<div mix={containerStyles}>
<DropdownMenu>
<DropdownMenuTrigger
mix={[buttonVariants({ variant: 'outline', size: 'sm' }), triggerStyles]}
>
Select <ChevronDownIcon />
</DropdownMenuTrigger>
<DropdownMenuContent mix={contentStyles} align="end">
{people.map((person) => (
<DropdownMenuItem key={person.username} mix={menuItemStyles}>
<Item size="sm" mix={itemStyles}>
<ItemMedia>
<Avatar mix={avatarStyles}>
<AvatarImage src={person.avatar} mix={grayscale} />
<AvatarFallback>{person.username.charAt(0)}</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent mix={itemContentStyles}>
<ItemTitle>{person.username}</ItemTitle>
<ItemDescription>{person.email}</ItemDescription>
</ItemContent>
</Item>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}API Reference
ItemGroup
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ItemSeparator
| Prop | Type | Default |
|---|---|---|
orientation | "horizontal" | "vertical" | - |
decorative | boolean | - |
mix | MixValue | - |
Item
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
href | string | - |
target | string | - |
rel | string | - |
variant | ItemVariant | "default" |
size | ItemSize | "default" |
ItemMedia
| Prop | Type | Default |
|---|---|---|
variant | ItemMediaVariant | "default" |
mix | MixValue | - |
ItemContent
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ItemTitle
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ItemDescription
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ItemActions
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ItemHeader
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ItemFooter
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |