A versatile component for displaying content with media, title, description, and actions.
1import { Button } from "@/components/ui/button";2import {3 Item,4 ItemActions,Installation
pnpm dlx sprawlify@latest add itemnpx sprawlify@latest add itemyarn sprawlify@latest add itembunx --bun sprawlify@latest add item
Install the following dependencies:
pnpm add @sprawlify/primitives @sprawlify/solidnpm install @sprawlify/primitives @sprawlify/solidyarn add @sprawlify/primitives @sprawlify/solidbun add @sprawlify/primitives @sprawlify/solid
Add the following files to your project:
1import { cva, type VariantProps } from "class-variance-authority";2import { cn } from "@/lib/utils";3import { Separator } from "@/components/ui/separator";4import { sprawlify } from "@sprawlify/solid";5import type { ComponentProps } from "solid-js";6import { splitProps } from "solid-js";78function ItemGroup(props: ComponentProps<typeof sprawlify.div>) {9 const [local, others] = splitProps(props, ["class"]);1011 return (12 <sprawlify.div13 role="list"14 data-scope="item"15 data-part="group"16 data-slot="item-group"17 class={cn(18 "gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2 group/item-group flex w-full flex-col",19 local.class,20 )}21 {...others}22 />23 );24}2526function ItemSeparator(props: ComponentProps<typeof Separator>) {27 const [local, others] = splitProps(props, ["class"]);2829 return (30 <Separator31 data-scope="item"32 data-part="separator"33 data-slot="item-separator"34 orientation="horizontal"35 class={cn("my-2", local.class)}36 {...others}37 />38 );39}4041const itemVariants = cva(42 "[a]:hover:bg-muted rounded-lg border text-sm w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors",43 {44 variants: {45 variant: {46 default: "border-transparent",47 outline: "border-border",48 muted: "bg-muted/50 border-transparent",49 },50 size: {51 default: "gap-2.5 px-3 py-2.5",52 sm: "gap-2.5 px-3 py-2.5",53 xs: "gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0",54 },55 },56 defaultVariants: {57 variant: "default",58 size: "default",59 },60 },61);6263function Item(props: ComponentProps<typeof sprawlify.div> & VariantProps<typeof itemVariants>) {64 const [local, others] = splitProps(props, ["variant", "size", "class"]);65 const variant = () => local.variant ?? "default";66 const size = () => local.size ?? "default";6768 return (69 <sprawlify.div70 data-scope="item"71 data-part="root"72 data-slot="item"73 data-variant={variant()}74 data-size={size()}75 class={cn(itemVariants({ variant: variant(), size: size(), class: local.class }))}76 {...others}77 />78 );79}8081const itemMediaVariants = cva(82 "gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none",83 {84 variants: {85 variant: {86 default: "bg-transparent",87 icon: "[&_svg:not([class*='size-'])]:size-4",88 image:89 "size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover",90 },91 },92 defaultVariants: {93 variant: "default",94 },95 },96);9798function ItemMedia(99 props: ComponentProps<typeof sprawlify.div> & VariantProps<typeof itemMediaVariants>,100) {101 const [local, others] = splitProps(props, ["variant", "class"]);102 const variant = () => local.variant ?? "default";103104 return (105 <sprawlify.div106 data-scope="item"107 data-part="media"108 data-slot="item-media"109 data-variant={variant()}110 class={cn(itemMediaVariants({ variant: variant(), class: local.class }))}111 {...others}112 />113 );114}115116function ItemContent(props: ComponentProps<typeof sprawlify.div>) {117 const [local, others] = splitProps(props, ["class"]);118119 return (120 <sprawlify.div121 data-scope="item"122 data-part="content"123 data-slot="item-content"124 class={cn(125 "gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none",126 local.class,127 )}128 {...others}129 />130 );131}132133function ItemTitle(props: ComponentProps<typeof sprawlify.div>) {134 const [local, others] = splitProps(props, ["class"]);135136 return (137 <sprawlify.div138 data-scope="item"139 data-part="title"140 data-slot="item-title"141 class={cn(142 "gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center",143 local.class,144 )}145 {...others}146 />147 );148}149150function ItemDescription(props: ComponentProps<typeof sprawlify.p>) {151 const [local, others] = splitProps(props, ["class"]);152153 return (154 <sprawlify.p155 data-scope="item"156 data-part="description"157 data-slot="item-description"158 class={cn(159 "text-muted-foreground text-left text-sm leading-normal group-data-[size=xs]/item:text-xs [&>a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4",160 local.class,161 )}162 {...others}163 />164 );165}166167function ItemActions(props: ComponentProps<typeof sprawlify.div>) {168 const [local, others] = splitProps(props, ["class"]);169170 return (171 <sprawlify.div172 data-scope="item"173 data-part="actions"174 data-slot="item-actions"175 class={cn("gap-2 flex items-center", local.class)}176 {...others}177 />178 );179}180181function ItemHeader(props: ComponentProps<typeof sprawlify.div>) {182 const [local, others] = splitProps(props, ["class"]);183184 return (185 <sprawlify.div186 data-scope="item"187 data-part="header"188 data-slot="item-header"189 class={cn("gap-2 flex basis-full items-center justify-between", local.class)}190 {...others}191 />192 );193}194195function ItemFooter(props: ComponentProps<typeof sprawlify.div>) {196 const [local, others] = splitProps(props, ["class"]);197198 return (199 <sprawlify.div200 data-scope="item"201 data-part="footer"202 data-slot="item-footer"203 class={cn("gap-2 flex basis-full items-center justify-between", local.class)}204 {...others}205 />206 );207}208209export {210 Item,211 ItemMedia,212 ItemContent,213 ItemActions,214 ItemGroup,215 ItemSeparator,216 ItemTitle,217 ItemDescription,218 ItemHeader,219 ItemFooter,220};221Update the import paths to match your project setup.
Usage
1import {
2 ItemGroup,
3 ItemSeparator,
4 Item,
5 ItemMedia,
6 ItemContent,
7 ItemTitle,
8 ItemDescription,
9 ItemAction
10} from "@/components/ui/item"1<Item>
2 <ItemMedia variant="icon">
3 <Icon />
4 </ItemMedia>
5 <ItemContent>
6 <ItemTitle>Title</ItemTitle>
7 <ItemDescription>Description</ItemDescription>
8 </ItemContent>
9 <ItemActions>
10 <Button>Action</Button>
11 </ItemActions>
12</Item>Examples
Avatar
You can use ItemMedia with variant="avatar" to display an avatar.
1import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";2import { Button } from "@/components/ui/button";3import {4 Item,Group
Use ItemGroup to group related items together.
1import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";2import { Button } from "@/components/ui/button";3import {4 Item,Header
Use ItemHeader to add a header above the item content.
1import {2 Item,3 ItemContent,4 ItemDescription,Icon
Use ItemMedia with variant="icon" to display an icon.
1import { Button } from "@/components/ui/button";2import {3 Item,4 ItemActions,Image
Use ItemMedia with variant="image" to display an image.
1import {2 Item,3 ItemContent,4 ItemDescription,Link
Use the asChild prop to render the item as a link. The hover and focus states will be applied to the anchor element.
1import { Item, ItemActions, ItemContent, ItemDescription, ItemTitle } from "@/components/ui/item";2import { ChevronRightIcon, ExternalLinkIcon } from "lucide-solid";34export default function Preview() {