컴포넌트
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
"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
/* 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);
}