来源：https://ui.docs.xihanfun.com/components/accordion

# Accordion 手风琴

一列可展开的区块，标题常驻，内容按需展开。

<div class="xh-resource-links">
  <a href="https://github.com/XiHanFun/XiHan.UI/tree/dev/ui/packages/engine/headless/src/accordion" target="_blank" rel="noreferrer">Headless</a>
  <a href="https://github.com/XiHanFun/XiHan.UI/blob/dev/ui/packages/design/styles/css/accordion.css" target="_blank" rel="noreferrer">Styles</a>
  <a href="https://github.com/XiHanFun/XiHan.UI/tree/dev/ui/packages/adapters/vue/src/components/accordion" target="_blank" rel="noreferrer">Vue</a>
  <a href="https://github.com/XiHanFun/XiHan.UI/tree/dev/ui/packages/adapters/react/src/components/accordion" target="_blank" rel="noreferrer">React</a>
  <a href="https://github.com/XiHanFun/XiHan.UI/blob/dev/ui/packages/adapters/web-components/src/elements/accordion.ts" target="_blank" rel="noreferrer">Web Components</a>
</div>

## 用法

默认单开：展开一项即收起其余，defaultValue 只提供初始值，之后由组件自行维护

```vue
<script setup lang="ts">
import { XhAccordionRoot } from "@xihan-ui/vue";

const items = [
  {
    value: "install",
    label: "怎么安装",
    content: "装 @xihan-ui/vue 与 @xihan-ui/styles 两个包，皮肤单独引一次。",
  },
  {
    value: "theme",
    label: "怎么换皮肤",
    content: "皮肤只认 data-part 与 data-state，覆写同名令牌即可。",
  },
  {
    value: "a11y",
    label: "键盘怎么走",
    content: "方向键只在标题之间搬焦点，永不进内容区，首尾不回绕。",
  },
];
</script>

<template>
  <div style="width: 100%; max-width: 420px">
    <XhAccordionRoot :collection="items" :default-value="['install']" />
  </div>
</template>
```

```html
<div style="width: 100%; max-width: 420px">
  <xh-accordion id="accordion-basic">
    <div data-xh-part="root">
      <div data-xh-part="item" value="install">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>怎么安装</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">
          装 @xihan-ui/web-components 与 @xihan-ui/styles 两个包，皮肤单独引一次。
        </div>
      </div>
      <div data-xh-part="item" value="theme">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>怎么换皮肤</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">
          皮肤只认 data-part 与 data-state，覆写同名令牌即可。
        </div>
      </div>
      <div data-xh-part="item" value="a11y">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>键盘怎么走</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">
          方向键只在标题之间搬焦点，永不进内容区，首尾不回绕。
        </div>
      </div>
    </div>
  </xh-accordion>
</div>

<script type="module">
  // 展开集合是数组，只走 property：设初值、每次变更写回
  const accordion = document.getElementById("accordion-basic");
  accordion.value = ["install"];
  accordion.addEventListener("value-change", (event) => {
    accordion.value = event.detail.value;
  });
</script>
```

## 组件结构

加粗的是必需部件。

`data-scope="accordion"`：`root` · `item` · `item-separator` · `header` · **`trigger`** · **`content`** · `indicator`

## 示例

### 多项展开

multiple 允许多项并存，展开集合恒为 string[]，受控绑定即可获取它

```vue
<script setup lang="ts">
import { XhAccordionRoot } from "@xihan-ui/vue";
import { ref } from "vue";

const items = [
  { value: "basic", label: "基础属性", content: "value、defaultValue、multiple。" },
  {
    value: "size",
    label: "排版",
    content: "orientation 决定方向键走哪条轴，默认 vertical。",
  },
  {
    value: "events",
    label: "事件",
    content: "value-change 携带 { value }，update:value 携带裸数组。",
  },
];

const panels = ref<string[]>(["basic", "size"]);
</script>

<template>
  <div style="width: 100%; max-width: 420px; display: grid; gap: 12px">
    <XhAccordionRoot v-model:value="panels" :collection="items" multiple />
    <span>展开：{{ panels.length ? panels.join("、") : "（无）" }}</span>
  </div>
</template>
```

```html
<div style="width: 100%; max-width: 420px; display: grid; gap: 12px">
  <xh-accordion id="accordion-multiple" multiple>
    <div data-xh-part="root">
      <div data-xh-part="item" value="basic">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>基础属性</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">value、defaultValue、multiple。</div>
      </div>
      <div data-xh-part="item" value="size">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>排版</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">
          orientation 决定方向键走哪条轴，默认 vertical。
        </div>
      </div>
      <div data-xh-part="item" value="events">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>事件</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">value-change 携带 { value }。</div>
      </div>
    </div>
  </xh-accordion>
  <span>展开：<span id="accordion-multiple-value">basic、size</span></span>
</div>

<script type="module">
  // 展开集合是数组，只走 property：设初值、每次变更写回、再回显
  const accordion = document.getElementById("accordion-multiple");
  const readout = document.getElementById("accordion-multiple-value");
  accordion.value = ["basic", "size"];
  accordion.addEventListener("value-change", (event) => {
    accordion.value = event.detail.value;
    readout.textContent = event.detail.value.join("、") || "（无）";
  });
</script>
```

### 允许全部收起

单开模式下最后一项默认无法收起，加 collapsible 后才能收起

```vue
<script setup lang="ts">
import { XhAccordionRoot } from "@xihan-ui/vue";

const items = [
  {
    value: "one",
    label: "再点一次就收起",
    content: "点当前展开项的标题，它会收起，展开集合变成空数组。",
  },
  {
    value: "two",
    label: "另一项",
    content: "展开它会把上一项挤掉，单开模式一次只留一项。",
  },
];
</script>

<template>
  <div style="width: 100%; max-width: 420px">
    <XhAccordionRoot :collection="items" :default-value="['one']" collapsible />
  </div>
</template>
```

