Skip to content

Commit 1382ffd

Browse files
Shrinks99claude
andauthored
Improve table props and docs (#79)
Co-authored-by: Claude <claude@users.noreply.github.com>
1 parent 338217f commit 1382ffd

8 files changed

Lines changed: 774 additions & 218 deletions

File tree

packages/demo/src/components/demo/table.tsx

Lines changed: 468 additions & 144 deletions
Large diffs are not rendered by default.

packages/demo/src/content/components/table.mdx

Lines changed: 233 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,64 +9,87 @@ import { ELEVATION } from "@eqtylab/equality";
99

1010
## Overview
1111

12-
The Table component displays structured data in rows and columns. It supports clickable rows, sortable column headers, bordered styling, elevation levels, and empty state messaging. Column and cell `content` accepts any `ReactNode`, allowing badges, buttons, and other components inline.
12+
---
13+
14+
Tables are built from compositional primitives that map directly to [HTML table elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/table). Each primitive is a styled wrapper that accepts all native HTML attributes, giving you full control over layout, sizing, and responsiveness.
15+
16+
- **TableContainer:** Wraps the `<table>` in a scrollable container with elevation styling.
17+
- **TableHeader / TableBody / TableFooter:** Semantic section wrappers (`<thead>`, `<tbody>`, `<tfoot>`).
18+
- **TableRow:** A table row (`<tr>`) that accepts `onClick` for clickable rows.
19+
- **TableHead:** A column header cell (`<th>`) with a `truncate` prop for overflow control.
20+
- **TableCell:** A data cell (`<td>`) with a `truncate` prop for overflow control.
1321

1422
## Usage
1523

16-
Import the component:
24+
---
25+
26+
Import the components:
1727

18-
```ts
19-
import { Table } from "@eqtylab/equality";
28+
```tsx
29+
import {
30+
TableContainer,
31+
TableHeader,
32+
TableBody,
33+
TableRow,
34+
TableHead,
35+
TableCell,
36+
} from "@eqtylab/equality";
2037
```
2138

22-
Basic usage with required properties:
39+
Basic usage:
2340

2441
```tsx
25-
<Table
26-
columns={[
27-
{ key: "name", content: "Name" },
28-
{ key: "email", content: "Email" },
29-
]}
30-
rows={[
31-
{
32-
key: "1",
33-
cells: [
34-
{ key: "name", content: "Alice Cooper" },
35-
{ key: "email", content: "alice@example.com" },
36-
],
37-
},
38-
]}
39-
/>
42+
<TableContainer>
43+
<TableHeader>
44+
<TableRow>
45+
<TableHead>Name</TableHead>
46+
<TableHead>Email</TableHead>
47+
</TableRow>
48+
</TableHeader>
49+
<TableBody>
50+
<TableRow>
51+
<TableCell>Alice Cooper</TableCell>
52+
<TableCell>alice@example.com</TableCell>
53+
</TableRow>
54+
</TableBody>
55+
</TableContainer>
4056
```
4157

4258
## Variants
4359

60+
---
61+
4462
### Default
4563

4664
<TableDemo client:only="react" />
4765

4866
### Clickable Rows
4967

50-
Rows accept an `onClick` handler, which applies hover and cursor styles.
68+
Rows accept an `onClick` handler, which enables hover interactions.
5169

5270
<TableDemo client:only="react" variant="clickable" />
5371

5472
### Sortable Columns
5573

56-
Use [`<SortButton>`](/components/sort-button) in column headers to add interactive sort controls.
74+
Use [`<SortButton>`](/components/sort-button) inside `<TableHead>` cells to add interactive sort controls.
5775

5876
<TableDemo client:only="react" variant="with-sorter" />
5977

6078
#### Usage
6179

6280
```tsx
63-
import { SortButton, Table } from "@eqtylab/equality";
64-
65-
<Table
66-
columns={[
67-
{
68-
key: "name",
69-
content: (
81+
import {
82+
SortButton,
83+
TableContainer,
84+
TableHeader,
85+
TableRow,
86+
TableHead,
87+
} from "@eqtylab/equality";
88+
89+
<TableContainer>
90+
<TableHeader>
91+
<TableRow>
92+
<TableHead>
7093
<SortButton
7194
field="name"
7295
sortField={sortField}
@@ -75,33 +98,174 @@ import { SortButton, Table } from "@eqtylab/equality";
7598
>
7699
Name
77100
</SortButton>
78-
),
79-
},
80-
]}
81-
rows={rows}
82-
/>;
101+
</TableHead>
102+
</TableRow>
103+
</TableHeader>
104+
</TableContainer>;
83105
```
84106

85107
### With Border
86108

87-
Apply a border to tables with the `border` prop. This should be added most places `<Table>` is used, except for when it lives within a different container which already has a border applied.
109+
Apply a border to tables by adding `overflow-hidden rounded-md border` to the `TableContainer` className. This should be added most places the table is used, except when it lives within a container that already has a border.
88110

89111
<TableDemo client:only="react" variant="with-border" />
90112

91113
### Empty State
92114

93-
When `rows` is empty and `emptyState` is provided, the table keeps column headers visible and renders the empty state content spanning all columns.
115+
When there are no rows, render empty state content in a `<TableCell>` that spans all columns with `colSpan`.
94116

95117
<TableDemo client:only="react" variant="empty-state" />
96118

97119
### Empty State with Custom Component
98120

99-
The `emptyState` prop accepts any `ReactNode`, so you can pass a custom component like `EmptyTableState`.
121+
The empty state cell accepts any `ReactNode`, so you can use a custom component like `EmptyTableState`.
100122

101123
<TableDemo client:only="react" variant="empty-state-custom" />
102124

125+
### Sticky Header
126+
127+
Use the `sticky` prop on `<TableHeader>` to keep column headers visible while scrolling. The height of the `<TableContainer>` must be constrained for this to work as expected.
128+
129+
<TableDemo client:only="react" variant="sticky-header" />
130+
131+
#### Usage
132+
133+
```tsx
134+
<TableContainer style={{ maxHeight: "400px" }}>
135+
<TableHeader sticky>
136+
<TableRow>
137+
<TableHead>Name</TableHead>
138+
<TableHead>Email</TableHead>
139+
</TableRow>
140+
</TableHeader>
141+
<TableBody>{/* rows */}</TableBody>
142+
</TableContainer>
143+
```
144+
145+
## Column Sizing
146+
147+
---
148+
149+
Use the `tableLayout` prop on `TableContainer` to control how column widths are calculated. Set explicit widths on `<TableHead>` cells using the `style` prop.
150+
151+
### Fixed Layout
152+
153+
With `tableLayout="fixed"`, columns respect explicit widths exactly. This is the recommended approach when you need predictable column sizing.
154+
155+
<TableDemo client:only="react" variant="column-sizing" />
156+
157+
#### Usage
158+
159+
```tsx
160+
<TableContainer tableLayout="fixed">
161+
<TableHeader>
162+
<TableRow>
163+
<TableHead style={{ width: "30%" }}>Name</TableHead>
164+
<TableHead style={{ width: "30%" }}>Email</TableHead>
165+
<TableHead style={{ width: "100px" }}>Role</TableHead>
166+
<TableHead style={{ width: "100px" }}>Status</TableHead>
167+
<TableHead style={{ width: "60px" }} />
168+
</TableRow>
169+
</TableHeader>
170+
</TableContainer>
171+
```
172+
173+
### Min and Max Width
174+
175+
Use `style={{ minWidth }}` or `style={{ maxWidth }}` on `<TableHead>` to constrain column sizes. `minWidth` works in both `auto` and `fixed` layouts. `maxWidth` works best with the default `auto` layout.
176+
177+
```tsx
178+
{
179+
/* Column won't shrink below 150px */
180+
}
181+
<TableHead style={{ minWidth: "150px" }}>Description</TableHead>;
182+
183+
{
184+
/* Column won't grow beyond 300px — pair with truncate */
185+
}
186+
<TableHead style={{ maxWidth: "300px" }} truncate>
187+
Email
188+
</TableHead>;
189+
```
190+
191+
### Shrink to Content
192+
193+
In the default `auto` layout, use `style={{ width: "1%" }}` to minimize a column to fit its content. The browser's table algorithm ensures the column still renders at least as wide as its content, while giving all remaining space to other columns. This is useful for action columns or icon-only columns.
194+
195+
```tsx
196+
<TableHead style={{ width: "1%" }}>{/* Actions */}</TableHead>
197+
```
198+
199+
## Truncation
200+
201+
---
202+
203+
Use the `truncate` prop on `<TableHead>` and `<TableCell>` to clip overflowing text with an ellipsis. This works best with `tableLayout="fixed"` and an explicit column width so the cell has a defined boundary to truncate against.
204+
205+
<TableDemo client:only="react" variant="truncation" />
206+
207+
#### Usage
208+
209+
```tsx
210+
<TableContainer tableLayout="fixed">
211+
<TableHeader>
212+
<TableRow>
213+
<TableHead style={{ width: "25%" }}>Name</TableHead>
214+
<TableHead style={{ width: "40%" }} truncate>
215+
Email
216+
</TableHead>
217+
</TableRow>
218+
</TableHeader>
219+
<TableBody>
220+
<TableRow>
221+
<TableCell>Alice Cooper</TableCell>
222+
<TableCell truncate>alice.cooper.very.long.email@example.com</TableCell>
223+
</TableRow>
224+
</TableBody>
225+
</TableContainer>
226+
```
227+
228+
## Responsive Columns
229+
230+
---
231+
232+
Use [container queries](https://tailwindcss.com/docs/responsive-design#what-are-container-queries) to show or hide columns based on the table's container width. Wrap the table in a `@container` element and apply `hidden @md:table-cell` (or similar) to both the `<TableHead>` and `<TableCell>` for columns that should collapse.
233+
234+
<TableDemo client:only="react" variant="responsive" />
235+
236+
#### Usage
237+
238+
```tsx
239+
<div className="@container">
240+
<TableContainer>
241+
<TableHeader>
242+
<TableRow>
243+
<TableHead>Name</TableHead>
244+
<TableHead className="hidden @md:table-cell">Email</TableHead>
245+
<TableHead className="hidden @lg:table-cell">Role</TableHead>
246+
<TableHead>Status</TableHead>
247+
</TableRow>
248+
</TableHeader>
249+
<TableBody>
250+
<TableRow>
251+
<TableCell>Alice Cooper</TableCell>
252+
<TableCell className="hidden @md:table-cell">
253+
alice@example.com
254+
</TableCell>
255+
<TableCell className="hidden @lg:table-cell">Admin</TableCell>
256+
<TableCell>
257+
<Badge variant="success">Active</Badge>
258+
</TableCell>
259+
</TableRow>
260+
</TableBody>
261+
</TableContainer>
262+
</div>
263+
```
264+
103265
## Elevations
104266

267+
---
268+
105269
### Sunken
106270

107271
<TableDemo client:only="react" elevation={ELEVATION.SUNKEN} />
@@ -120,10 +284,35 @@ The `emptyState` prop accepts any `ReactNode`, so you can pass a custom componen
120284

121285
## Props
122286

123-
| Name | Description | Type | Default | Required |
124-
| ------------ | ------------------------------------------------------------ | ------------------------------------- | ------- | -------- |
125-
| `columns` | Column definitions with key, content, and optional className | `TableColumn[]` | ||
126-
| `rows` | Row data with key, cells, and optional onClick/className | `TableRowData[]` | ||
127-
| `border` | Adds a border and rounded corners around the table | `boolean` | `false` ||
128-
| `elevation` | Controls the shadow and border elevation level | `sunken`, `base`, `raised`, `overlay` | `base` ||
129-
| `emptyState` | Content rendered when rows is empty, spanning all columns | `ReactNode` | ||
287+
---
288+
289+
### TableContainer
290+
291+
| Name | Description | Type | Default | Required |
292+
| ------------- | ------------------------------------------ | ------------------------------------- | ------- | -------- |
293+
| `elevation` | Controls the shadow and background styling | `sunken`, `base`, `raised`, `overlay` | `base` ||
294+
| `tableLayout` | Controls the CSS table-layout algorithm | `auto`, `fixed` | `auto` ||
295+
296+
### TableHeader
297+
298+
| Name | Description | Type | Default | Required |
299+
| -------- | ------------------------------------------------ | --------- | ------- | -------- |
300+
| `sticky` | Keeps the header visible while the table scrolls | `boolean` | `false` ||
301+
302+
### TableRow
303+
304+
| Name | Description | Type | Default | Required |
305+
| ----------- | ------------------------------------------- | --------- | ------- | -------- |
306+
| `clickable` | Applies hover and cursor interaction styles | `boolean` | `false` ||
307+
308+
### TableHead
309+
310+
| Name | Description | Type | Default | Required |
311+
| ---------- | ------------------------------------------ | --------- | ------- | -------- |
312+
| `truncate` | Clips overflowing content with an ellipsis | `boolean` | `false` ||
313+
314+
### TableCell
315+
316+
| Name | Description | Type | Default | Required |
317+
| ---------- | ------------------------------------------ | --------- | ------- | -------- |
318+
| `truncate` | Clips overflowing content with an ellipsis | `boolean` | `false` ||

packages/ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@eqtylab/equality",
33
"description": "EQTYLab's component and token-based design system",
44
"homepage": "https://equality.eqtylab.io/",
5-
"version": "1.4.1",
5+
"version": "1.5.0",
66
"license": "Apache-2.0",
77
"keywords": [
88
"component library",

packages/ui/src/components/badge/badge.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
@apply rounded-full;
66
@apply flex items-center;
77
@apply w-max;
8+
@apply tabular-nums;
89
}
910

1011
/* Size Variants */

packages/ui/src/components/table/table-components.module.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
@apply [&_tr]:border-b;
1414
}
1515

16+
.table-header--sticky {
17+
@apply sticky top-0 z-10;
18+
}
19+
1620
.table-body {
1721
@apply [&_tr:last-child]:border-0;
1822
}
@@ -35,6 +39,24 @@
3539
@apply [&:has([role=checkbox])]:pr-0;
3640
}
3741

42+
/* Clickable Variant */
43+
44+
.table-row--clickable {
45+
--mix-color: var(--color-brand-primary);
46+
--hover-darken: 50%;
47+
--hover-lighten: 50%;
48+
@apply cursor-pointer;
49+
@apply hover:bg-mixed-light! dark:hover:bg-mixed-dark!;
50+
@apply data-[state=selected]:bg-mixed-light dark:data-[state=selected]:bg-mixed-dark;
51+
}
52+
53+
/* Truncate Variants */
54+
55+
.table-head--truncate,
56+
.table-cell--truncate {
57+
@apply max-w-0 overflow-hidden text-ellipsis whitespace-nowrap;
58+
}
59+
3860
.table-caption {
3961
@apply text-text-secondary mt-4 text-sm;
4062
}

0 commit comments

Comments
 (0)