コンテンツへスキップ

アイテム

通知フィード、設定リスト、ファイル行向けに、メディア、タイトル、説明、アクションのスロットを備えたフレックス行――ネオブルータリズム風のデザインです。

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

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

Item は、メディア・コンテンツ・アクションの名前付きスロットを持つ flex 行で、どのアプリにも出てくる「ものの一覧」パターン向けです。下層にヘッドレスプリミティブはなく、スタイル付き要素だけです(render prop 用に Base UI の useRender を使用)。ネオブルータリストの仕上げで、2px の枠線(outline バリアントで見える)と硬いオフセットのフォーカスリングです。

次のような場面で使います:

  • 通知とアクティビティフィード — 行ごとにアバター、メッセージ、タイムスタンプ、却下またはアクションボタン。
  • 設定と連携一覧 — アイコン、名前、状態行と、末尾のスイッチやボタン。
  • ファイルブラウザと検索結果 — サムネイルやアイコンにメタデータ。render で行全体をリンクに。

Item コンポーネントは、ほぼあらゆる種類のコンテンツを収められる、シンプルな flex コンテナです。タイトル、説明、アクションの表示に使用します。ItemGroup コンポーネントでまとめると、項目のリストを作成できます。

インストール

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

以下のコードをコピーして、プロジェクトに貼り付けます。

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

インポートのパスをプロジェクトの構成に合わせて更新します。

使い方

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>

コンポジション

Item を構築するには、次のコンポジションを使用します。

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

Item vs Field

チェックボックス、入力欄、ラジオ、セレクトなどのフォーム入力を表示する必要がある場合は、Field を使用します。

タイトル、説明、アクションなどのコンテンツを表示するだけでよい場合は、Item を使用します。

バリアント

variant プロパティを使って、項目の見た目のスタイルを変更します。

import { InboxIcon } from "lucide-react"

import {

サイズ

size プロパティを使って、項目のサイズを変更します。指定できるサイズは defaultsmxs です。

import { InboxIcon } from "lucide-react"

import {

使用例

アイコン

アイコンを表示するには、ItemMediavariant="icon" を指定します。

import { ShieldAlertIcon } from "lucide-react"

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

アバター

アバターを表示するには、ItemMediavariant="avatar" を指定します。

import { Plus } from "lucide-react"

import {

画像

画像を表示するには、ItemMediavariant="image" を指定します。

import Image from "next/image"

import {

グループ

関連する項目をまとめるには、ItemGroup を使用します。

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

ヘッダー

項目のコンテンツの上にヘッダーを追加するには、ItemHeader を使用します。

import Image from "next/image"

import {

リンク

render プロパティを使って、項目をリンクとしてレンダリングします。ホバーとフォーカスの状態は、アンカー要素に適用されます。

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

Neobrutalism で RTL を有効にする方法は、RTL 設定ガイドをご覧ください。

"use client"

import * as React from "react"

アクセシビリティ

Item は見た目専用です。スタイル付き要素に独自の ARIA 配線はありません。ただし ItemGrouprole="list" を描画します(リスト構造を読み上げたい場合は各 Itemrole="listitem" を追加)。インタラクティブな行では div にクリックハンドラを付けず、render prop で本物の <a> または <button> を描画し、ネイティブのフォーカスとキーボード挙動を得てください。見えるフォーカスリングは既にスタイル済みです。装飾用のアイコンメディアには aria-hidden、画像メディアには意味のある alt を付けてください。

APIリファレンス

Item

メディア、タイトル、説明、アクションを備えたコンテンツを表示するためのメインコンポーネントです。

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

ItemGroup

関連する項目を一貫したスタイルでまとめるコンテナです。

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

ItemSeparator

グループ内の項目の間に配置する区切りです。

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

ItemMedia

アイコン、画像、アバターなどのメディアコンテンツを表示するには、ItemMedia を使用します。

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

ItemContent

項目のタイトルと説明をラップします。

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

ItemTitle

項目のタイトルを表示します。

<ItemTitle>Item Title</ItemTitle>

ItemDescription

項目の説明を表示します。

<ItemDescription>Item description</ItemDescription>

ItemActions

アクションボタンやその他の操作可能な要素のためのコンテナです。

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

ItemHeader

項目のコンテンツの上にヘッダーを表示します。

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

ItemFooter

項目のコンテンツの下にフッターを表示します。

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