```html
<div style="width: 100%; max-width: 420px">
  <xh-accordion id="accordion-collapsible" collapsible>
    <div data-xh-part="root">
      <div data-xh-part="item" value="one">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>再点一次就收起</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">
          点当前展开项的标题，它会收起，展开集合变成空数组。
        </div>
      </div>
      <div data-xh-part="item" value="two">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>另一项</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">
          展开它会把上一项挤掉，单开模式一次只留一项。
        </div>
      </div>
    </div>
  </xh-accordion>
</div>

<script type="module">
  // 展开集合是数组，只走 property：设初值、每次变更写回
  const accordion = document.getElementById("accordion-collapsible");
  accordion.value = ["one"];
  accordion.addEventListener("value-change", (event) => {
    accordion.value = event.detail.value;
  });
</script>
```

### 指示器与禁用

indicator 的朝向由 data-state 驱动，禁用项不可点击、方向键也跳过它

```vue
<script setup lang="ts">
import { XhAccordionRoot } from "@xihan-ui/vue";

const items = [
  {
    value: "ready",
    label: "已发布",
    content: "标题右侧那个箭头就是 indicator，展开时自动翻转。",
  },
  {
    value: "draft",
    label: "草稿（禁用）",
    content: "这一项展不开。",
    disabled: true,
  },
  {
    value: "archived",
    label: "已归档",
    content: "从第一项按方向键，会直接跳到这里。",
  },
];
</script>

<template>
  <div style="width: 100%; max-width: 420px">
    <XhAccordionRoot :collection="items" :default-value="['ready']" />
  </div>
</template>
```

```html
<div style="width: 100%; max-width: 420px">
  <xh-accordion id="accordion-indicator">
    <div data-xh-part="root">
      <div data-xh-part="item" value="ready">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>已发布</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">
          标题右侧那个箭头就是 indicator，展开时自动翻转。
        </div>
      </div>
      <!-- 禁用写在条目节点上，条目内的部件跟着它走 -->
      <div data-xh-part="item" value="draft" aria-disabled="true">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>草稿（禁用）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">这一项展不开。</div>
      </div>
      <div data-xh-part="item" value="archived">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>已归档</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">从第一项按方向键，会直接跳到这里。</div>
      </div>
    </div>
  </xh-accordion>
</div>

<script type="module">
  // 展开集合是数组，只走 property：设初值、每次变更写回
  const accordion = document.getElementById("accordion-indicator");
  accordion.value = ["ready"];
  accordion.addEventListener("value-change", (event) => {
    accordion.value = event.detail.value;
  });
</script>
```

### 颜色

tone 落在展开态的标题上，六种颜色各预置一项展开做对照

```vue
<script setup lang="ts">
import { XhAccordionRoot } from "@xihan-ui/vue";

const tones = [
  { value: "brand", label: "品牌" },
  { value: "neutral", label: "中性" },
  { value: "success", label: "成功" },
  { value: "warning", label: "警告" },
  { value: "danger", label: "危险" },
  { value: "info", label: "信息" },
].map(tone => ({
  ...tone,
  panels: [
    {
      value: "open",
      label: `${tone.label}（展开）`,
      content: `tone="${tone.value}"`,
    },
    {
      value: "closed",
      label: `${tone.label}（收起）`,
      content: "收起态标题保持默认颜色。",
    },
  ],
}));
</script>

<template>
  <div
    style="
      display: grid;
      gap: 16px;
      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    "
  >
    <XhAccordionRoot
      v-for="tone in tones"
      :key="tone.value"
      :tone="tone.value"
      :collection="tone.panels"
      :default-value="['open']"
    />
  </div>
</template>
```

```html
<div
  id="accordion-tones"
  style="
    display: grid;
    gap: 16px;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  "
>
  <xh-accordion tone="brand">
    <div data-xh-part="root">
      <div data-xh-part="item" value="open">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>品牌（展开）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">tone="brand"</div>
      </div>
      <div data-xh-part="item" value="closed">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>品牌（收起）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">收起态标题保持默认颜色。</div>
      </div>
    </div>
  </xh-accordion>
  <xh-accordion tone="neutral">
    <div data-xh-part="root">
      <div data-xh-part="item" value="open">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>中性（展开）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">tone="neutral"</div>
      </div>
      <div data-xh-part="item" value="closed">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>中性（收起）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">收起态标题保持默认颜色。</div>
      </div>
    </div>
  </xh-accordion>
  <xh-accordion tone="success">
    <div data-xh-part="root">
      <div data-xh-part="item" value="open">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>成功（展开）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">tone="success"</div>
      </div>
      <div data-xh-part="item" value="closed">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>成功（收起）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">收起态标题保持默认颜色。</div>
      </div>
    </div>
  </xh-accordion>
  <xh-accordion tone="warning">
    <div data-xh-part="root">
      <div data-xh-part="item" value="open">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>警告（展开）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">tone="warning"</div>
      </div>
      <div data-xh-part="item" value="closed">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>警告（收起）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">收起态标题保持默认颜色。</div>
      </div>
    </div>
  </xh-accordion>
  <xh-accordion tone="danger">
    <div data-xh-part="root">
      <div data-xh-part="item" value="open">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>危险（展开）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">tone="danger"</div>
      </div>
      <div data-xh-part="item" value="closed">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>危险（收起）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">收起态标题保持默认颜色。</div>
      </div>
    </div>
  </xh-accordion>
  <xh-accordion tone="info">
    <div data-xh-part="root">
      <div data-xh-part="item" value="open">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>信息（展开）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">tone="info"</div>
      </div>
      <div data-xh-part="item" value="closed">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>信息（收起）</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">收起态标题保持默认颜色。</div>
      </div>
    </div>
  </xh-accordion>
</div>

<script type="module">
  // 展开集合是数组，只走 property：逐组设初值、每次变更写回
  const grid = document.getElementById("accordion-tones");
  for (const accordion of grid.querySelectorAll("xh-accordion")) {
    accordion.value = ["open"];
    accordion.addEventListener("value-change", (event) => {
      accordion.value = event.detail.value;
    });
  }
</script>
```

### 尺寸

size 改变标题栏的高度、内边距与字号，三档并排对照

