Table
טבלה מובנית עם מתאם אינטראקטיבי אופציונלי.
- דפדפנים
- Chrome 123+Firefox 121+Safari 17.5+
- שכבה
- 0
- גודל חבילה דחוסה
- 617 B
- מגבלת פריסה כפויה
- 0 ms
- מגבלת Axe
- 0
- תואם SSR
- כן
- חוזה מפורסם
- סביבת בדיקות
מגרש ניסויים
import { Table } from "@gunkin/uiify";
const rows = [
{ id: "alice", name: "Alice Chen", role: "Admin", status: "Active" },
{ id: "bob", name: "Bob Smith", role: "Editor", status: "Active" },
{ id: "carol", name: "Carol Lee", role: "Viewer", status: "Invited" },
];
export default function Example() {
return (
<Table.Root className="" variant="outline" size="sm">
<Table.Caption className="">Team members</Table.Caption>
<Table.Header className="">
<Table.Row className="" id="team-head">
<Table.ColumnHeader className="" id="team-name">
Name
</Table.ColumnHeader>
<Table.ColumnHeader className="" id="team-role">
Role
</Table.ColumnHeader>
<Table.ColumnHeader className="" id="team-status">
Status
</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body className="">
{rows.map((row) => (
<Table.Row className="" id={row.id} key={row.id}>
<Table.RowHeader className="">{row.name}</Table.RowHeader>
<Table.Cell className="">{row.role}</Table.Cell>
<Table.Cell className="">{row.status}</Table.Cell>
</Table.Row>
))}
</Table.Body>
<Table.Footer className="">
<Table.Row className="" id="team-total">
<Table.Cell className="" colSpan={3}>
{rows.length} members · {rows.filter((row) => row.status === "Active").length} active
</Table.Cell>
</Table.Row>
</Table.Footer>
</Table.Root>
);
}@layer components {
[data-uiify-table] {
inline-size: 100%;
border-collapse: separate;
border-spacing: 0;
background: var(--card);
color: var(--card-foreground);
font-size: var(--text-sm);
}
[data-uiify-table][data-layout="fixed"] {
table-layout: fixed;
}
[data-uiify-table] :is(th, td) {
padding-block: 0.75rem;
padding-inline: 1rem;
text-align: start;
vertical-align: middle;
border-block-end: 1px solid var(--border);
background: inherit;
}
[data-uiify-table] :is(th, td)[data-align="center"] {
text-align: center;
}
[data-uiify-table] :is(th, td)[data-align="end"] {
text-align: end;
}
[data-uiify-table] :is(thead, tfoot) {
background: var(--secondary);
font-weight: 600;
}
[data-uiify-table] tbody tr {
background: var(--card);
}
[data-uiify-table] tbody tr:hover {
background: var(--secondary);
}
[data-uiify-table] tbody tr[data-selected] {
background: var(--accent);
}
[data-uiify-table] caption {
padding-block: 0.75rem;
text-align: start;
color: var(--muted-foreground);
}
[data-uiify-table][data-size="sm"] :is(th, td) {
padding-block: 0.5rem;
padding-inline: 0.75rem;
}
[data-uiify-table][data-size="lg"] :is(th, td) {
padding-block: 1rem;
padding-inline: 1.25rem;
}
[data-uiify-table][data-variant="outline"] {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
}
[data-uiify-table][data-variant="ghost"] {
background: transparent;
}
[data-uiify-table] :is(thead, tfoot, tr)[data-sticky] {
position: sticky;
inset-block-start: var(--uiify-table-sticky-offset, 0);
z-index: 2;
}
[data-uiify-table] tfoot[data-sticky] {
inset-block: auto var(--uiify-table-sticky-offset, 0);
}
[data-uiify-table] :is(th, td)[data-sticky] {
position: sticky;
z-index: 1;
}
[data-uiify-table] :is(th, td)[data-sticky="start"] {
inset-inline-start: var(--uiify-table-sticky-offset, 0);
}
[data-uiify-table] :is(th, td)[data-sticky="end"] {
inset-inline-end: var(--uiify-table-sticky-offset, 0);
}
[data-uiify-table] :is(thead, tfoot)[data-sticky] :is(th, td)[data-sticky] {
z-index: 3;
}
[data-uiify-table] th > button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}
[data-uiify-table] th[aria-sort="ascending"] > button::after {
content: "↑" / "";
}
[data-uiify-table] th[aria-sort="descending"] > button::after {
content: "↓" / "";
}
[data-uiify-table] [data-part="expanded-row"] {
background: var(--secondary);
}
[data-uiify-table] :is(button, tr):focus-visible {
outline: 2px solid var(--ring);
outline-offset: -2px;
}
}import { Table } from "@gunkin/uiify";
const rows = [
{ id: "alice", name: "Alice Chen", role: "Admin", status: "Active" },
{ id: "bob", name: "Bob Smith", role: "Editor", status: "Active" },
{ id: "carol", name: "Carol Lee", role: "Viewer", status: "Invited" },
];
export default function Example() {
return (
<Table.Root
className={
"group/table [inline-size:100%] [border-collapse:separate] [border-spacing:0] bg-card text-card-foreground t" +
"ext-sm data-[layout=fixed]:table-fixed data-[variant=outline]:[border-width:1px] data-[variant=outline]:bor" +
"der-solid data-[variant=outline]:border-border data-[variant=outline]:rounded-lg data-[variant=ghost]:bg-tr" +
"ansparent [&_:is(thead,tfoot)[data-sticky]_:is(th,td)[data-sticky]]:z-[3] [&_th[aria-sort=ascending]>button" +
"::after]:content-[\"↑\"_/_\"\"] [&_th[aria-sort=descending]>button::after]:content-[\"↓\"_/_\"\"]"
}
variant="outline"
size="sm"
>
<Table.Caption className="[padding-block:0.75rem] text-start text-muted-foreground">Team members</Table.Caption>
<Table.Header
className="
bg-secondary [font-weight:600] data-[sticky]:sticky
data-[sticky]:[inset-block-start:var(--uiify-table-sticky-offset,_0)] data-[sticky]:z-[2]
"
>
<Table.Row
className="
group-data-[part=body]/table-body:bg-card group-data-[part=body]/table-body:hover:bg-secondary
group-data-[part=body]/table-body:data-[selected]:bg-accent focus-visible:[outline:2px_solid_var(--ring)]
focus-visible:[outline-offset:-2px] data-[sticky]:sticky
data-[sticky]:[inset-block-start:var(--uiify-table-sticky-offset,_0)] data-[sticky]:z-[2]
"
id="team-head"
>
<Table.ColumnHeader
className="
[padding-block:0.75rem] [padding-inline:1rem] text-start [vertical-align:middle]
[border-block-end-width:1px] [border-block-end-style:solid] [border-block-end-color:var(--border)]
[background:inherit] data-[align=center]:text-center data-[align=end]:text-end
group-data-[size=sm]/table:[padding-block:0.5rem] group-data-[size=sm]/table:[padding-inline:0.75rem]
group-data-[size=lg]/table:[padding-block:1rem] group-data-[size=lg]/table:[padding-inline:1.25rem]
data-[sticky]:sticky data-[sticky]:z-[1]
data-[sticky=start]:[inset-inline-start:var(--uiify-table-sticky-offset,_0)]
data-[sticky=end]:[inset-inline-end:var(--uiify-table-sticky-offset,_0)] [[data-sticky]_&]:z-[3]
"
id="team-name"
>
Name
</Table.ColumnHeader>
<Table.ColumnHeader
className="
[padding-block:0.75rem] [padding-inline:1rem] text-start [vertical-align:middle]
[border-block-end-width:1px] [border-block-end-style:solid] [border-block-end-color:var(--border)]
[background:inherit] data-[align=center]:text-center data-[align=end]:text-end
group-data-[size=sm]/table:[padding-block:0.5rem] group-data-[size=sm]/table:[padding-inline:0.75rem]
group-data-[size=lg]/table:[padding-block:1rem] group-data-[size=lg]/table:[padding-inline:1.25rem]
data-[sticky]:sticky data-[sticky]:z-[1]
data-[sticky=start]:[inset-inline-start:var(--uiify-table-sticky-offset,_0)]
data-[sticky=end]:[inset-inline-end:var(--uiify-table-sticky-offset,_0)] [[data-sticky]_&]:z-[3]
"
id="team-role"
>
Role
</Table.ColumnHeader>
<Table.ColumnHeader
className="
[padding-block:0.75rem] [padding-inline:1rem] text-start [vertical-align:middle]
[border-block-end-width:1px] [border-block-end-style:solid] [border-block-end-color:var(--border)]
[background:inherit] data-[align=center]:text-center data-[align=end]:text-end
group-data-[size=sm]/table:[padding-block:0.5rem] group-data-[size=sm]/table:[padding-inline:0.75rem]
group-data-[size=lg]/table:[padding-block:1rem] group-data-[size=lg]/table:[padding-inline:1.25rem]
data-[sticky]:sticky data-[sticky]:z-[1]
data-[sticky=start]:[inset-inline-start:var(--uiify-table-sticky-offset,_0)]
data-[sticky=end]:[inset-inline-end:var(--uiify-table-sticky-offset,_0)] [[data-sticky]_&]:z-[3]
"
id="team-status"
>
Status
</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body className="group/table-body">
{rows.map((row) => (
<Table.Row
className="
group-data-[part=body]/table-body:bg-card group-data-[part=body]/table-body:hover:bg-secondary
group-data-[part=body]/table-body:data-[selected]:bg-accent focus-visible:[outline:2px_solid_var(--ring)]
focus-visible:[outline-offset:-2px] data-[sticky]:sticky
data-[sticky]:[inset-block-start:var(--uiify-table-sticky-offset,_0)] data-[sticky]:z-[2]
"
id={row.id}
key={row.id}
>
<Table.RowHeader
className="
[padding-block:0.75rem] [padding-inline:1rem] text-start [vertical-align:middle]
[border-block-end-width:1px] [border-block-end-style:solid] [border-block-end-color:var(--border)]
[background:inherit] data-[align=center]:text-center data-[align=end]:text-end
group-data-[size=sm]/table:[padding-block:0.5rem] group-data-[size=sm]/table:[padding-inline:0.75rem]
group-data-[size=lg]/table:[padding-block:1rem] group-data-[size=lg]/table:[padding-inline:1.25rem]
data-[sticky]:sticky data-[sticky]:z-[1]
data-[sticky=start]:[inset-inline-start:var(--uiify-table-sticky-offset,_0)]
data-[sticky=end]:[inset-inline-end:var(--uiify-table-sticky-offset,_0)] [[data-sticky]_&]:z-[3]
"
>
{row.name}
</Table.RowHeader>
<Table.Cell
className="
[padding-block:0.75rem] [padding-inline:1rem] text-start [vertical-align:middle]
[border-block-end-width:1px] [border-block-end-style:solid] [border-block-end-color:var(--border)]
[background:inherit] data-[align=center]:text-center data-[align=end]:text-end
group-data-[size=sm]/table:[padding-block:0.5rem] group-data-[size=sm]/table:[padding-inline:0.75rem]
group-data-[size=lg]/table:[padding-block:1rem] group-data-[size=lg]/table:[padding-inline:1.25rem]
data-[sticky]:sticky data-[sticky]:z-[1]
data-[sticky=start]:[inset-inline-start:var(--uiify-table-sticky-offset,_0)]
data-[sticky=end]:[inset-inline-end:var(--uiify-table-sticky-offset,_0)] [[data-sticky]_&]:z-[3]
"
>
{row.role}
</Table.Cell>
<Table.Cell
className="
[padding-block:0.75rem] [padding-inline:1rem] text-start [vertical-align:middle]
[border-block-end-width:1px] [border-block-end-style:solid] [border-block-end-color:var(--border)]
[background:inherit] data-[align=center]:text-center data-[align=end]:text-end
group-data-[size=sm]/table:[padding-block:0.5rem] group-data-[size=sm]/table:[padding-inline:0.75rem]
group-data-[size=lg]/table:[padding-block:1rem] group-data-[size=lg]/table:[padding-inline:1.25rem]
data-[sticky]:sticky data-[sticky]:z-[1]
data-[sticky=start]:[inset-inline-start:var(--uiify-table-sticky-offset,_0)]
data-[sticky=end]:[inset-inline-end:var(--uiify-table-sticky-offset,_0)] [[data-sticky]_&]:z-[3]
"
>
{row.status}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
<Table.Footer
className="
bg-secondary [font-weight:600] data-[sticky]:sticky
data-[sticky]:[inset-block:auto_var(--uiify-table-sticky-offset,_0)] data-[sticky]:z-[2]
"
>
<Table.Row
className="
group-data-[part=body]/table-body:bg-card group-data-[part=body]/table-body:hover:bg-secondary
group-data-[part=body]/table-body:data-[selected]:bg-accent focus-visible:[outline:2px_solid_var(--ring)]
focus-visible:[outline-offset:-2px] data-[sticky]:sticky
data-[sticky]:[inset-block-start:var(--uiify-table-sticky-offset,_0)] data-[sticky]:z-[2]
"
id="team-total"
>
<Table.Cell
className="
[padding-block:0.75rem] [padding-inline:1rem] text-start [vertical-align:middle]
[border-block-end-width:1px] [border-block-end-style:solid] [border-block-end-color:var(--border)]
[background:inherit] data-[align=center]:text-center data-[align=end]:text-end
group-data-[size=sm]/table:[padding-block:0.5rem] group-data-[size=sm]/table:[padding-inline:0.75rem]
group-data-[size=lg]/table:[padding-block:1rem] group-data-[size=lg]/table:[padding-inline:1.25rem]
data-[sticky]:sticky data-[sticky]:z-[1]
data-[sticky=start]:[inset-inline-start:var(--uiify-table-sticky-offset,_0)]
data-[sticky=end]:[inset-inline-end:var(--uiify-table-sticky-offset,_0)] [[data-sticky]_&]:z-[3]
"
colSpan={3}
>
{rows.length} members · {rows.filter((row) => row.status === "Active").length} active
</Table.Cell>
</Table.Row>
</Table.Footer>
</Table.Root>
);
}API
Table.Root (native)
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
layout | "auto" | "fixed" | "auto" | פריסת טבלה מובנית. בחירה, מיון והרחבה משתמשים בנקודת הכניסה המפורשת TableClient. |
size | "sm" | "md" | "lg" | — | ריווח התאים מתוך גיליון הסגנונות שנבחר. |
variant | "solid" | "outline" | "ghost" | — | גרסת המשטח מתוך גיליון הסגנונות שנבחר. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Table.Footer
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
sticky | boolean | — | מצמיד את התחתית לסוף ציר הבלוק של מכל הגלילה. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Table.RowHeader / Table.Cell / Table.ColumnHeader
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
align | "start" | "center" | "end" | "start" | יישור הטקסט הלוגי לתא זה. |
sticky | "start" | "end" | — | מצמיד תא לקצה הלוגי בציר השורה. בתאים נצמדים סמוכים יש להגדיר --uiify-table-sticky-offset בכל תא. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
TableClient.ExpandTrigger / TableClient.ExpandedRow
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
rowId* | string | — | מפתח שורה משותף המחבר את כפתור ההרחבה ל-<tr> המובנה שלו. יש להציב <td colSpan={columnCount}> בתוך השורה המורחבת. |
TableClient.Root — @gunkin/uiify/components/table/client
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
aria-label | string | — | שם נגיש לטבלה כשאין כיתוב או כותרת גלויים המציינים את שמה. |
aria-labelledby | string | — | מזהה האלמנט המשמש כתווית הטבלה. |
selectionMode | "none" | "single" | "multiple" | "none" | קובע אם ניתן לבחור שורות וכמה בו-זמנית; "none" מציג טבלה סטטית פשוטה ללא תפקיד grid. |
selectionBehavior | "toggle" | "replace" | — | קובע אם לחיצה על שורה מוסיפה אותה לבחירה או מחליפה את הבחירה הנוכחית. |
defaultSelectedKeys | Iterable<string> | — | מזהי השורות שנבחרות בתחילה, כשהבחירה מנוהלת בתוך הרכיב ללא בקרה חיצונית. |
selectedKeys | Iterable<string> | — | מזהי השורות שנבחרות כרגע, כשהבחירה מבוקרת על ידי הקוד הקורא. |
onSelectionChange | (selectedKeys: Set<string>) => void | — | נקראת עם קבוצת מזהי השורות הנבחרות החדשה בכל שינוי בבחירה. |
defaultSortDescriptor | SortDescriptor | null | — | העמודה וכיוון המיון הראשוניים, כשמצב המיון מנוהל בתוך הרכיב ללא בקרה חיצונית. |
sortDescriptor | SortDescriptor | null | — | העמודה וכיוון המיון הנוכחיים, ההופכים את המיון למבוקר. |
onSortChange | (descriptor: SortDescriptor | null) => void | — | נקראת עם מתאר המיון החדש, או null, בהפעלת כותרת עמודה הניתנת למיון. |
expandedKeys / defaultExpandedKeys | Iterable<string> | — | מפתחות השורות המורחבות המבוקרים או הראשוניים. |
onExpandedChange | (keys: Set<string>) => void | — | מדווחת על בקשות הרחבה. שורות מבוקרות ממתינות למפתחות המעודכנים. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Table.Header
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
sticky | boolean | false | מצמיד מקטע זה למכל הגלילה; Header משתמש בתחילת ציר הבלוק ו-Footer בסופו. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Table.Body
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
children | ReactNode | — | שורות גוף הטבלה. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Table.Row
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
id* | string | — | מזהה שורה יציב המשמש כמפתח הבחירה וכמזהה האלמנט. |
disabled | boolean | false | מונע בחירה בשורה או העברת מיקוד אליה באמצעות המקלדת. |
selected | boolean | — | מסמן את השורה כנבחרת ב-HTML שנוצר בשרת, לפני שמאגר הבחירה מקבל שליטה. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Table.ColumnHeader
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
id* | string | — | מזהה העמודה שמתארי המיון מפנים אליו. |
allowsSorting | boolean | false | גורם לכותרת להחליף את מיון העמודה שלה ולחשוף aria-sort. |
sortDirection | "ascending" | "descending" | null | — | כיוון המיון המוצג לעמודה זו, המשמש ב-HTML הראשוני מהשרת כאשר מיון מותר. |
onClick | React.MouseEventHandler<HTMLTableCellElement> | — | מופעלת לפני המיון; preventDefault() מונעת את שינוי המיון. |
slotProps | TableClientColumnHeaderSlotProps | — | מאפיינים מובנים לכפתור המיון שנוצר ב-TableClient.ColumnHeader. |
children | ReactNode | — | תווית הכותרת. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Table.Cell
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
children | ReactNode | — | תוכן התא. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Table.Caption
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
children | ReactNode | — | טקסט הכיתוב המתאר את הטבלה. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Table.RowSelect
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
rowId* | string | — | מזהה השורה שתיבת סימון זו בוחרת ומבטלת את בחירתה. |
slotProps | TableSelectionSlotProps | — | מאפיינים מובנים לקלט תיבת הסימון ולתווית העוטפת שנוצרים. |
Table.SelectAll
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
slotProps | TableSelectionSlotProps | — | מאפיינים מובנים לקלט תיבת הסימון ולתווית העוטפת שנוצרים. |
מקלדת
| מקש | פעולה |
|---|---|
| Tab | מיקוד פקדי המיון, הבחירה וההרחבה ב-TableClient |
| Enter / Space | הפעלת כפתור מיון או הרחבה ממוקד; החלפת מצב תיבת סימון |
| Arrow keys | מעבר בין שורות גוף שניתנות לבחירה ב-TableClient |
נגישות
Table מרנדר כברירת מחדל אלמנטים מובנים של <table>, <caption>, <thead>, <tbody> ו-<tfoot> ללא JavaScript בצד הלקוח; השתמשו ב-ColumnHeader וב-RowHeader לסמנטיקת כותרות. נקודת הכניסה /client מוסיפה מיון, בחירה, ניווט מקלדת בין שורות והרחבה מבוקרת, הנחשפים באמצעות aria-sort, aria-expanded ו-aria-controls. ExpandedRow הוא <tr> מובנה; הניחו בתוכו Cell עם colSpan. הסינון וסדר הפריטים נשארים באחריות המשתמש בספרייה.
נקודות חיבור לעיצוב
data-uiify-tabledata-partdata-layoutdata-sizedata-variantdata-stickydata-selecteddata-disabled--uiify-table-sticky-offset