Accordion

可折叠列表。v0.4.0 采用嵌套语法改进(能安全地传递 Blade slot,无 XSS 风险)。在 <x-accordion> 内并排放置 <x-accordion-item title="...">content</x-accordion-item>。支持 single-open / multiple-open,3 个尺寸等级。内容可以是任意 Blade 标记、组件或链接。Alpine x-collapse 驱动平滑动画。

single-open (default) — 同一时间只展开一个

First section content.
Second section content.
Third section content.
<x-accordion>
    <x-accordion-item title="Section 1">First section content.</x-accordion-item>
    <x-accordion-item title="Section 2">Second section content.</x-accordion-item>
    <x-accordion-item title="Section 3">Third section content.</x-accordion-item>
</x-accordion>
<div x-data="{ open: null }" class="w-full divide-y divide-base-300 radius-box tune-border border-base-300 overflow-hidden">
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_17a3e39e') ? null : 'item_17a3e39e'" x-bind:aria-expanded="open === 'item_17a3e39e'"><span>Section 1</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_17a3e39e' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_17a3e39e'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-sm text-base-content/70">
                First section content.
            </div>
</div>
</div>
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_ba77fdfb') ? null : 'item_ba77fdfb'" x-bind:aria-expanded="open === 'item_ba77fdfb'"><span>Section 2</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_ba77fdfb' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_ba77fdfb'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-sm text-base-content/70">
                Second section content.
            </div>
</div>
</div>
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_2da03f3f') ? null : 'item_2da03f3f'" x-bind:aria-expanded="open === 'item_2da03f3f'"><span>Section 3</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_2da03f3f' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_2da03f3f'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-sm text-base-content/70">
                Third section content.
            </div>
</div>
</div>
</div>

multiple-open — 可同时展开多个

First section content.
Second section content.
Third section content.
<x-accordion :multiple="true">
    <x-accordion-item title="Section 1">First section content.</x-accordion-item>
    <x-accordion-item title="Section 2">Second section content.</x-accordion-item>
    <x-accordion-item title="Section 3">Third section content.</x-accordion-item>
</x-accordion>
<div x-data="{ open: [] }" class="w-full divide-y divide-base-300 radius-box tune-border border-base-300 overflow-hidden">
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open.includes('item_8e024754') ? (open = open.filter(i =&gt; i !== 'item_8e024754')) : open.push('item_8e024754')" x-bind:aria-expanded="open.includes('item_8e024754')"><span>Section 1</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open.includes('item_8e024754') &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open.includes('item_8e024754')" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-sm text-base-content/70">
                First section content.
            </div>
</div>
</div>
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open.includes('item_4181c084') ? (open = open.filter(i =&gt; i !== 'item_4181c084')) : open.push('item_4181c084')" x-bind:aria-expanded="open.includes('item_4181c084')"><span>Section 2</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open.includes('item_4181c084') &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open.includes('item_4181c084')" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-sm text-base-content/70">
                Second section content.
            </div>
</div>
</div>
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open.includes('item_d69ed48f') ? (open = open.filter(i =&gt; i !== 'item_d69ed48f')) : open.push('item_d69ed48f')" x-bind:aria-expanded="open.includes('item_d69ed48f')"><span>Section 3</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open.includes('item_d69ed48f') &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open.includes('item_d69ed48f')" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-sm text-base-content/70">
                Third section content.
            </div>
</div>
</div>
</div>

尺寸 (sm / md / lg)

Compact rows.
Second section content.
Standard rows.
Second section content.
Roomy rows.
Second section content.
<x-accordion size="sm">
    <x-accordion-item title="size=sm">Compact rows.</x-accordion-item>
    <x-accordion-item title="Section 2">Second section content.</x-accordion-item>
</x-accordion>
<x-accordion size="md">
    <x-accordion-item title="size=md (default)">Standard rows.</x-accordion-item>
    <x-accordion-item title="Section 2">Second section content.</x-accordion-item>
</x-accordion>
<x-accordion size="lg">
    <x-accordion-item title="size=lg">Roomy rows.</x-accordion-item>
    <x-accordion-item title="Section 2">Second section content.</x-accordion-item>
</x-accordion>
<div x-data="{ open: null }" class="w-full divide-y divide-base-300 radius-box tune-border border-base-300 overflow-hidden">
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_f1088ae3') ? null : 'item_f1088ae3'" x-bind:aria-expanded="open === 'item_f1088ae3'"><span>size=sm</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_f1088ae3' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_f1088ae3'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] py-sm text-base-content/70">
                Compact rows.
            </div>
</div>
</div>
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_1ddd3f7e') ? null : 'item_1ddd3f7e'" x-bind:aria-expanded="open === 'item_1ddd3f7e'"><span>Section 2</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_1ddd3f7e' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_1ddd3f7e'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] py-sm text-base-content/70">
                Second section content.
            </div>
</div>
</div>
</div>
<div x-data="{ open: null }" class="w-full divide-y divide-base-300 radius-box tune-border border-base-300 overflow-hidden">
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_d74f32b5') ? null : 'item_d74f32b5'" x-bind:aria-expanded="open === 'item_d74f32b5'"><span>size=md (default)</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_d74f32b5' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_d74f32b5'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-sm text-base-content/70">
                Standard rows.
            </div>
</div>
</div>
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_01bd1478') ? null : 'item_01bd1478'" x-bind:aria-expanded="open === 'item_01bd1478'"><span>Section 2</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_01bd1478' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_01bd1478'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-sm text-base-content/70">
                Second section content.
            </div>