```vue
<script setup lang="ts">
import { XhAccordionRoot } from "@xihan-ui/vue";

const another = {
  value: "b",
  label: "另一项",
  content: "同一档内所有标题一致。",
};

// 中间一档不写 size，用 undefined 表达
const groups = [
  {
    size: "sm",
    key: "sm",
    panels: [
      { value: "a", label: "小号 sm", content: "标题栏最矮，字号也最小。" },
      another,
    ],
  },
  {
    size: undefined,
    key: "md",
    panels: [
      { value: "a", label: "缺省档", content: "不写 size 就是这一档。" },
      another,
    ],
  },
  {
    size: "lg",
    key: "lg",
    panels: [
      { value: "a", label: "大号 lg", content: "标题栏最高，字号也最大。" },
      another,
    ],
  },
];
</script>

<template>
  <div
    style="
      display: grid;
      gap: 16px;
      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
      align-items: start;
    "
  >
    <XhAccordionRoot
      v-for="group in groups"
      :key="group.key"
      :size="group.size"
      :collection="group.panels"
      :default-value="['a']"
    />
  </div>
</template>
```

```html
<div
  id="accordion-sizes"
  style="
    display: grid;
    gap: 16px;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    align-items: start;
  "
>
  <xh-accordion size="sm">
    <div data-xh-part="root">
      <div data-xh-part="item" value="a">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>小号 sm</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">标题栏最矮，字号也最小。</div>
      </div>
      <div data-xh-part="item" value="b">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>另一项</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">同一档内所有标题一致。</div>
      </div>
    </div>
  </xh-accordion>

  <!-- 中间一档不写 size -->
  <xh-accordion>
    <div data-xh-part="root">
      <div data-xh-part="item" value="a">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>缺省档</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">不写 size 就是这一档。</div>
      </div>
      <div data-xh-part="item" value="b">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>另一项</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">同一档内所有标题一致。</div>
      </div>
    </div>
  </xh-accordion>

  <xh-accordion size="lg">
    <div data-xh-part="root">
      <div data-xh-part="item" value="a">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>大号 lg</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">标题栏最高，字号也最大。</div>
      </div>
      <div data-xh-part="item" value="b">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>另一项</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">同一档内所有标题一致。</div>
      </div>
    </div>
  </xh-accordion>
</div>

<script type="module">
  // 展开集合是数组，只走 property：逐组设初值、每次变更写回
  const grid = document.getElementById("accordion-sizes");
  for (const accordion of grid.querySelectorAll("xh-accordion")) {
    accordion.value = ["a"];
    accordion.addEventListener("value-change", (event) => {
      accordion.value = event.detail.value;
    });
  }
</script>
```

### 嵌套

content 中再放一组手风琴，内外两组各自维护展开集合，方向键也各自独立

```vue
<script setup lang="ts">
import {
  XhAccordionContent,
  XhAccordionHeader,
  XhAccordionIndicator,
  XhAccordionItem,
  XhAccordionRoot,
  XhAccordionTrigger,
} from "@xihan-ui/vue";
</script>

<template>
  <div style="width: 100%; max-width: 420px">
    <XhAccordionRoot :default-value="['shipping']">
      <XhAccordionItem value="shipping">
        <XhAccordionHeader>
          <XhAccordionTrigger>
            <span>配送</span>
            <XhAccordionIndicator />
          </XhAccordionTrigger>
        </XhAccordionHeader>
        <XhAccordionContent>
          <!-- 内层是另一组独立的手风琴：展开集合、单开与否都自己说了算 -->
          <XhAccordionRoot :default-value="['express']" multiple>
            <XhAccordionItem value="express">
              <XhAccordionHeader>
                <XhAccordionTrigger>
                  <span>快递</span>
                  <XhAccordionIndicator />
                </XhAccordionTrigger>
              </XhAccordionHeader>
              <XhAccordionContent>次日达，节假日照常发货。</XhAccordionContent>
            </XhAccordionItem>
            <XhAccordionItem value="pickup">
              <XhAccordionHeader>
                <XhAccordionTrigger>
                  <span>自提</span>
                  <XhAccordionIndicator />
                </XhAccordionTrigger>
              </XhAccordionHeader>
              <XhAccordionContent>下单后到门店凭码取货。</XhAccordionContent>
            </XhAccordionItem>
          </XhAccordionRoot>
        </XhAccordionContent>
      </XhAccordionItem>

      <XhAccordionItem value="refund">
        <XhAccordionHeader>
          <XhAccordionTrigger>
            <span>退换</span>
            <XhAccordionIndicator />
          </XhAccordionTrigger>
        </XhAccordionHeader>
        <XhAccordionContent>签收七日内可退，运费到付。</XhAccordionContent>
      </XhAccordionItem>
    </XhAccordionRoot>
  </div>
</template>
```

```html
<div style="width: 100%; max-width: 420px">
  <xh-accordion id="accordion-outer">
    <div data-xh-part="root">
      <div data-xh-part="item" value="shipping">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>配送</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">
          <!-- 内层是另一组独立的手风琴：展开集合、单开与否都自己说了算 -->
          <xh-accordion id="accordion-inner" multiple>
            <div data-xh-part="root">
              <div data-xh-part="item" value="express">
                <h3 data-xh-part="header">
                  <button data-xh-part="trigger">
                    <span>快递</span>
                    <span data-xh-part="indicator"></span>
                  </button>
                </h3>
                <div data-xh-part="content">次日达，节假日照常发货。</div>
              </div>
              <div data-xh-part="item" value="pickup">
                <h3 data-xh-part="header">
                  <button data-xh-part="trigger">
                    <span>自提</span>
                    <span data-xh-part="indicator"></span>
                  </button>
                </h3>
                <div data-xh-part="content">下单后到门店凭码取货。</div>
              </div>
            </div>
          </xh-accordion>
        </div>
      </div>

      <div data-xh-part="item" value="refund">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>退换</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">签收七日内可退，运费到付。</div>
      </div>
    </div>
  </xh-accordion>
</div>

<script type="module">
  // 展开集合是数组，只走 property：内外两组各设各的初值、各写各的回
  const outer = document.getElementById("accordion-outer");
  const inner = document.getElementById("accordion-inner");
  outer.value = ["shipping"];
  inner.value = ["express"];
  // 内层的事件会冒泡上来，只认自己派的那份
  outer.addEventListener("value-change", (event) => {
    if (event.target === outer) outer.value = event.detail.value;
  });
  inner.addEventListener("value-change", (event) => {
    inner.value = event.detail.value;
  });
</script>
```

