컴포넌트

Accordion

여러 항목의 내용을 접거나 펼칩니다.

모션

내용의 실제 높이에 맞춰 200ms 동안 펼치거나 접습니다. 움직임 감소 설정에서는 전환을 생략합니다. 공통 모션 규칙을 따릅니다.

설치

pnpm dlx ply-ui add accordion

공통 설치 설정을 먼저 적용합니다.

사용법

각 Item에 고유한 value를 주고 Trigger와 Panel을 같은 Item에 둡니다.

defaultValue는 펼친 항목의 배열입니다. multiple로 여러 항목을 동시에 열지 결정합니다.

구성

구성 요소역할
Root펼쳐진 항목을 값 배열로 관리합니다. multiple로 여러 항목을 동시에 펼칩니다.
Item하나의 제목과 본문을 묶습니다. 고유한 value가 필요합니다.
Header펼침 버튼을 담는 제목 영역입니다.
Trigger해당 항목을 펼치거나 접습니다.
Chevron펼침 상태에 따라 바뀌는 화살표입니다.
Panel펼쳐지는 본문입니다.

여러 항목 펼치기

Root.multiple을 지정하면 여러 항목을 동시에 펼칠 수 있습니다. defaultValue는 펼칠 항목의 값 배열입니다.

구현 코드

components/ui/accordion.tsx
accordion.tsx
"use client";

import type { ComponentProps } from "react";
import { Accordion as Primitive } from "@base-ui/react/accordion";
import { withClassName } from "../../lib/cx";
import { Icon } from "./icon";
import "./accordion.css";

// 동작과 접근성은 Base UI가 담당합니다. 이 파일은 스타일 연결만 담당합니다.
function AccordionRoot({
  multiple = true,
  className,
  ...props
}: ComponentProps<typeof Primitive.Root>) {
  return (
    <Primitive.Root
      {...props}
      multiple={multiple}
      className={withClassName("rbx-accordion", className)}
    />
  );
}

function AccordionItem({
  className,
  ...props
}: ComponentProps<typeof Primitive.Item>) {
  return (
    <Primitive.Item
      {...props}
      className={withClassName("rbx-accordion-item", className)}
    />
  );
}

function AccordionHeader({
  className,
  ...props
}: ComponentProps<typeof Primitive.Header>) {
  return (
    <Primitive.Header
      {...props}
      className={withClassName("rbx-accordion-header", className)}
    />
  );
}

function AccordionTrigger({
  className,
  ...props
}: ComponentProps<typeof Primitive.Trigger>) {
  return (
    <Primitive.Trigger
      {...props}
      className={withClassName("rbx-accordion-trigger", className)}
    />
  );
}

function AccordionPanel({
  className,
  ...props
}: ComponentProps<typeof Primitive.Panel>) {
  return (
    <Primitive.Panel
      {...props}
      className={withClassName("rbx-accordion-panel", className)}
    />
  );
}

/** 상태는 Base UI Trigger의 data-panel-open 속성을 따라갑니다. */
function AccordionChevron() {
  return (
    <span className="rbx-accordion-chevron" aria-hidden="true">
      <Icon name="icon-regular-chevron-large-down" size={20} />
      <Icon name="icon-regular-chevron-large-up" size={20} />
    </span>
  );
}
// Root/Portal 등 스타일 없는 파트와 제네릭 API는 원본을 그대로 보존합니다.
export const Accordion = {
  ...Primitive,
  Chevron: AccordionChevron,
  Root: AccordionRoot,
  Item: AccordionItem,
  Header: AccordionHeader,
  Trigger: AccordionTrigger,
  Panel: AccordionPanel,
};
components/ui/accordion.css
accordion.css
/* Roblox 표면 스타일 위에 프로젝트의 펼침/접힘 모션을 적용합니다. */
.rbx-accordion {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.rbx-accordion-item {
  border: 1px solid var(--rbx-color-stroke-default);
  border-radius: 8px;
  overflow: hidden;
  padding: 8px 12px;
}
.rbx-accordion-header {
  margin: 0;
}
.rbx-accordion-trigger {
  width: 100%;
  min-height: 40px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  border: 0;
  background: transparent;
  color: var(--rbx-color-content-default);
  font: var(--rbx-typography-title-medium-font);
  text-align: left;
  cursor: pointer;
}
.rbx-accordion-panel {
  color: var(--rbx-color-content-default);
  overflow: hidden;
  font: var(--rbx-typography-body-medium-font);
  white-space: pre-wrap;
  padding-bottom: 12px;
}
.rbx-accordion-chevron {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--rbx-color-content-emphasis);
}
.rbx-accordion-chevron > :last-child {
  display: none;
}
.rbx-accordion-trigger[data-panel-open] .rbx-accordion-chevron > :first-child {
  display: none;
}
.rbx-accordion-trigger[data-panel-open] .rbx-accordion-chevron > :last-child {
  display: inline-block;
}
@media (min-width: 1141px) {
  .rbx-accordion {
    gap: 16px;
  }
}

.rbx-accordion-panel {
  height: var(--accordion-panel-height, var(--collapsible-panel-height, auto));
  transition:
    height var(--rbx-motion-duration-enter) var(--rbx-motion-ease-enter),
    opacity var(--rbx-motion-duration-enter) var(--rbx-motion-ease-enter),
    padding-bottom var(--rbx-motion-duration-enter) var(--rbx-motion-ease-enter);
}
.rbx-accordion-panel:is([data-starting-style], [data-ending-style]) {
  height: 0;
  opacity: 0;
  padding-bottom: 0;
}
.rbx-accordion-panel[data-ending-style] {
  transition-timing-function: var(--rbx-motion-ease-exit);
}

목차