Saltar al contenido

Elemento

Una fila flexible con espacios para medios, título, descripción y acción, pensada para feeds de notificaciones, listas de configuración y filas de archivos, con un estilo neobrutalista.

import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

El item es una fila flex con áreas con nombre —multimedia, contenido, acciones— para el patrón de «lista de cosas» con el que acaba toda app. No hay ninguna primitiva headless debajo, solo elementos estilizados (más el useRender de Base UI para la prop render), rematados con la receta neobrutalista: un borde de 2px —visible en la variante outline— y un anillo de foco con desplazamiento marcado.

Recurre a este componente cuando:

  • Feeds de notificaciones y actividad — avatar, mensaje, marca de tiempo y un botón de descartar o de acción por fila.
  • Listas de ajustes e integraciones — icono, nombre y línea de estado, con un switch o un botón en el borde final.
  • Exploradores de archivos y resultados de búsqueda — miniatura o icono más metadatos, con la fila entera como enlace mediante render.

El componente Item es un contenedor flex sencillo capaz de alojar casi cualquier tipo de contenido. Úsalo para mostrar un título, una descripción y acciones. Agrúpalo con el componente ItemGroup para crear una lista de elementos.

Instalación

pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/item.json
npx shadcn@latest add https://neobrutalism.com/r/base/item.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/item.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/item.json

Copia y pega el siguiente código en tu proyecto.

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 w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2",
        className
      )}
      {...props}
    />
  )
}

function ItemSeparator({
  className,
  ...props
}: React.ComponentProps<typeof Separator>) {
  return (
    <Separator
      data-slot="item-separator"
      orientation="horizontal"
      className={cn("my-2", className)}
      {...props}
    />
  )
}

const itemVariants = cva(
  "group/item flex w-full flex-wrap items-center rounded border-2 text-sm transition-colors duration-100 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary [a]:transition-colors [a]:hover:bg-accent",
  {
    variants: {
      variant: {
        default: "border-transparent",
        outline: "border-border",
        muted: "border-transparent bg-muted/50",
      },
      size: {
        default: "gap-2.5 px-3 py-2.5",
        sm: "gap-2.5 px-3 py-2.5",
        xs: "gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0",
      },
    },
    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: "[&_svg:not([class*='size-'])]:size-4",
        image:
          "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",
      },
    },
    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 flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none",
        className
      )}
      {...props}
    />
  )
}

function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="item-title"
      className={cn(
        "line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4",
        className
      )}
      {...props}
    />
  )
}

function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="item-description"
      className={cn(
        "line-clamp-2 text-left text-sm leading-normal font-normal text-muted-foreground group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
        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",
        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,
}

Actualiza las rutas de importación según la configuración de tu proyecto.

Uso

import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemMedia,
  ItemTitle,
} from "@/components/ui/item"
<Item>
  <ItemMedia variant="icon">
    <Icon />
  </ItemMedia>
  <ItemContent>
    <ItemTitle>Title</ItemTitle>
    <ItemDescription>Description</ItemDescription>
  </ItemContent>
  <ItemActions>
    <Button>Action</Button>
  </ItemActions>
</Item>

Composición

Usa la siguiente composición para construir un Item:

ItemGroup
└── Item
    ├── ItemHeader
    ├── ItemMedia
    ├── ItemContent
    │   ├── ItemTitle
    │   └── ItemDescription
    ├── ItemActions
    └── ItemFooter

Item vs Field

Usa Field si necesitas mostrar un campo de formulario como una casilla de verificación, un campo de entrada, un botón de opción o un select.

Si solo necesitas mostrar contenido como un título, una descripción y acciones, usa Item.

Variant

Usa la prop variant para cambiar el estilo visual del elemento.

import { InboxIcon } from "lucide-react"

import {

Size

Usa la prop size para cambiar el tamaño del elemento. Los tamaños disponibles son default, sm y xs.

import { InboxIcon } from "lucide-react"

import {

Ejemplos

Icono

Usa ItemMedia con variant="icon" para mostrar un icono.

import { ShieldAlertIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

Avatar

Puedes usar ItemMedia con variant="avatar" para mostrar un avatar.

import { Plus } from "lucide-react"

import {

Imagen

Usa ItemMedia con variant="image" para mostrar una imagen.

import Image from "next/image"

import {

Grupo

Usa ItemGroup para agrupar elementos relacionados.

import * as React from "react"
import { PlusIcon } from "lucide-react"

Encabezado

Usa ItemHeader para añadir un encabezado encima del contenido del elemento.

import Image from "next/image"

import {

Enlace

Usa la prop render para renderizar el elemento como un enlace. Los estados de hover y foco se aplican al elemento de anclaje.

import { ChevronRightIcon, ExternalLinkIcon } from "lucide-react"

import {
<Item render={<a href="/dashboard" />}>
  <ItemMedia variant="icon">
    <HomeIcon />
  </ItemMedia>
  <ItemContent>
    <ItemTitle>Dashboard</ItemTitle>
    <ItemDescription>Overview of your account and activity.</ItemDescription>
  </ItemContent>
</Item>
"use client"

import { ChevronDownIcon } from "lucide-react"

RTL

Para activar el soporte RTL en Neobrutalism, consulta la guía de configuración de RTL.

"use client"

import * as React from "react"

Accesibilidad

El item es presentacional: elementos estilizados sin configuración ARIA propia, aunque ItemGroup renderiza role="list" (añade role="listitem" a cada Item si la estructura de lista debe anunciarse). Para las filas interactivas, no pongas un manejador de clic en el div: renderiza un <a> o un <button> real mediante la prop render para que reciba el foco y el comportamiento de teclado nativos; el anillo de foco visible ya viene estilizado. Marca con aria-hidden los iconos decorativos y da a las imágenes un texto alt con significado.

Referencia de API

Item

El componente principal para mostrar contenido con medios, título, descripción y acciones.

PropTypeDefault
variant"default" | "outline" | "muted""default"
size"default" | "sm" | "xs""default"
renderReact.ReactElement

ItemGroup

Un contenedor que agrupa elementos relacionados con un estilo coherente.

<ItemGroup>
  <Item />
  <Item />
</ItemGroup>

ItemSeparator

Un separador entre los elementos de un grupo.

<ItemGroup>
  <Item />
  <ItemSeparator />
  <Item />
</ItemGroup>

ItemMedia

Usa ItemMedia para mostrar contenido multimedia como iconos, imágenes o avatares.

PropTypeDefault
variant"default" | "icon" | "image""default"
<ItemMedia variant="icon">
  <Icon />
</ItemMedia>
<ItemMedia variant="image">
  <img src="..." alt="..." />
</ItemMedia>

ItemContent

Envuelve el título y la descripción del elemento.

<ItemContent>
  <ItemTitle>Title</ItemTitle>
  <ItemDescription>Description</ItemDescription>
</ItemContent>

ItemTitle

Muestra el título del elemento.

<ItemTitle>Item Title</ItemTitle>

ItemDescription

Muestra la descripción del elemento.

<ItemDescription>Item description</ItemDescription>

ItemActions

Contenedor para los botones de acción u otros elementos interactivos.

<ItemActions>
  <Button>Action</Button>
</ItemActions>

ItemHeader

Muestra un encabezado encima del contenido del elemento.

<Item>
  <ItemHeader>Header</ItemHeader>
  <ItemContent>...</ItemContent>
</Item>

ItemFooter

Muestra un pie debajo del contenido del elemento.

<Item>
  <ItemContent>...</ItemContent>
  <ItemFooter>Footer</ItemFooter>
</Item>