### 标题栏附加信息

标题栏中的节点全部归作者，把计数与指示器包为一组排在末尾

```vue
<script setup lang="ts">
import {
  XhAccordionContent,
  XhAccordionHeader,
  XhAccordionIndicator,
  XhAccordionItem,
  XhAccordionRoot,
  XhAccordionTrigger,
} from "@xihan-ui/vue";

const groups = [
  { value: "todo", label: "待处理", extra: "3 项", body: "还没有人认领。" },
  { value: "doing", label: "进行中", extra: "1 项", body: "预计今天完成。" },
  { value: "done", label: "已完成", extra: "12 项", body: "本周已归档。" },
];
</script>

<template>
  <div style="width: 100%; max-width: 420px">
    <XhAccordionRoot :default-value="['todo']">
      <XhAccordionItem v-for="g in groups" :key="g.value" :value="g.value">
        <XhAccordionHeader>
          <XhAccordionTrigger>
            <span>{{ g.label }}</span>
            <!-- 附加信息与指示器同属末尾这一组，标题栏两端对齐照旧生效 -->
            <span style="display: flex; align-items: center; gap: 8px; font-size: 12px">
              <span>{{ g.extra }}</span>
              <XhAccordionIndicator />
            </span>
          </XhAccordionTrigger>
        </XhAccordionHeader>
        <XhAccordionContent>{{ g.body }}</XhAccordionContent>
      </XhAccordionItem>
    </XhAccordionRoot>
  </div>
</template>
```

```html
<div style="width: 100%; max-width: 420px">
  <xh-accordion id="accordion-header-extra">
    <div data-xh-part="root">
      <div data-xh-part="item" value="todo">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>待处理</span>
            <!-- 附加信息与指示器同属末尾这一组，标题栏两端对齐照旧生效 -->
            <span
              style="display: flex; align-items: center; gap: 8px; font-size: 12px"
            >
              <span>3 项</span>
              <span data-xh-part="indicator"></span>
            </span>
          </button>
        </h3>
        <div data-xh-part="content">还没有人认领。</div>
      </div>
      <div data-xh-part="item" value="doing">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>进行中</span>
            <span
              style="display: flex; align-items: center; gap: 8px; font-size: 12px"
            >
              <span>1 项</span>
              <span data-xh-part="indicator"></span>
            </span>
          </button>
        </h3>
        <div data-xh-part="content">预计今天完成。</div>
      </div>
      <div data-xh-part="item" value="done">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>已完成</span>
            <span
              style="display: flex; align-items: center; gap: 8px; font-size: 12px"
            >
              <span>12 项</span>
              <span data-xh-part="indicator"></span>
            </span>
          </button>
        </h3>
        <div data-xh-part="content">本周已归档。</div>
      </div>
    </div>
  </xh-accordion>
</div>

<script type="module">
  // 展开集合是数组，只走 property：设初值、每次变更写回
  const accordion = document.getElementById("accordion-header-extra");
  accordion.value = ["todo"];
  accordion.addEventListener("value-change", (event) => {
    accordion.value = event.detail.value;
  });
</script>
```

### 指示器在前

指示器写在标题之前即落到起始缘，标题用 auto 外边距占据余量

```vue
<script setup lang="ts">
import {
  XhAccordionContent,
  XhAccordionHeader,
  XhAccordionIndicator,
  XhAccordionItem,
  XhAccordionRoot,
  XhAccordionTrigger,
} from "@xihan-ui/vue";

const items = [
  { value: "one", label: "第一章", body: "指示器在标题左边，展开时照样翻转。" },
  { value: "two", label: "第二章", body: "部件的先后顺序就是它们在标题栏里的顺序。" },
  { value: "three", label: "第三章", body: "标题吃掉余量，右侧留白。" },
];
</script>

<template>
  <div style="width: 100%; max-width: 420px">
    <XhAccordionRoot :default-value="['one']">
      <XhAccordionItem v-for="item in items" :key="item.value" :value="item.value">
        <XhAccordionHeader>
          <XhAccordionTrigger>
            <XhAccordionIndicator />
            <span style="margin-inline-end: auto">{{ item.label }}</span>
          </XhAccordionTrigger>
        </XhAccordionHeader>
        <XhAccordionContent>{{ item.body }}</XhAccordionContent>
      </XhAccordionItem>
    </XhAccordionRoot>
  </div>
</template>
```

```html
<div style="width: 100%; max-width: 420px">
  <xh-accordion id="accordion-indicator-start">
    <div data-xh-part="root">
      <div data-xh-part="item" value="one">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span data-xh-part="indicator"></span>
            <span style="margin-inline-end: auto">第一章</span>
          </button>
        </h3>
        <div data-xh-part="content">指示器在标题左边，展开时照样翻转。</div>
      </div>
      <div data-xh-part="item" value="two">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span data-xh-part="indicator"></span>
            <span style="margin-inline-end: auto">第二章</span>
          </button>
        </h3>
        <div data-xh-part="content">部件的先后顺序就是它们在标题栏里的顺序。</div>
      </div>
      <div data-xh-part="item" value="three">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span data-xh-part="indicator"></span>
            <span style="margin-inline-end: auto">第三章</span>
          </button>
        </h3>
        <div data-xh-part="content">标题吃掉余量，右侧留白。</div>
      </div>
    </div>
  </xh-accordion>
</div>

<script type="module">
  // 展开集合是数组，只走 property：设初值、每次变更写回
  const accordion = document.getElementById("accordion-indicator-start");
  accordion.value = ["one"];
  accordion.addEventListener("value-change", (event) => {
    accordion.value = event.detail.value;
  });
</script>
```

### 缩小触发区域

trigger 只包住指示器，标题文字留在 header 里，点标题不再展开

