الأكورديون
أقسام يفتحها القارئ واحدًا تلو الآخر. اللوحات المغلقة تبقى في HTML، فيقرؤها الزاحف ويفتحها البحث في الصفحة.
split - الحجم لم يُقَس بعد
الأكورديون
split - الحجم لم يُقَس بعدأقسام يفتحها القارئ واحدًا تلو الآخر. اللوحات المغلقة تبقى في HTML، فيقرؤها الزاحف ويفتحها البحث في الصفحة.
جرّبه
hiddenUntilFound
headingLevel
الشيفرة
accordion.tsx
import { AccordionItem, AccordionRoot } from 'topclassui/accordion'
<AccordionRoot className="max-w-105">
<AccordionItem value="server" title="What is a server component?">
<p>A component that renders on the server and sends HTML, with no JavaScript of its own.</p>
</AccordionItem>
<AccordionItem value="arabic" title="Does it work in Arabic?">
<p>Yes. The caret moves to the left edge and the text starts at the right one.</p>
</AccordionItem>
<AccordionItem value="bundle" title="How big is the bundle?">
<p>About one kilobyte in the browser. The answers cost nothing: they are plain HTML.</p>
</AccordionItem>
</AccordionRoot>اللوحة المغلقة hidden="until-found": بحث المتصفح أو رابط #id أو جزء نصي في العنوان يفتحها.
كل الحالات، بدون تمرير الفأرة
حالتانأو استورده من الحزمة
topclassui/accordion
import { AccordionRoot, AccordionItem } from 'topclassui/accordion'ثبّت مرة واحدة:npm i topclassui@beta zod
أمثلة
- جزءان بأسماء مسطّحة، ولا يوجد
Accordionوحده:AccordionRootوAccordionItem. العنصر يحتاجvalueوtitle، وأبناؤه هم اللوحة. - مفتوح من البداية:
defaultValue={['install']}على الجذر، وهي قيمvalueللعناصر التي تبدأ مفتوحة. بيانات بسيطة، فتمرّرها صفحة خادم. - عنصر واحد في كل مرة هو الافتراضي، والضغط على العنصر المفتوح يغلقه.
multipleيترك العناصر الأخرى كما هي. - العنوان نص أو عناصر سطرية، مثل أيقونة أو
Badge. لا رابط ولا زر: العنوان داخل زر. ضع الرابط في اللوحة. - الجدول الأعرض من الصندوق يمرّر أفقيًا داخل لوحته. العنوان الطويل يلتف على أسطر ولا يُقصّ أبدًا.
hiddenUntilFound={false}على الجذر يُخرج اللوحات المغلقة من الصفحة، وkeepMountedيبقيها حينها مخفية عن البحث أيضًا.- من صفحة خادم مرّر النصوص والقيم المنطقية و
defaultValue. أماvalueمعonValueChange، وonOpenChange، فتحتاج ملفًا فيه'use client'.
basic
basic.tsx
import { AccordionItem, AccordionRoot } from 'topclassui/accordion'
/** The drawn sample: three questions, all closed, one open at a time. Each answer is in the
* HTML from the start, hidden until the reader opens it or the browser finds a word in it. */
export default function Demo() {
return (
<AccordionRoot className="max-w-105">
<AccordionItem value="server" title="What is a server component?" headingLevel={2}>
<p>A component that renders on the server and sends HTML, with no JavaScript of its own.</p>
</AccordionItem>
<AccordionItem value="arabic" title="Does it work in Arabic?" headingLevel={2}>
<p>Yes. The caret moves to the left edge and the text starts at the right one.</p>
</AccordionItem>
<AccordionItem value="bundle" title="How big is the bundle?" headingLevel={2}>
<p>About one kilobyte in the browser. The answers cost nothing: they are plain HTML.</p>
</AccordionItem>
</AccordionRoot>
)
}
first-open
first-open.tsx
import { AccordionItem, AccordionRoot } from 'topclassui/accordion'
/** The drawn state: the first item starts open. defaultValue names it by its value, and is
* plain data, so a server page can pass it. */
export default function Demo() {
return (
<AccordionRoot defaultValue={['install']} className="max-w-105">
<AccordionItem value="install" title="How do I install a component?" headingLevel={2}>
<p>Run npx topclassui add accordion. The files land in your own repository.</p>
</AccordionItem>
<AccordionItem value="arabic" title="Does it work in Arabic?" headingLevel={2}>
<p>Yes. The caret moves to the left edge and the text starts at the right one.</p>
</AccordionItem>
<AccordionItem value="bundle" title="How big is the bundle?" headingLevel={2}>
<p>About one kilobyte in the browser. The answers cost nothing: they are plain HTML.</p>
</AccordionItem>
</AccordionRoot>
)
}
multiple
multiple.tsx
import { AccordionItem, AccordionRoot } from 'topclassui/accordion'
/** multiple: opening one item leaves the others as they are. Two start open. */
export default function Demo() {
return (
<AccordionRoot multiple defaultValue={['plan', 'seats']} className="max-w-105">
<AccordionItem value="plan" title="Plan" headingLevel={2}>
<p>Team, billed every month.</p>
</AccordionItem>
<AccordionItem value="seats" title="Seats" headingLevel={2}>
<p>Twelve of twenty are in use.</p>
</AccordionItem>
<AccordionItem value="invoices" title="Invoices" headingLevel={2}>
<p>The last one was paid on 2 March.</p>
</AccordionItem>
</AccordionRoot>
)
}
disabled
disabled.tsx
import { AccordionItem, AccordionRoot } from 'topclassui/accordion'
/** A disabled item between two that work: its trigger keeps its place in the tab order, says
* that it is disabled, and does nothing. Its panel stays closed. */
export default function Demo() {
return (
<AccordionRoot className="max-w-105">
<AccordionItem value="profile" title="Profile" headingLevel={2}>
<p>Your name and your picture.</p>
</AccordionItem>
<AccordionItem value="billing" title="Billing" headingLevel={2} disabled>
<p>Only the owner of the workspace can change the plan.</p>
</AccordionItem>
<AccordionItem value="members" title="Members" headingLevel={2}>
<p>Invite people and choose what each one can do.</p>
</AccordionItem>
</AccordionRoot>
)
}
long-content
long-content.tsx
import { Badge } from 'topclassui/badge'
import { Button } from 'topclassui/button'
import { AccordionItem, AccordionRoot } from 'topclassui/accordion'
const WORD = 'a'.repeat(300)
const COLUMNS = ['Order', 'Customer', 'Placed', 'Status', 'Items', 'Total', 'Warehouse', 'Carrier']
const ORDERS = [
['ORD-5521', 'Northwind Traders', '12 March 2026', 'Awaiting payment', '14', '$1,250.00', 'Amman, building 4', 'Aramex'],
['ORD-5519', 'Fabrikam Incorporated', '11 March 2026', 'Awaiting a refund', '120', '$9,120.50', 'Irbid, building 1', 'DHL Express'],
]
/** Hostile content. A title of one 300-letter word wraps beside the caret. A title that is an element
* keeps its badge. A table wider than the box scrolls inside its panel, which is then a tab stop; it
* starts closed, because that needs the browser. An accordion inside a panel takes the next level. */
export default function Demo() {
return (
<AccordionRoot multiple defaultValue={['word', 'element', 'nested']} className="max-w-105">
<AccordionItem value="word" title={WORD} headingLevel={2}>
<Button size="sm" variant="outline" render={<a href="/orders" />}>See every order</Button>
<p className="pt-2">{WORD}</p>
</AccordionItem>
<AccordionItem
value="element"
headingLevel={2}
title={<span className="flex flex-wrap items-center gap-2"><span>Invoices</span><Badge tone="info">3 new</Badge></span>}
>
<p>Three invoices arrived since your last visit.</p>
</AccordionItem>
<AccordionItem value="table" title="Orders that wait for a payment" headingLevel={2}>
<table className="w-max whitespace-nowrap">
<thead>
<tr>
{COLUMNS.map((column) => <th key={column} scope="col" className="pe-6 pb-2 text-start font-medium">{column}</th>)}
</tr>
</thead>
<tbody>
{ORDERS.map((order) => (
<tr key={order[0]}>{order.map((cell) => <td key={cell} className="pe-6 pb-2">{cell}</td>)}</tr>
))}
</tbody>
</table>
</AccordionItem>
<AccordionItem value="nested" title="Shipping" headingLevel={2}>
<AccordionRoot>
<AccordionItem value="inside" title="Inside the country" headingLevel={3}>
<p>Two working days.</p>
</AccordionItem>
<AccordionItem value="abroad" title="Abroad" headingLevel={3}>
<p>Five to nine working days.</p>
</AccordionItem>
</AccordionRoot>
</AccordionItem>
</AccordionRoot>
)
}
many-items
many-items.tsx
import { AccordionItem, AccordionRoot } from 'topclassui/accordion'
const QUESTIONS = Array.from({ length: 60 }, (_, index) => index + 1)
/** Sixty items. None is windowed: every answer is in the HTML, which is the point of the
* component, and the page scrolls as any long page does. */
export default function Demo() {
return (
<AccordionRoot className="max-w-105">
{QUESTIONS.map((number) => (
<AccordionItem key={number} value={`question-${String(number)}`} title={`Question ${String(number)}`} headingLevel={2}>
<p>The answer to question {number}.</p>
</AccordionItem>
))}
</AccordionRoot>
)
}
بالعربية، من اليمين إلى اليسار
rtl.tsx
import { TopClassProvider } from 'topclassui/provider'
import { AccordionItem, AccordionRoot } from 'topclassui/accordion'
/** Arabic, inside TopClassProvider as the root layout would mount it. Nothing here is told the
* direction: the title starts at the right and the caret sits at the left, the inline end. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar">
<AccordionRoot defaultValue={['server']} className="max-w-105">
<AccordionItem value="server" title="ما هو مكوّن الخادم؟" headingLevel={2}>
<p>مكوّن يُعرض على الخادم ويرسل HTML فقط، دون أي JavaScript خاص به.</p>
</AccordionItem>
<AccordionItem value="arabic" title="هل يعمل بالعربية؟" headingLevel={2}>
<p>نعم. ينتقل السهم إلى الحافة اليسرى ويبدأ النص من اليمنى.</p>
</AccordionItem>
<AccordionItem value="bundle" title="ما حجم الحزمة؟" headingLevel={2}>
<p>نحو كيلوبايت واحد في المتصفح. الإجابات نفسها لا تكلّف شيئًا.</p>
</AccordionItem>
</AccordionRoot>
</div>
</TopClassProvider>
)
}
الخصائص
AccordionRoot
AccordionItem
لوحة المفاتيح وإمكانية الوصول
- اضبط
headingLevelعلى كل عنصر، من 2 إلى 6 (الافتراضي 3): أعلى بدرجة من العنوان الذي فوق الأكورديون. القيمة محصورة في هذا المدى. - يسمع قارئ الشاشة زر الفتح زرًّا داخل عنوان، موسَّعًا أو مطويًّا، واللوحة المفتوحة منطقة اسمها عنوانها.
- العنصر المعطّل بـ
disabledيبقي زره في ترتيب التنقل ويعلن أنه معطّل. تقليل الحركة يلغي التلاشي ذا 150 ms.
من اليمين إلى اليسار
لا شيء يُخبَر بالاتجاه: مع dir="rtl" يبدأ العنوان من اليمين ويجلس السهم على اليسار، في نهاية السطر.
الخادم والمتصفح
يُعرض على الخادم، والجزء التفاعلي مكوّن متصفح لا يُرسل إلا عند استخدامه.
الملفات: index.tsx, client.tsx.
على الشارة: split - الحجم لم يُقَس بعد.