الشريط الجانبي
شريط جانبي روابطه تُعرض على الخادم، وينطوي إلى عمود من الأيقونات. الشريط وزرّه وحدهما يعملان في المتصفح.
split - الحجم لم يُقَس بعد
الشريط الجانبي
split - الحجم لم يُقَس بعدشريط جانبي روابطه تُعرض على الخادم، وينطوي إلى عمود من الأيقونات. الشريط وزرّه وحدهما يعملان في المتصفح.
جرّبه
defaultOpen
open
current
الشيفرة
sidebar.tsx
import { folder } from 'topclassui/icons/folder'
import { gear } from 'topclassui/icons/gear'
import { house } from 'topclassui/icons/house'
import { Svg } from 'topclassui/svg'
import { SidebarBody, SidebarGroup, SidebarHeader, SidebarLink, SidebarRoot } from 'topclassui/sidebar'
<div className="h-96">
<SidebarRoot>
<SidebarHeader>Workspace</SidebarHeader>
<SidebarBody>
<SidebarGroup>
<SidebarLink href="#home" icon={<Svg icon={house} />}>Home</SidebarLink>
<SidebarLink href="#projects" icon={<Svg icon={folder} />}>Projects</SidebarLink>
<SidebarLink href="#settings" icon={<Svg icon={gear} />}>Settings</SidebarLink>
</SidebarGroup>
</SidebarBody>
</SidebarRoot>
</div>مع defaultOpen={false} يرسل الخادم الشريط مطويًّا، فلا يتحرك شيء عند الترطيب.
كل الحالات، بدون تمرير الفأرة
حالتانأمثلة
- ستة أجزاء بأسماء مسطّحة، ولا يوجد
Sidebarوحده.SidebarRootوSidebarTriggerيعملان في المتصفح، وSidebarHeaderوSidebarBodyوSidebarGroupوSidebarLinkHTML من الخادم.SidebarHeaderيرسم زر الطي بنفسه. - أعطِ العنصر الأب للشريط ارتفاعًا، مثل
h-dvh. عندها تمرّر القائمة الطويلة داخلSidebarBody، ولا تتحرك الصفحة. - في صف flex يحافظ الشريط على عرضه 220 px، أو 60 وهو مطوي. أعطِ العمود المجاور له
min-w-0 flex-1. - الرابط يأخذ
href، أوrender={<Link href="/docs" />}في Next.js. كل خاصية أخرى تذهب إلى الرابط، لا إلى عنصر القائمة حوله. - أعطِ كل رابط
iconإن كان الشريط ينطوي: وهو مطوي، الأيقونة هي كل ما يُرسم. openمعonOpenChangeيحتاجان ملفًا فيه'use client'، كما في المثال المتحكَّم به. عندها يستطيع زر خارج الشريط أن يطويه.- لعرض آخر:
className="data-open:w-72"علىSidebarRoot، ويبقى عرض الشريط المطوي كما هو. على الهاتف ابدأ به مطويًّا: صيغة الدرج لم تُبنَ بعد.
basic
basic.tsx
import { folder } from 'topclassui/icons/folder'
import { gear } from 'topclassui/icons/gear'
import { house } from 'topclassui/icons/house'
import { users } from 'topclassui/icons/users'
import { Svg } from 'topclassui/svg'
import { SidebarBody, SidebarGroup, SidebarHeader, SidebarLink, SidebarRoot } from 'topclassui/sidebar'
/** The drawn rail: the toggle and a title, then four links, of which Home is the page being
* read. Every link is written here, on the server; only the rail and its toggle hydrate. */
export default function Demo() {
return (
<div className="h-96">
<SidebarRoot>
<SidebarHeader>Workspace</SidebarHeader>
<SidebarBody>
<SidebarGroup>
<SidebarLink href="#home" icon={<Svg icon={house} />} current>Home</SidebarLink>
<SidebarLink href="#projects" icon={<Svg icon={folder} />}>Projects</SidebarLink>
<SidebarLink href="#team" icon={<Svg icon={users} />}>Team</SidebarLink>
<SidebarLink href="#settings" icon={<Svg icon={gear} />}>Settings</SidebarLink>
</SidebarGroup>
</SidebarBody>
</SidebarRoot>
</div>
)
}
groups
groups.tsx
import { bell } from 'topclassui/icons/bell'
import { folder } from 'topclassui/icons/folder'
import { gear } from 'topclassui/icons/gear'
import { house } from 'topclassui/icons/house'
import { signOut } from 'topclassui/icons/sign-out'
import { users } from 'topclassui/icons/users'
import { Svg } from 'topclassui/svg'
import { SidebarBody, SidebarGroup, SidebarHeader, SidebarLink, SidebarRoot } from 'topclassui/sidebar'
/** Two groups, each a list named by its label. Collapsed, the labels are hidden and a
* screen reader still hears "Workspace, list" and "Account, list". */
export default function Demo() {
return (
<div className="h-112">
<SidebarRoot>
<SidebarHeader>Acme</SidebarHeader>
<SidebarBody>
<SidebarGroup label="Workspace">
<SidebarLink href="#home" icon={<Svg icon={house} />}>Home</SidebarLink>
<SidebarLink href="#projects" icon={<Svg icon={folder} />} current>Projects</SidebarLink>
<SidebarLink href="#team" icon={<Svg icon={users} />}>Team</SidebarLink>
</SidebarGroup>
<SidebarGroup label="Account">
<SidebarLink href="#notifications" icon={<Svg icon={bell} />}>Notifications</SidebarLink>
<SidebarLink href="#settings" icon={<Svg icon={gear} />}>Settings</SidebarLink>
<SidebarLink href="#sign-out" icon={<Svg icon={signOut} />}>Sign out</SidebarLink>
</SidebarGroup>
</SidebarBody>
</SidebarRoot>
</div>
)
}
collapsed
collapsed.tsx
import { folder } from 'topclassui/icons/folder'
import { gear } from 'topclassui/icons/gear'
import { house } from 'topclassui/icons/house'
import { users } from 'topclassui/icons/users'
import { Svg } from 'topclassui/svg'
import { SidebarBody, SidebarGroup, SidebarHeader, SidebarLink, SidebarRoot } from 'topclassui/sidebar'
/** Collapsed from the start: the server sends the 60 px column of icons, so nothing moves
* when the page hydrates. Each label is hidden and still names its link. */
export default function Demo() {
return (
<div className="h-96">
<SidebarRoot defaultOpen={false}>
<SidebarHeader>Workspace</SidebarHeader>
<SidebarBody>
<SidebarGroup>
<SidebarLink href="#home" icon={<Svg icon={house} />} current>Home</SidebarLink>
<SidebarLink href="#projects" icon={<Svg icon={folder} />}>Projects</SidebarLink>
<SidebarLink href="#team" icon={<Svg icon={users} />}>Team</SidebarLink>
<SidebarLink href="#settings" icon={<Svg icon={gear} />}>Settings</SidebarLink>
</SidebarGroup>
</SidebarBody>
</SidebarRoot>
</div>
)
}
controlled
The rail asked to change 0 times.
controlled.tsx
'use client'
import { useState } from 'react'
import { folder } from 'topclassui/icons/folder'
import { house } from 'topclassui/icons/house'
import { Button } from 'topclassui/button'
import { Svg } from 'topclassui/svg'
import { SidebarBody, SidebarGroup, SidebarHeader, SidebarLink, SidebarRoot } from 'topclassui/sidebar'
/** open and onOpenChange need a client file: a Server Component page cannot pass a function.
* Here the page holds the state, so a button outside the rail toggles it as its own does. */
export default function Demo() {
const [open, setOpen] = useState(true)
const [changes, setChanges] = useState(0)
return (
<div className="flex h-96 flex-wrap gap-4">
<SidebarRoot open={open} onOpenChange={(next) => { setOpen(next); setChanges((count) => count + 1) }}>
<SidebarHeader>Workspace</SidebarHeader>
<SidebarBody>
<SidebarGroup>
<SidebarLink href="#home" icon={<Svg icon={house} />} current>Home</SidebarLink>
<SidebarLink href="#projects" icon={<Svg icon={folder} />}>Projects</SidebarLink>
</SidebarGroup>
</SidebarBody>
</SidebarRoot>
<div className="grid min-w-0 grow basis-40 content-start gap-3">
<Button variant="outline" className="justify-self-start" onClick={() => { setOpen((current) => !current) }}>
{open ? 'Hide the labels' : 'Show the labels'}
</Button>
<p className="text-sm text-muted">The rail asked to change {changes} times.</p>
</div>
</div>
)
}
in-a-layout
Orders
in-a-layout.tsx
import { folder } from 'topclassui/icons/folder'
import { gear } from 'topclassui/icons/gear'
import { house } from 'topclassui/icons/house'
import { users } from 'topclassui/icons/users'
import { Svg } from 'topclassui/svg'
import { Table, type TableColumn } from 'topclassui/table'
import { SidebarBody, SidebarGroup, SidebarHeader, SidebarLink, SidebarRoot } from 'topclassui/sidebar'
interface Order { id: string; customer: string; placed: string; status: string; total: string }
const orders: Order[] = [
{ id: 'ORD-5521', customer: 'Northwind Traders', placed: '12 Mar 2026', status: 'Awaiting payment', total: '$1,250.00' },
{ id: 'ORD-5520', customer: 'Contoso Ltd', placed: '11 Mar 2026', status: 'Shipped', total: '$480.00' },
{ id: 'ORD-5519', customer: 'Fabrikam Inc', placed: '11 Mar 2026', status: 'Awaiting payment', total: '$9,120.50' },
]
const columns: TableColumn<Order>[] = [
{ key: 'id', header: 'Order', rowHeader: true, cell: (order) => order.id },
{ key: 'customer', header: 'Customer', cell: (order) => order.customer },
{ key: 'placed', header: 'Placed', cell: (order) => order.placed },
{ key: 'status', header: 'Status', cell: (order) => order.status },
{ key: 'total', header: 'Total', align: 'end', cell: (order) => order.total },
]
/** The app shell: a flex row as tall as the screen, the rail, then a column that takes the
* rest. The rail keeps its width; the column gives way, and its table scrolls inside it. */
export default function Demo() {
return (
<div className="flex h-[calc(100dvh-4rem)]">
<SidebarRoot>
<SidebarHeader>Workspace</SidebarHeader>
<SidebarBody>
<SidebarGroup>
<SidebarLink href="#home" icon={<Svg icon={house} />}>Home</SidebarLink>
<SidebarLink href="#orders" icon={<Svg icon={folder} />} current>Orders</SidebarLink>
<SidebarLink href="#team" icon={<Svg icon={users} />}>Team</SidebarLink>
<SidebarLink href="#settings" icon={<Svg icon={gear} />}>Settings</SidebarLink>
</SidebarGroup>
</SidebarBody>
</SidebarRoot>
<div className="min-w-0 flex-1 overflow-y-auto p-4">
<h2 className="pb-3 text-base font-semibold">Orders</h2>
<Table caption="Orders" items={orders} getKey={(order) => order.id} columns={columns} />
</div>
</div>
)
}
long-list
long-list.tsx
import { fileText } from 'topclassui/icons/file-text'
import { Svg } from 'topclassui/svg'
import { SidebarBody, SidebarGroup, SidebarHeader, SidebarLink, SidebarRoot } from 'topclassui/sidebar'
const PAGES = Array.from({ length: 60 }, (_, index) => `Page ${String(index + 1)}`)
/** Sixty links in a rail as tall as the screen: the list scrolls inside the body, under a
* header that stays put, and the page does not move. The parent gives the rail its height. */
export default function Demo() {
return (
<div className="h-[calc(100dvh-4rem)]">
<SidebarRoot>
<SidebarHeader>Handbook</SidebarHeader>
<SidebarBody>
<SidebarGroup label="Pages">
{PAGES.map((page, index) => (
<SidebarLink key={page} href={`#page-${String(index + 1)}`} icon={<Svg icon={fileText} />} current={index === 0}>
{page}
</SidebarLink>
))}
</SidebarGroup>
</SidebarBody>
</SidebarRoot>
</div>
)
}
بالعربية، من اليمين إلى اليسار
rtl.tsx
import { folder } from 'topclassui/icons/folder'
import { gear } from 'topclassui/icons/gear'
import { house } from 'topclassui/icons/house'
import { users } from 'topclassui/icons/users'
import { TopClassProvider } from 'topclassui/provider'
import { Svg } from 'topclassui/svg'
import { SidebarBody, SidebarGroup, SidebarHeader, SidebarLink, SidebarRoot } from 'topclassui/sidebar'
/** Arabic, inside TopClassProvider as the root layout would mount it. The rail's line is on
* its left, the toggle at its right with its icon mirrored, and both are named in Arabic. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar" className="h-96">
<SidebarRoot>
<SidebarHeader>مساحة العمل</SidebarHeader>
<SidebarBody>
<SidebarGroup label="التنقل">
<SidebarLink href="#home" icon={<Svg icon={house} />} current>الرئيسية</SidebarLink>
<SidebarLink href="#projects" icon={<Svg icon={folder} />}>المشاريع</SidebarLink>
<SidebarLink href="#team" icon={<Svg icon={users} />}>الفريق</SidebarLink>
<SidebarLink href="#settings" icon={<Svg icon={gear} />}>الإعدادات</SidebarLink>
</SidebarGroup>
</SidebarBody>
</SidebarRoot>
</div>
</TopClassProvider>
)
}
الخصائص
SidebarRoot
SidebarTrigger
لا خصائص خاصة به: يأخذ سمات العنصر الذي يعرضه.
SidebarHeader
SidebarBody
SidebarGroup
SidebarLink
لوحة المفاتيح وإمكانية الوصول
- زر الطي زر حقيقي عليه
aria-expanded، واسمه ما ستفعله الضغطة: «طي الشريط الجانبي» أو «توسيع الشريط الجانبي». currentعلى رابطSidebarLinkواحد يضعaria-current="page"ويرسم الصف معبّأً وعريض الخط وبشريط عند بدايته.- وهو مطوي، يختفي العنوان واسم كل مجموعة وتسمية كل رابط عن الشاشة، ويبقى قارئ الشاشة يقرؤها.
من اليمين إلى اليسار
مع dir="rtl" يكون خط الشريط على يساره وزر الطي على يمينه، وأيقونته معكوسة. TopClassProvider مع locale="ar" يسمّي الاثنين بالعربية.
الخادم والمتصفح
يُعرض على الخادم، والجزء التفاعلي مكوّن متصفح لا يُرسل إلا عند استخدامه.
الملفات: index.tsx, client.tsx.
على الشارة: split - الحجم لم يُقَس بعد.