```vue
<script setup lang="ts">
import {
  XhAccordionContent,
  XhAccordionHeader,
  XhAccordionIndicator,
  XhAccordionItem,
  XhAccordionRoot,
  XhAccordionTrigger,
} from "@xihan-ui/vue";

const items = [
  { value: "profile", label: "账户资料", body: "只有右边那个按钮能展开这一段。" },
  { value: "billing", label: "账单信息", body: "标题文字不在按钮里，点它没有反应。" },
];
</script>

<template>
  <div style="width: 100%; max-width: 420px">
    <XhAccordionRoot :default-value="['profile']">
      <XhAccordionItem v-for="item in items" :key="item.value" :value="item.value">
        <!-- 标题栏自己排布：文字是普通节点，按钮只占末尾一小格 -->
        <XhAccordionHeader
          style="display: flex; align-items: center; gap: 8px; padding-inline-start: 12px"
        >
          <span style="flex: 1">{{ item.label }}</span>
          <XhAccordionTrigger
            style="inline-size: auto"
            :aria-label="`展开${item.label}`"
          >
            <XhAccordionIndicator />
          </XhAccordionTrigger>
        </XhAccordionHeader>
        <XhAccordionContent>{{ item.body }}</XhAccordionContent>
      </XhAccordionItem>
    </XhAccordionRoot>
  </div>
</template>
```

```html
<div style="width: 100%; max-width: 420px">
  <xh-accordion id="accordion-trigger-area">
    <div data-xh-part="root">
      <div data-xh-part="item" value="profile">
        <!-- 标题栏自己排布：文字是普通节点，按钮只占末尾一小格 -->
        <h3
          data-xh-part="header"
          style="
            display: flex;
            align-items: center;
            gap: 8px;
            padding-inline-start: 12px;
          "
        >
          <span style="flex: 1">账户资料</span>
          <button
            data-xh-part="trigger"
            style="inline-size: auto"
            aria-label="展开账户资料"
          >
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">只有右边那个按钮能展开这一段。</div>
      </div>
      <div data-xh-part="item" value="billing">
        <h3
          data-xh-part="header"
          style="
            display: flex;
            align-items: center;
            gap: 8px;
            padding-inline-start: 12px;
          "
        >
          <span style="flex: 1">账单信息</span>
          <button
            data-xh-part="trigger"
            style="inline-size: auto"
            aria-label="展开账单信息"
          >
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">标题文字不在按钮里，点它没有反应。</div>
      </div>
    </div>
  </xh-accordion>
</div>

<script type="module">
  // 展开集合是数组，只走 property：设初值、每次变更写回
  const accordion = document.getElementById("accordion-trigger-area");
  accordion.value = ["profile"];
  accordion.addEventListener("value-change", (event) => {
    accordion.value = event.detail.value;
  });
</script>
```

### 自定义展开图标

indicator 是可选部件，不渲染它就没有默认字形；标记由作者按展开集合自行绘制

```vue
<script setup lang="ts">
import { MinusIcon, PlusIcon } from "@xihan-ui/icons";
import {
  XhAccordionContent,
  XhAccordionHeader,
  XhAccordionItem,
  XhAccordionRoot,
  XhAccordionTrigger,
  XhIcon,
} from "@xihan-ui/vue";
import { ref } from "vue";

const items = [
  { value: "shipping", label: "配送方式", body: "同城次日达，跨省三日达。" },
  { value: "invoice", label: "发票", body: "支持电子普票与专票。" },
  { value: "refund", label: "退换货", body: "签收七日内无理由退换。" },
];

const panels = ref<string[]>(["shipping"]);
</script>

<template>
  <div style="width: 100%; max-width: 420px">
    <XhAccordionRoot v-model:value="panels" multiple>
      <XhAccordionItem v-for="item in items" :key="item.value" :value="item.value">
        <XhAccordionHeader>
          <XhAccordionTrigger>
            <span>{{ item.label }}</span>
            <!-- 标记按这一项在不在展开集合里切换图标 -->
            <XhIcon
              :icon="panels.includes(item.value) ? MinusIcon : PlusIcon"
              size="sm"
              style="color: var(--xh-fg-muted)"
            />
          </XhAccordionTrigger>
        </XhAccordionHeader>
        <XhAccordionContent>{{ item.body }}</XhAccordionContent>
      </XhAccordionItem>
    </XhAccordionRoot>
  </div>
</template>
```

```html
<div style="width: 100%; max-width: 420px">
  <xh-accordion id="accordion-custom-icon" multiple>
    <div data-xh-part="root">
      <div data-xh-part="item" value="shipping">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>配送方式</span>
            <svg data-mark aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--xh-fg-muted)"><path d="M12 5V19"/><path data-cross d="M5 12H19"/></svg>
          </button>
        </h3>
        <div data-xh-part="content">同城次日达，跨省三日达。</div>
      </div>
      <div data-xh-part="item" value="invoice">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>发票</span>
            <svg data-mark aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--xh-fg-muted)"><path d="M12 5V19"/><path data-cross d="M5 12H19"/></svg>
          </button>
        </h3>
        <div data-xh-part="content">支持电子普票与专票。</div>
      </div>
      <div data-xh-part="item" value="refund">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>退换货</span>
            <svg data-mark aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--xh-fg-muted)"><path d="M12 5V19"/><path data-cross d="M5 12H19"/></svg>
          </button>
        </h3>
        <div data-xh-part="content">签收七日内无理由退换。</div>
      </div>
    </div>
  </xh-accordion>
</div>

<script type="module">
  // 标记按这一项在不在展开集合里切换图形
  const accordion = document.getElementById("accordion-custom-icon");
  const paint = (value) => {
    for (const item of accordion.querySelectorAll('[data-xh-part="item"]')) {
      const mark = item.querySelector("[data-mark]");
      const expanded = value.includes(item.getAttribute("value"));
      mark.querySelector("path").setAttribute("d", expanded ? "M5 12H19" : "M12 5V19");
      mark.querySelector("[data-cross]").style.display = expanded ? "none" : "";
    }
  };
  accordion.value = ["shipping"];
  paint(accordion.value);
  accordion.addEventListener("value-change", (event) => {
    accordion.value = event.detail.value;
    paint(event.detail.value);
  });
</script>
```

### 变体

