Aller au contenu

Élément

Une ligne flexible comportant des emplacements pour les médias, le titre, la description et l'action, destinée aux flux de notifications, aux listes de paramètres et aux lignes de fichiers — dans un style néobrutaliste.

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

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

L’item est une ligne flexible à emplacements nommés — média, contenu, actions — pour le motif « liste de choses » que toute application finit par adopter. Aucune primitive headless en dessous, seulement des éléments stylés (plus le useRender de Base UI pour la prop render), finis selon la recette néobrutaliste : une bordure de 2px — visible sur la variante outline — et un anneau de focus décalé bien marqué.

À privilégier pour :

  • Fils de notifications et d’activité — avatar, message, horodatage, et un bouton d’action ou de fermeture par ligne.
  • Listes de réglages et d’intégrations — icône, nom, ligne d’état, avec un interrupteur ou un bouton en fin de ligne.
  • Explorateurs de fichiers et résultats de recherche — vignette ou icône plus métadonnées, la ligne entière devenant un lien via render.

Le composant Item est un simple conteneur flex capable d’accueillir presque n’importe quel type de contenu. Utilisez-le pour afficher un titre, une description et des actions. Regroupez-le avec le composant ItemGroup pour créer une liste d’éléments.

Installation

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

Copiez-collez le code suivant dans votre projet.

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,
}

Adaptez les chemins d’import à la structure de votre projet.

Utilisation

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>

Composition

Utilisez la composition suivante pour construire un Item :

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

Item vs Field

Utilisez Field si vous devez afficher un champ de formulaire tel qu’une case à cocher, un champ de saisie, un bouton radio ou un select.

Si vous n’avez besoin d’afficher que du contenu comme un titre, une description et des actions, utilisez Item.

Variante

Utilisez la prop variant pour modifier le style visuel de l’élément.

import { InboxIcon } from "lucide-react"

import {

Taille

Utilisez la prop size pour modifier la taille de l’élément. Les tailles disponibles sont default, sm et xs.

import { InboxIcon } from "lucide-react"

import {

Exemples

Icône

Utilisez ItemMedia avec variant="icon" pour afficher une icône.

import { ShieldAlertIcon } from "lucide-react"

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

Avatar

Vous pouvez utiliser ItemMedia avec variant="avatar" pour afficher un avatar.

import { Plus } from "lucide-react"

import {

Image

Utilisez ItemMedia avec variant="image" pour afficher une image.

import Image from "next/image"

import {

Groupe

Utilisez ItemGroup pour regrouper des éléments liés.

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

En-tête

Utilisez ItemHeader pour ajouter un en-tête au-dessus du contenu de l’élément.

import Image from "next/image"

import {

Lien

Utilisez la prop render pour afficher l’élément sous forme de lien. Les états de survol et de focus s’appliquent à l’élément d’ancrage.

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

Pour activer le RTL dans Neobrutalism, consultez le guide de configuration RTL.

"use client"

import * as React from "react"

Accessibilité

L’item est présentationnel — des éléments stylés sans câblage ARIA propre, même si ItemGroup rend role="list" (ajoutez role="listitem" à chaque Item si la structure de liste doit être annoncée). Pour les lignes interactives, ne posez pas de gestionnaire de clic sur la div — rendez un vrai <a> ou <button> via la prop render afin qu’il profite du focus et du comportement clavier natifs ; l’anneau de focus visible est déjà stylé. Marquez aria-hidden les icônes décoratives et donnez aux images un texte alt significatif.

Référence API

Item

Le composant principal pour afficher du contenu avec média, titre, description et actions.

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

ItemGroup

Un conteneur qui regroupe des éléments liés avec un style cohérent.

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

ItemSeparator

Un séparateur entre les éléments d’un groupe.

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

ItemMedia

Utilisez ItemMedia pour afficher du contenu média tel que des icônes, des images ou des avatars.

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

ItemContent

Enveloppe le titre et la description de l’élément.

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

ItemTitle

Affiche le titre de l’élément.

<ItemTitle>Item Title</ItemTitle>

ItemDescription

Affiche la description de l’élément.

<ItemDescription>Item description</ItemDescription>

ItemActions

Conteneur pour les boutons d’action ou d’autres éléments interactifs.

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

ItemHeader

Affiche un en-tête au-dessus du contenu de l’élément.

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

ItemFooter

Affiche un pied sous le contenu de l’élément.

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