HextaUI

Accordion

それぞれがパネルを表示する見出しの積み重ねです。途中で反転できる高さのモーションを持ち、閉じている間もパネルの内容を検索できます。

A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionDemo() {
  return (
    <Accordion defaultValue={["what"]} className="w-full max-w-md">
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/accordion.json

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"
<Accordion defaultValue={["item-1"]}>
  <AccordionItem value="item-1">
    <AccordionTrigger>Is it accessible?</AccordionTrigger>
    <AccordionContent>
      Yes. It follows the WAI-ARIA accordion pattern.
    </AccordionContent>
  </AccordionItem>
</Accordion>
Accordion
└── AccordionItem
    ├── AccordionTrigger
    └── AccordionContent

アウトライン

項目は枠線付きの 1 つの面を共有し、外側の角が丸くなります。

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionOutline() {
  return (
    <Accordion
      variant="outline"
      defaultValue={["what"]}
      className="w-full max-w-md"
    >
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}

Separated

各項目が独立したカードになります。

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionSeparated() {
  return (
    <Accordion
      variant="separated"
      defaultValue={["what"]}
      className="w-full max-w-md"
    >
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}

Ghost

枠線なし。トリガーはホバー時と開いているときに、淡い塗りが付きます。

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionGhost() {
  return (
    <Accordion
      variant="ghost"
      defaultValue={["what"]}
      className="w-full max-w-md"
    >
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}

複数

multiple を設定すると、複数の項目を開いたままにできます。

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionMultiple() {
  return (
    <Accordion
      variant="outline"
      multiple
      defaultValue={["what", "install"]}
      className="w-full max-w-md"
    >
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}

無効

個別の項目は <AccordionItem /> の disabled で、アコーディオン全体はルートで無効にできます。

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionDisabled() {
  return (
    <Accordion variant="outline" className="w-full max-w-md">
      {items.map((item) => (
        <AccordionItem
          key={item.value}
          value={item.value}
          disabled={item.value === "install"}
        >
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}

制御

開いている項目を独自の state で管理するには、value と onValueChange を渡します。

"use client"

import * as React from "react"

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"
import { Button } from "@/components/ui/button"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionControlled() {
  const [value, setValue] = React.useState<string[]>(["what"])

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <div className="flex gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => setValue(items.map((item) => item.value))}
        >
          Open all
        </Button>
        <Button variant="outline" size="sm" onClick={() => setValue([])}>
          Close all
        </Button>
      </div>
      <Accordion
        variant="outline"
        multiple
        value={value}
        onValueChange={setValue}
      >
        {items.map((item) => (
          <AccordionItem key={item.value} value={item.value}>
            <AccordionTrigger>{item.question}</AccordionTrigger>
            <AccordionContent>{item.answer}</AccordionContent>
          </AccordionItem>
        ))}
      </Accordion>
    </div>
  )
}

カスタムアイコン

icon には任意の要素を渡せます。開いたときのスタイルは group-data-panel-open/accordion-trigger で指定します。

import { IconPlus } from "@tabler/icons-react"

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionCustomIcon() {
  return (
    <Accordion variant="ghost" className="w-full max-w-md">
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger
            icon={
              <IconPlus className="transition-transform duration-200 ease-out-cubic group-data-panel-open/accordion-trigger:rotate-45 motion-reduce:transition-none" />
            }
          >
            {item.question}
          </AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}

先頭のアイコン

ラベルの前に置くアイコンは、サイズと位置が自動で調整されます。icon={null} を指定すると山形アイコンが削除されます。

import { IconLock, IconUser } from "@tabler/icons-react"

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function AccordionLeadingIcons() {
  return (
    <Accordion variant="outline" className="w-full max-w-md">
      <AccordionItem value="account">
        <AccordionTrigger icon={null}>
          <IconUser />
          Account
        </AccordionTrigger>
        <AccordionContent>Name, email and avatar.</AccordionContent>
      </AccordionItem>
      <AccordionItem value="security">
        <AccordionTrigger icon={null}>
          <IconLock />
          Security
        </AccordionTrigger>
        <AccordionContent>Password and two-factor settings.</AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}

長いコンテンツ

長いタイトルは折り返されますが、アイコンは 1 行目に残ります。閉じたパネルも検索対象のままなので、Cmd/Ctrl+F で「retention」を検索するとこのパネルが開きます。

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function AccordionLongContent() {
  return (
    <Accordion className="w-full max-w-md">
      <AccordionItem value="long">
        <AccordionTrigger>
          What happens to my data if I cancel my subscription halfway through a
          billing period and later decide to come back?
        </AccordionTrigger>
        <AccordionContent>
          <p>
            Your workspace is kept for 30 days after cancellation. During that
            time you can reactivate and everything is restored exactly as you
            left it.
          </p>
          <p>
            After the retention window, data is permanently deleted. Read the{" "}
            <a href="#">data policy</a> for details.
          </p>
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}

入れ子

内側のアコーディオンは、独自の variant、フォーカススタイル、矢印キーによる操作を保持します。

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionNested() {
  return (
    <Accordion variant="separated" className="w-full max-w-md">
      <AccordionItem value="billing">
        <AccordionTrigger>Billing</AccordionTrigger>
        <AccordionContent>
          <Accordion variant="ghost">
            {items.map((item) => (
              <AccordionItem key={item.value} value={item.value}>
                <AccordionTrigger>{item.question}</AccordionTrigger>
                <AccordionContent>{item.answer}</AccordionContent>
              </AccordionItem>
            ))}
          </Accordion>
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="shipping">
        <AccordionTrigger>Shipping</AccordionTrigger>
        <AccordionContent>Ships within 2 business days.</AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}

右から左

コンテンツ、アイコン、開くときのモーションは、読む方向に従います。

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "ما هي HextaUI؟",
    answer: "مجموعة مكونات مبنية على shadcn/ui مع اهتمام بالتفاصيل والحركة.",
  },
  {
    value: "install",
    question: "كيف أثبّت مكوّنًا؟",
    answer: "انسخ الشيفرة إلى مشروعك وعدّلها كأي ملف تملكه.",
  },
  {
    value: "license",
    question: "هل يمكنني استخدامها في مشاريع تجارية؟",
    answer: "نعم، جميع المكونات مجانية ومفتوحة المصدر.",
  },
]

export function AccordionRtl() {
  return (
    <div dir="rtl" className="w-full max-w-md">
      <Accordion variant="outline" defaultValue={["what"]}>
        {items.map((item) => (
          <AccordionItem key={item.value} value={item.value}>
            <AccordionTrigger>{item.question}</AccordionTrigger>
            <AccordionContent>{item.answer}</AccordionContent>
          </AccordionItem>
        ))}
      </Accordion>
    </div>
  )
}
キーアクション
EnterSpaceフォーカス中の項目を開閉します。
↓フォーカスを次のトリガーに移動します。
↑フォーカスを前のトリガーに移動します。
Homeフォーカスを最初のトリガーに移動します。
Endフォーカスを最後のトリガーに移動します。
Tabフォーカスを開いたパネルの中へ移し、その後は次のトリガーへ移動します。

Base UI の accordion をベースにしています。各パーツは、ラップしているプリミティブの props をすべて受け付けます。

プロパティ型デフォルト
variant
"default" | "outline" | "separated" | "ghost""default"
multiple複数の項目を同時に開けるようにします。
booleanfalse
value
Value[]–
defaultValue
Value[]–
onValueChange
(value: Value[], details) => void–
disabled
booleanfalse
hiddenUntilFound閉じたパネルも、ブラウザのページ内検索で見つけられるようにします。
booleantrue
keepMountedhiddenUntilFound が有効な間は無視されます。
booleanfalse
render
ReactElement | (props, state) => ReactElement<div>
属性説明
data-slot="accordion"CSSでルートを指定します。
data-variant現在のバリアント。
data-disabledアコーディオンが無効なときに付与されます。
プロパティ型デフォルト
value省略すると自動生成されます。
Value–
disabled
booleanfalse
onOpenChange
(open: boolean, details) => void–
render
ReactElement | (props, state) => ReactElement<div>
属性説明
data-slot="accordion-item"CSSで項目を指定します。
data-open項目が開いているときに付与されます。
data-disabled項目が無効のときに存在します。
data-index項目の位置。0 から始まります。
プロパティ型デフォルト
iconnull を指定するとアイコンが削除されます。
ReactNode | null<IconChevronDown />
render
ReactElement | (props, state) => ReactElement<button>
属性説明
data-slot="accordion-trigger"CSSでトリガーを指定します。
data-panel-openパネルが開いているときに付与されます。アイコンのスタイルは group-data-panel-open/accordion-trigger で指定します。
data-disabled項目が無効のときに存在します。
プロパティ型デフォルト
className内側のラッパーに適用されるため、パディングが高さのアニメーションと干渉しません。
string–
keepMounted
booleanfalse
hiddenUntilFound
boolean–
属性説明
data-slot="accordion-content"CSS でパネルを指定します。
data-openパネルが開いているときに付与されます。
data-starting-styleパネルが開くアニメーションの間、付与されます。
data-ending-styleパネルが閉じるアニメーションの間、付与されます。
data-settledパネルが完全に開いた時点で付与されます。このときクリッピングが解除されるため、ネストしたアコーディオン内のフォーカスリングが全体表示されます。
--accordion-panel-height高さアニメーションに使われる、パネルの計測された高さ。