ghost 不绘制外壳，outline 连成单一表面，subtle 用淡底；三档只改变与页面分开的方式

```vue
<script setup lang="ts">
import { XhAccordionRoot } from "@xihan-ui/vue";

const panels = [
  { value: "shipping", label: "配送方式", content: "下单后 48 小时内发出。" },
  { value: "refund", label: "退换政策", content: "签收 7 天内可申请退换。" },
];
</script>

<template>
  <div style="display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr))">
    <XhAccordionRoot
      v-for="variant in ['ghost', 'outline', 'subtle']"
      :key="variant"
      :variant="variant"
      :collection="panels"
      :default-value="['shipping']"
    />
  </div>
</template>
```

```html
<div
  id="accordion-variants"
  style="display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr))"
>
  <xh-accordion variant="ghost">
    <div data-xh-part="root">
      <div data-xh-part="item" value="shipping">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>配送方式</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">下单后 48 小时内发出。</div>
      </div>
      <div data-xh-part="item" value="refund">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>退换政策</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">签收 7 天内可申请退换。</div>
      </div>
    </div>
  </xh-accordion>

  <xh-accordion variant="outline">
    <div data-xh-part="root">
      <div data-xh-part="item" value="shipping">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>配送方式</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">下单后 48 小时内发出。</div>
      </div>
      <div data-xh-part="item" value="refund">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>退换政策</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">签收 7 天内可申请退换。</div>
      </div>
    </div>
  </xh-accordion>

  <xh-accordion variant="subtle">
    <div data-xh-part="root">
      <div data-xh-part="item" value="shipping">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>配送方式</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">下单后 48 小时内发出。</div>
      </div>
      <div data-xh-part="item" value="refund">
        <h3 data-xh-part="header">
          <button data-xh-part="trigger">
            <span>退换政策</span>
            <span data-xh-part="indicator"></span>
          </button>
        </h3>
        <div data-xh-part="content">签收 7 天内可申请退换。</div>
      </div>
    </div>
  </xh-accordion>
</div>

<script type="module">
  // 展开集合是数组，只走 property：逐组设初值、每次变更写回
  const grid = document.getElementById("accordion-variants");
  for (const accordion of grid.querySelectorAll("xh-accordion")) {
    accordion.value = ["shipping"];
    accordion.addEventListener("value-change", (event) => {
      accordion.value = event.detail.value;
    });
  }
</script>
```

## 设计指引

### 何时使用

- 常见问题、设置分组等由标题即可判断是否需要展开的内容。
- 内容较长，一次全部铺开会使页面失去结构。

### 何时不用

- 只有一块内容时，使用[折叠区域](./collapsible)。
- 各块内容需要对照阅读时，直接铺开。
- 各块是并列视图且同一时间只看一个时，使用[标签页](./tabs)。

### 特性

- `multiple` 决定能否同时展开多项，`collapsible` 决定能否全部收起。
- 指示器可置于标题前或标题后，图形可自定义。
- 支持嵌套；触发区大小由作者决定。

### 组合

- 标题栏可以放置附加信息，如计数或状态[徽标](./badge)。

### 最佳实践

- 标题应说明区块内容，不依赖展开来发现。
- 默认展开第一项，让用户看到内容的形态。

### 反模式

- 将关键信息放进折叠区块，用户不会逐个展开。
- 展开时页面下方内容大幅跳动而没有滚动补偿。

## API 参考

### 产物

| 层 | 值 |
| --- | --- |
| 自定义元素 | `<xh-accordion>` |
| Vue 组件 | `XhAccordionContent` `XhAccordionHeader` `XhAccordionIndicator` `XhAccordionItem` `XhAccordionItemSeparator` `XhAccordionRoot` `XhAccordionTrigger` |
| 组合式函数 | `useAccordion` |
| 状态机 | `accordionMachine` |
| 皮肤 | `@xihan-ui/styles/accordion.css` |

### Props

| 属性 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `collection` | `AccordionNode[]` |  | 条目数据，标题文本、正文与禁用的事实源。提供后条目部件只需声明 value。 未提供时回到文本写在部件中、禁用写在条目上的方式。 |
| `value` | `string[]` |  | 展开集合，提供即受控。 |
| `defaultValue` | `string[]` |  |  |
| `multiple` | `boolean` |  | 允许多项同时展开；false 时展开一项即收起其余。 |
| `collapsible` | `boolean` |  | 允许收起最后一个展开项，默认 false。 |
| `loop` | `boolean` |  | 方向键到达末尾是否回绕，默认 false。 |
| `disabled` | `boolean` |  | 整组禁用：所有条目都不可切换，条目上的 disabled 只能收紧不能放宽。 |
| `variant` | `ControlVariant` |  | 形态：ghost 条目直接相邻不画容器（默认），outline 为单一连续表面，subtle 为淡底。默认 ghost。 |
| `orientation` | `Orientation` |  | 方向键轴向，默认 vertical。 |
| `dir` | `Direction` |  | 文字方向，默认 ltr；影响水平轴上 ArrowLeft / ArrowRight 的语义。 |
| `tone` | `Tone` |  | 颜色：brand / neutral / success / warning / danger / info，决定使用哪组状态色。 |
| `size` | `Size` |  | 尺寸：sm / md / lg。 |
| `onValueChange` | `(details: AccordionValueChangeDetails) => void` |  | 展开集合变化回调。 |

### AccordionNode

`collection` 的元素。

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `value` | `string` | 是 |  |
| `label` | `string` |  | 标题文本；默认回退为 value。 |
| `content` | `string` |  | 正文；需要放置纯文本以外的内容时改用 content 插槽。 |
| `disabled` | `boolean` |  | 条目禁用：方向键跳过该条目，但它仍可聚焦、仍是导航起点。 |

### 事件

自定义元素将载荷放在 `detail`；Vue 使用同名 emit。

| 事件 | 载荷 | 说明 |
| --- | --- | --- |
| `value-change` | `AccordionValueChangeDetails` | 展开集合变化；detail 为 `{ value: string[] }` |

### React 适配器 props

只列各组件自己声明的那些：继承自 `ComponentPropsWithRef` 的 DOM 属性不在其中，根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。