</div>
</div>
</div>
<div x-data="{ open: null }" class="w-full divide-y divide-base-300 radius-box tune-border border-base-300 overflow-hidden">
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-lg)] px-[var(--px-field-lg)] text-[length:var(--text-field-lg)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_9f33398c') ? null : 'item_9f33398c'" x-bind:aria-expanded="open === 'item_9f33398c'"><span>size=lg</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_9f33398c' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_9f33398c'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-lg)] px-[var(--px-field-lg)] text-[length:var(--text-field-lg)] py-sm text-base-content/70">
                Roomy rows.
            </div>
</div>
</div>
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-lg)] px-[var(--px-field-lg)] text-[length:var(--text-field-lg)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_05ac7b68') ? null : 'item_05ac7b68'" x-bind:aria-expanded="open === 'item_05ac7b68'"><span>Section 2</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_05ac7b68' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_05ac7b68'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-lg)] px-[var(--px-field-lg)] text-[length:var(--text-field-lg)] py-sm text-base-content/70">
                Second section content.
            </div>
</div>
</div>
</div>

富内容 — 由于是 Blade slot,任意 markup / 组件 / 链接 / 列表、多段落都可以自然书写

Content containing a link to an external resource. See the official docs.

  • Bullet 1
  • Bullet 2
  • Bullet 3

The body is a Blade slot, so any HTML or Blade component can be embedded.

A second paragraph. Line breaks and styling are free-form.

<x-accordion>
    <x-accordion-item title="With a link">
        <p>Content containing a link to an external resource. See the <a href="#" class="text-primary underline underline-offset-2 hover:opacity-80">official docs</a>.</p>
    </x-accordion-item>
    <x-accordion-item title="With a list">
        <ul class="list-disc list-inside space-y-1">
            <li>Bullet 1</li>
            <li>Bullet 2</li>
            <li>Bullet 3</li>
        </ul>
    </x-accordion-item>
    <x-accordion-item title="Multiple paragraphs">
        <p class="mb-2">The body is a Blade slot, so any HTML or Blade component can be embedded.</p>
        <p>A second paragraph. Line breaks and styling are free-form.</p>
    </x-accordion-item>
</x-accordion>
<div x-data="{ open: null }" class="w-full divide-y divide-base-300 radius-box tune-border border-base-300 overflow-hidden">
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_3367558b') ? null : 'item_3367558b'" x-bind:aria-expanded="open === 'item_3367558b'"><span>With a link</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_3367558b' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_3367558b'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-sm text-base-content/70">
<p>
                    Content containing a link to an external resource. See the <a href="#" class="text-primary underline underline-offset-2 hover:opacity-80">official docs</a>.
                </p>
</div>
</div>
</div>
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_f065fad0') ? null : 'item_f065fad0'" x-bind:aria-expanded="open === 'item_f065fad0'"><span>With a list</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_f065fad0' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_f065fad0'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-sm text-base-content/70">
<ul class="list-disc list-inside space-y-1">
<li>Bullet 1
                    </li>
<li>Bullet 2
                    </li>
<li>Bullet 3
                    </li>
</ul>
</div>
</div>
</div>
<div>
<button type="button" class="w-full flex items-center justify-between min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-0 font-medium text-base-content hover:bg-base-200/50 transition-colors cursor-pointer" x-on:click="open = (open === 'item_e8836bdb') ? null : 'item_e8836bdb'" x-bind:aria-expanded="open === 'item_e8836bdb'"><span>Multiple paragraphs</span>
<svg class="w-4 h-4 shrink-0 transition-transform duration-200" x-bind:class="open === 'item_e8836bdb' &amp;&amp; 'rotate-180'" fill="none" viewbox="0 0 24 24" stroke="currentcolor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path></svg></button>
<div x-show="open === 'item_e8836bdb'" x-collapse="" x-cloak="">
<div class="min-h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] py-sm text-base-content/70">
<p class="mb-2">
                    The body is a Blade slot, so any HTML or Blade component can be embedded.
                </p>
<p>
                    A second paragraph. Line breaks and styling are free-form.
                </p>
</div>
</div>
</div>
</div>

FAQ 演示 — 实际使用场景 (single-open)

基于 Tailwind v4 + daisyUI v5 + Alpine.js 打造的 Yakaze Tech Studio 内部 Blade 组件库,旨在保证跨项目的 UI 一致性。
先执行 composer require sparrowhawk-labs/pinion-ui,然后执行 php artisan ui:install 配置所需资源。
可以。pinion-ui 内部使用了 daisyUI 的 class,因此共享同一套 theme variable。<x-...> 与常规 daisyUI markup 混用也没有问题。
请使用 daisyUI v5 的 @plugin "daisyui/theme" 指令定义主题。pinion-ui 的 class 全部通过 theme variable,只需切换主题配色就会自动跟随。
请通过 GitHub Issues 反馈。附上复现步骤和截图会很有帮助。sparrowhawk-labs/pinion-ui#issues

FAQ (multiple-open) — 想同时查看多个比较项时

基于 Tailwind v4 + daisyUI v5 + Alpine.js 的 Blade 组件库。
composer requirephp artisan ui:install
自 v0.4.0 起仅支持 light (pinion)。如需 dark,请在 <html data-theme> 上指定 daisyUI 标准的 dark / dim / night