reopt designreopt design
DocsExploreToolsPricingBuilder
Start
Overview
Start
Next.js 설치
Private install
Core Concepts
아키텍처
Composition Patterns
Accessibility
Keyboard Patterns
Styling
Theme System
Advanced Patterns
Build & Operate
Skills
AI Integration
CLI (opt surface add)
Dependency Graph
Tools
Canvas Catalog
Theme Builder
Form Builder
Templates
Templates
Releases
Release Notes
Oopt-ui
reopt designreopt design

A design system for the AI era

  • Docs
  • Pricing
  • Releases
  • GitHub
  • Terms of Service
  • Privacy Policy

© 2026 reopt-ai. All rights reserved.

Core Concepts
  1. Docs
  2. /
  3. Core Concepts
  4. /
  5. Composition Patterns

Composition patterns

How Shells wrap Core to extend functionality, plus a guide for building your own custom Shell.

reopt design · Updated Jun 26, 2026

Shell → Core composition

A Shell imports Core, wraps it, and extends its functionality. We'll use ContentTabs as the example.

tsx
"use client";

import { type TabDef, TabsRoot, TabList, Tab, TabPanel } from "@reopt-ai/opt-ui";

export interface ContentTabsProps {
  tabs: TabDef[];
  defaultTabId?: string;
}

export function ContentTabs({ tabs, defaultTabId }: ContentTabsProps) {
  return (
    <TabsRoot defaultSelectedId={defaultTabId ?? tabs[0]?.id}>
      <TabList aria-label="콘텐츠 탭">
        {tabs.map((tab) => (
          <Tab key={tab.id} id={tab.id}>{tab.label}</Tab>
        ))}
      </TabList>
      {tabs.map((tab) => (
        <TabPanel key={tab.id} tabId={tab.id}>
          {tab.content}
        </TabPanel>
      ))}
    </TabsRoot>
  );
}

Pattern summary: a Shell (1) defines its data interface, (2) imports Core, and (3) maps data onto Core. The Shell itself contains no styling code.

Data-driven rendering

A Shell accepts a data array via props and maps it onto Core internally. Thanks to this pattern, the consumer only has to prepare the data.

tsx
// 사용처 (서버 컴포넌트)
const tabs: TabDef[] = [
  { id: "overview", label: "개요", content: <Overview /> },
  { id: "api", label: "API", content: <ApiDocs /> },
];

// Shell에 데이터 전달 — 렌더링은 Shell이 담당
<ContentTabs tabs={tabs} />

Provider nesting

FaqAccordion nests multiple DisclosureRoot instances inside a CompositeProvider. The Composite handles keyboard navigation while each DisclosureRoot manages its own open/close state.

tsx
"use client";

import {
  CompositeZone,
  CompositeRow,
  CompositeItem,
  DisclosureRoot,
  DisclosureTrigger,
  DisclosureContent,
  type FaqItem,
} from "@reopt-ai/opt-ui";

export function FaqAccordion({ items }: { items: FaqItem[] }) {
  return (
    <CompositeProvider focusLoop orientation="vertical">
      <Composite role="region" aria-label="자주 묻는 질문" className="...">
        {items.map((faq) => (
          <DisclosureRoot key={faq.id}>
            <div>
              <CompositeItem render={<DisclosureTrigger />}>
                {faq.question}
              </CompositeItem>
              <DisclosureContent>
                <p>{faq.answer}</p>
              </DisclosureContent>
            </div>
          </DisclosureRoot>
        ))}
      </Composite>
    </CompositeProvider>
  );
}

Passing DisclosureTrigger to CompositeItem's render prop uses a render prop to compose a Composite item and a Disclosure trigger into a single element.

Building a custom Shell

You can build a new Shell in four steps.

Step 1. Pick the Core

Choose the Core that matches your UI pattern. For example, a notifications list needs Disclosure (open/close) and Composite (keyboard navigation).

Step 2. Define the data interface

tsx
// lib/types.ts에 추가
export interface NotificationItem {
  id: string;
  title: string;
  body: string;
  time: string;
}

Step 3. Compose Core

tsx
// shells/notification-list.tsx
"use client";

import {
  CompositeZone,
  CompositeRow,
  CompositeItem,
  DisclosureRoot,
  DisclosureTrigger,
  DisclosureContent,
  type NotificationItem,
} from "@reopt-ai/opt-ui";

export interface NotificationListProps {
  items: NotificationItem[];
}

export function NotificationList({ items }: NotificationListProps) {
  return (
    <CompositeProvider focusLoop orientation="vertical">
      <Composite role="region" aria-label="알림 목록" className="...">
        {items.map((item) => (
          <DisclosureRoot key={item.id}>
            <div>
              <CompositeItem render={<DisclosureTrigger />}>
                <span>{item.title}</span>
                <time>{item.time}</time>
              </CompositeItem>
              <DisclosureContent>
                <p>{item.body}</p>
              </DisclosureContent>
            </div>
          </DisclosureRoot>
        ))}
      </Composite>
    </CompositeProvider>
  );
}

Step 4. Add the export

Add a barrel export in shells/index.ts and you're done.

tsx
// shells/index.ts
export { NotificationList } from "./notification-list";
Previous아키텍처opt-ui Core/Shells/Surfaces와 opt-charts 분리, docs/registry 연결 방식을 설명합니다.Core Concepts
Go to 아키텍처
NextAccessibilitySpatial Navigation, 포커스 관리, WAI-ARIA 역할, 키보드 단축키Core Concepts