| React 组件 | 属性 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `XhAccordionItem` | `value` | `string` | 是 |  |
| `XhAccordionItem` | `disabled` | `boolean` |  | 默认交给 connect 查询 collection，写死 false 会覆盖数据中的禁用。 |
| `XhAccordionRoot` | `renderContent` | `(node: AccordionNodeMeta) => ReactNode` |  | 每个条目正文的自定义内容；未提供时使用 collection 中的 content。 |
| `XhAccordionRoot` | `children` | `ReactNode` |  |  |

### 状态

公开状态写入 `data-state`。

| 部件 | 取值 |
| --- | --- |
| `item` | 'open' \| 'closed' |
| `header` | 'open' \| 'closed' |
| `trigger` | 'open' \| 'closed' |
| `content` | 'open' \| 'closed' |
| `indicator` | 'open' \| 'closed' |

以下名称仅用于内部状态机。

**状态**：`idle`

**事件**：`ITEM.TOGGLE` · `VALUE.SET` · `PRESS.START` · `PRESS.END`

**判据**：`canPress`

### connect API

`getXxxProps()` 返回对应部件的宿主属性。

| 成员 | 类型 | 说明 |
| --- | --- | --- |
| `value` | `string[]` | 当前展开集合，单开模式下长度 ≤ 1。 |
| `collection` | `readonly AccordionNodeMeta[]` | 由 collection 推导的条目元信息，按数据顺序排列；未提供 collection 时为空数组。 |
| `setValue` | `(next: string[]) => void` |  |
| `isOpen` | `(value: string) => boolean` |  |
| `getRootProps` | `() => T['element']` |  |
| `getItemProps` | `(props: AccordionItemProps) => T['element']` |  |
| `getItemSeparatorProps` | `() => T['element']` |  |
| `getHeaderProps` | `(props: AccordionItemProps) => T['element']` |  |
| `getTriggerProps` | `(props: AccordionItemProps) => T['button']` |  |
| `getContentProps` | `(props: AccordionItemProps) => T['element']` |  |
| `getIndicatorProps` | `(props: AccordionItemProps) => T['element']` |  |

## 无障碍

### 键盘

规格出处：[W3C APG](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/#keyboardinteraction)

| 按键 | 生效条件 | 行为 |
| --- | --- | --- |
| `Space` / `Enter` | focus in trigger, not disabled | 展开/收起该条目的 content |
| `ArrowDown` / `ArrowRight` | focus in trigger, 按键与 orientation 同轴（dir=rtl 时左右键语义互换） | 焦点移到下一个 trigger，末条不回绕 |
| `ArrowUp` / `ArrowLeft` | focus in trigger, 按键与 orientation 同轴（dir=rtl 时左右键语义互换） | 焦点移到上一个 trigger，首条不回绕 |
| `Home` | focus in trigger | 焦点移到首个 trigger |
| `End` | focus in trigger | 焦点移到末个 trigger |
| `Tab` / `Shift+Tab` | focus in trigger | 按文档序进出：每个 trigger 都是独立 Tab 停靠点，无 roving tabindex |
| `Enter` / `Space` | held in trigger, not disabled | 按住期间该 trigger 投影 data-pressed，与指针 :active 同一副按压面（disclosure trigger 只换面不缩放）；抬起、失焦或整组转禁用撤下 |

### ARIA

以下属性由 `connect` 生成。

| 部件 | 属性 | 值 |
| --- | --- | --- |
| `item-separator` | `aria-hidden` | 'true' |
| `header` | `aria-level` | 3 |
| `header` | `role` | 'heading' |
| `trigger` | `aria-controls` | `content` 部件的 id |
| `trigger` | `aria-disabled` | 'true' \| 'false' |
| `trigger` | `aria-expanded` | 'true' \| 'false' |
| `content` | `aria-labelledby` | `trigger` 部件的 id |
| `content` | `role` | 'region' |
| `indicator` | `aria-hidden` | 'true' |

## 样式参考

### 皮肤

`@xihan-ui/styles/accordion.css` 使用 `[data-scope="accordion"][data-part="root"]` 部件选择器，位于 `xihan.components` 与 `xihan.motion` 层。覆盖样式使用 `xihan.overrides`。

### 数据属性

由 `connect` 生成；条件不成立时不输出无值属性。

| 部件 | 属性 | 值 |
| --- | --- | --- |
| `root` | `data-disabled` | ''（条件成立时才出现） |
| `root` | `data-orientation` | props.orientation |
| `root` | `data-size` | props.size |
| `root` | `data-tone` | props.tone |
| `root` | `data-variant` | props.variant |
| `item` | `data-disabled` | ''（条件成立时才出现） |
| `item` | `data-state` | 'open' \| 'closed' |
| `header` | `data-disabled` | ''（条件成立时才出现） |
| `header` | `data-state` | 'open' \| 'closed' |
| `trigger` | `data-disabled` | ''（条件成立时才出现） |
| `trigger` | `data-pressed` | ''（条件成立时才出现） |
| `trigger` | `data-state` | 'open' \| 'closed' |
| `trigger` | `data-xh-action-control` | '' |
| `trigger` | `data-xh-action-display` | 'always' |
| `trigger` | `data-xh-action-profile` | 'disclosure-trigger' |
| `trigger` | `data-xh-action-size` | props.size |
| `trigger` | `data-xh-action-variant` | 'ghost' |
| `content` | `data-instant` | '' |
| `content` | `data-state` | 'open' \| 'closed' |
| `indicator` | `data-disabled` | ''（条件成立时才出现） |
| `indicator` | `data-instant` | '' |
| `indicator` | `data-state` | 'open' \| 'closed' |

<!-- xh-component-tokens:start -->
### CSS 变量

本组件公开覆盖槽由独立皮肤的实际消费位生成；默认来源、作用部件和状态均与 CSS 同源。

| 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `--xh-accordion-border` | `root` | `border` | `variant=outline` | `--xh-border-default` | accordion 的 root 部件 border 覆盖槽。 |
| `--xh-accordion-content-fg` | `content` | `color` | `default` | `--xh-fg-muted` | accordion 的 content 部件 color 覆盖槽。 |
| `--xh-accordion-content-font-size` | `content` | `font-size` | `default` | `--xh-text-secondary-size` | accordion 的 content 部件 font-size 覆盖槽。 |
| `--xh-accordion-content-pb` | `content` | `padding-block-end` | `@keyframes xh-disclosure-collapse`<br>`@keyframes xh-disclosure-expand`<br>`default` | `--xh-_accordion-content-pb` | accordion 的 content 部件 padding-block-end 覆盖槽。 |
| `--xh-accordion-content-px` | `content` | `padding-inline` | `default` | `--xh-_accordion-content-px` | accordion 的 content 部件 padding-inline 覆盖槽。 |
| `--xh-accordion-icon-size` | `root`<br>`trigger` | `--xh-icon-size` | `default` | `--xh-_action-profile-glyph-size`<br>`--xh-glyph-size-md` | accordion 的 root、trigger 部件 --xh-icon-size 覆盖槽。 |
| `--xh-accordion-indicator-fg` | `indicator` | `color` | `default` | `--xh-fg-muted` | accordion 的 indicator 部件 color 覆盖槽。 |
| `--xh-accordion-item-bg` | `root` | `background` | `variant=outline`<br>`variant=subtle` | `--xh-bg-subtle`<br>`--xh-bg-surface` | accordion 的 root 部件 background 覆盖槽。 |
| `--xh-accordion-item-border` | `item`<br>`item-separator`<br>`root` | `background`<br>`border-block-start`<br>`border-inline-start` | `default`<br>`is([data-variant='outline'], [data-variant='subtle'])`<br>`not(:last-child)`<br>`orientation=horizontal`<br>`variant=outline`<br>`variant=subtle` | `--xh-border-subtle` | accordion 的 item、item-separator、root 部件 background、border-block-start、border-inline-start 覆盖槽。 |
| `--xh-accordion-item-radius` | `root` | `border-radius` | `variant=outline`<br>`variant=subtle` | `--xh-shape-surface` | accordion 的 root 部件 border-radius 覆盖槽。 |
| `--xh-accordion-item-shadow` | `root` | `box-shadow` | `variant=outline`<br>`variant=subtle` | `none` | accordion 的 root 部件 box-shadow 覆盖槽。 |
| `--xh-accordion-trigger-bg` | `trigger` | `--xh-ink-surface`<br>`background-color` | `default`<br>`xh-ink-surface` | `--xh-_action-variant-bg-rest` | accordion 的 trigger 部件 --xh-ink-surface、background-color 覆盖槽。 |
| `--xh-accordion-trigger-bg-hover` | `trigger` | `background-color` | `disabled`<br>`hover`<br>`loading`<br>`not([data-disabled])`<br>`not([data-loading])` | `--xh-_action-variant-bg-hover` | accordion 的 trigger 部件 background-color 覆盖槽。 |
| `--xh-accordion-trigger-fg` | `trigger` | `color` | `default`<br>`disabled`<br>`hover`<br>`is(:active, [data-pressed])`<br>`loading`<br>`not([data-disabled])`<br>`not([data-loading])`<br>`pressed` | `--xh-_action-variant-fg-hover`<br>`--xh-_action-variant-fg-pressed`<br>`--xh-_action-variant-fg-rest` | accordion 的 trigger 部件 color 覆盖槽。 |
| `--xh-accordion-trigger-fg-disabled` | `trigger` | `color` | `disabled` | `--xh-_action-variant-fg-disabled` | accordion 的 trigger 部件 color 覆盖槽。 |
| `--xh-accordion-trigger-fg-open` | `trigger` | `color` | `disabled`<br>`hover`<br>`is(:active, [data-pressed])`<br>`loading`<br>`not([data-disabled])`<br>`not([data-loading])`<br>`pressed`<br>`state=open` | `--xh-_accordion-open-fg` | accordion 的 trigger 部件 color 覆盖槽。 |
| `--xh-accordion-trigger-font-size` | `trigger` | `font-size` | `default` | `--xh-_action-profile-font-size` | accordion 的 trigger 部件 font-size 覆盖槽。 |
| `--xh-accordion-trigger-font-weight` | `trigger` | `font-weight` | `default` | `--xh-text-label-weight` | accordion 的 trigger 部件 font-weight 覆盖槽。 |
| `--xh-accordion-trigger-gap` | `trigger` | `gap` | `default` | `--xh-_action-profile-gap` | accordion 的 trigger 部件 gap 覆盖槽。 |
| `--xh-accordion-trigger-h` | `trigger` | `block-size`<br>`min-block-size` | `default`<br>`xh-action-profile=disclosure-trigger` | `--xh-_action-profile-visual-size` | accordion 的 trigger 部件 block-size、min-block-size 覆盖槽。 |
| `--xh-accordion-trigger-px` | `trigger` | `padding-inline` | `default` | `--xh-_action-profile-padding-inline` | accordion 的 trigger 部件 padding-inline 覆盖槽。 |
| `--xh-accordion-trigger-py` | `trigger` | `padding-block` | `xh-action-profile=disclosure-trigger` | `--xh-_action-profile-padding-block` | accordion 的 trigger 部件 padding-block 覆盖槽。 |
| `--xh-accordion-trigger-radius` | `trigger` | `border-radius` | `default` | `--xh-_action-profile-radius` | accordion 的 trigger 部件 border-radius 覆盖槽。 |
<!-- xh-component-tokens:end -->

### 动效

动效角色：按压 · 状态 · 披露（见[动效规范](../design/motion#角色)）。

共享关键帧 `xh-disclosure-collapse` · `xh-disclosure-expand` 由 `family/motion.css` 提供，皮肤 `@import` 它，单独引入仍成立；`rotate` 走 `transition` 过渡。时长与缓动读[动效令牌](../guide/motion)，改令牌即改全局节奏。

皮肤之外还有一段：退场由适配器的退场闸门把关，动画播完才真收起。

系统开启减弱动效时由令牌层统一收敛，皮肤不另作判断。

### RTL

皮肤用逻辑属性排布（`inline-start` 一族），`dir="rtl"` 下自动镜像。
