Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
612 changes: 468 additions & 144 deletions packages/demo/src/components/demo/table.tsx

Large diffs are not rendered by default.

277 changes: 233 additions & 44 deletions packages/demo/src/content/components/table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,64 +9,87 @@ import { ELEVATION } from "@eqtylab/equality";

## Overview

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.
---

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.

- **TableContainer:** Wraps the `<table>` in a scrollable container with elevation styling.
- **TableHeader / TableBody / TableFooter:** Semantic section wrappers (`<thead>`, `<tbody>`, `<tfoot>`).
- **TableRow:** A table row (`<tr>`) that accepts `onClick` for clickable rows.
- **TableHead:** A column header cell (`<th>`) with a `truncate` prop for overflow control.
- **TableCell:** A data cell (`<td>`) with a `truncate` prop for overflow control.

## Usage

Import the component:
---

Import the components:

```ts
import { Table } from "@eqtylab/equality";
```tsx
import {
TableContainer,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from "@eqtylab/equality";
```

Basic usage with required properties:
Basic usage:

```tsx
<Table
columns={[
{ key: "name", content: "Name" },
{ key: "email", content: "Email" },
]}
rows={[
{
key: "1",
cells: [
{ key: "name", content: "Alice Cooper" },
{ key: "email", content: "alice@example.com" },
],
},
]}
/>
<TableContainer>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Alice Cooper</TableCell>
<TableCell>alice@example.com</TableCell>
</TableRow>
</TableBody>
</TableContainer>
```

## Variants

---

### Default

<TableDemo client:only="react" />

### Clickable Rows

Rows accept an `onClick` handler, which applies hover and cursor styles.
Rows accept an `onClick` handler, which enables hover interactions.

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

### Sortable Columns

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

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

#### Usage

```tsx
import { SortButton, Table } from "@eqtylab/equality";

<Table
columns={[
{
key: "name",
content: (
import {
SortButton,
TableContainer,
TableHeader,
TableRow,
TableHead,
} from "@eqtylab/equality";

<TableContainer>
<TableHeader>
<TableRow>
<TableHead>
<SortButton
field="name"
sortField={sortField}
Expand All @@ -75,33 +98,174 @@ import { SortButton, Table } from "@eqtylab/equality";
>
Name
</SortButton>
),
},
]}
rows={rows}
/>;
</TableHead>
</TableRow>
</TableHeader>
</TableContainer>;
```

### With Border

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.
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.

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

### Empty State

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

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

### Empty State with Custom Component

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

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

### Sticky Header

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.

<TableDemo client:only="react" variant="sticky-header" />

#### Usage

```tsx
<TableContainer style={{ maxHeight: "400px" }}>
<TableHeader sticky>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
</TableRow>
</TableHeader>
<TableBody>{/* rows */}</TableBody>
</TableContainer>
```

## Column Sizing

---

Use the `tableLayout` prop on `TableContainer` to control how column widths are calculated. Set explicit widths on `<TableHead>` cells using the `style` prop.

### Fixed Layout

With `tableLayout="fixed"`, columns respect explicit widths exactly. This is the recommended approach when you need predictable column sizing.

<TableDemo client:only="react" variant="column-sizing" />

#### Usage

```tsx
<TableContainer tableLayout="fixed">
<TableHeader>
<TableRow>
<TableHead style={{ width: "30%" }}>Name</TableHead>
<TableHead style={{ width: "30%" }}>Email</TableHead>
<TableHead style={{ width: "100px" }}>Role</TableHead>
<TableHead style={{ width: "100px" }}>Status</TableHead>
<TableHead style={{ width: "60px" }} />
</TableRow>
</TableHeader>
</TableContainer>
```

### Min and Max Width

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.

```tsx
{
/* Column won't shrink below 150px */
}
<TableHead style={{ minWidth: "150px" }}>Description</TableHead>;

{
/* Column won't grow beyond 300px — pair with truncate */
}
<TableHead style={{ maxWidth: "300px" }} truncate>
Email
</TableHead>;
```

### Shrink to Content

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.

```tsx
<TableHead style={{ width: "1%" }}>{/* Actions */}</TableHead>
```

## Truncation

---

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.

<TableDemo client:only="react" variant="truncation" />

#### Usage

```tsx
<TableContainer tableLayout="fixed">
<TableHeader>
<TableRow>
<TableHead style={{ width: "25%" }}>Name</TableHead>
<TableHead style={{ width: "40%" }} truncate>
Email
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Alice Cooper</TableCell>
<TableCell truncate>alice.cooper.very.long.email@example.com</TableCell>
</TableRow>
</TableBody>
</TableContainer>
```

## Responsive Columns

---

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.

<TableDemo client:only="react" variant="responsive" />

#### Usage

```tsx
<div className="@container">
<TableContainer>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead className="hidden @md:table-cell">Email</TableHead>
<TableHead className="hidden @lg:table-cell">Role</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Alice Cooper</TableCell>
<TableCell className="hidden @md:table-cell">
alice@example.com
</TableCell>
<TableCell className="hidden @lg:table-cell">Admin</TableCell>
<TableCell>
<Badge variant="success">Active</Badge>
</TableCell>
</TableRow>
</TableBody>
</TableContainer>
</div>
```

## Elevations

---

### Sunken

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

## Props

| Name | Description | Type | Default | Required |
| ------------ | ------------------------------------------------------------ | ------------------------------------- | ------- | -------- |
| `columns` | Column definitions with key, content, and optional className | `TableColumn[]` | | ✅ |
| `rows` | Row data with key, cells, and optional onClick/className | `TableRowData[]` | | ✅ |
| `border` | Adds a border and rounded corners around the table | `boolean` | `false` | ❌ |
| `elevation` | Controls the shadow and border elevation level | `sunken`, `base`, `raised`, `overlay` | `base` | ❌ |
| `emptyState` | Content rendered when rows is empty, spanning all columns | `ReactNode` | | ❌ |
---

### TableContainer

| Name | Description | Type | Default | Required |
| ------------- | ------------------------------------------ | ------------------------------------- | ------- | -------- |
| `elevation` | Controls the shadow and background styling | `sunken`, `base`, `raised`, `overlay` | `base` | ❌ |
| `tableLayout` | Controls the CSS table-layout algorithm | `auto`, `fixed` | `auto` | ❌ |

### TableHeader

| Name | Description | Type | Default | Required |
| -------- | ------------------------------------------------ | --------- | ------- | -------- |
| `sticky` | Keeps the header visible while the table scrolls | `boolean` | `false` | ❌ |

### TableRow

| Name | Description | Type | Default | Required |
| ----------- | ------------------------------------------- | --------- | ------- | -------- |
| `clickable` | Applies hover and cursor interaction styles | `boolean` | `false` | ❌ |

### TableHead

| Name | Description | Type | Default | Required |
| ---------- | ------------------------------------------ | --------- | ------- | -------- |
| `truncate` | Clips overflowing content with an ellipsis | `boolean` | `false` | ❌ |

### TableCell

| Name | Description | Type | Default | Required |
| ---------- | ------------------------------------------ | --------- | ------- | -------- |
| `truncate` | Clips overflowing content with an ellipsis | `boolean` | `false` | ❌ |
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@eqtylab/equality",
"description": "EQTYLab's component and token-based design system",
"homepage": "https://equality.eqtylab.io/",
"version": "1.4.1",
"version": "1.5.0",
"license": "Apache-2.0",
"keywords": [
"component library",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/badge/badge.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@apply rounded-full;
@apply flex items-center;
@apply w-max;
@apply tabular-nums;
}

/* Size Variants */
Expand Down
22 changes: 22 additions & 0 deletions packages/ui/src/components/table/table-components.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
@apply [&_tr]:border-b;
}

.table-header--sticky {
@apply sticky top-0 z-10;
}

.table-body {
@apply [&_tr:last-child]:border-0;
}
Expand All @@ -35,6 +39,24 @@
@apply [&:has([role=checkbox])]:pr-0;
}

/* Clickable Variant */

.table-row--clickable {
--mix-color: var(--color-brand-primary);
--hover-darken: 50%;
--hover-lighten: 50%;
@apply cursor-pointer;
@apply hover:bg-mixed-light! dark:hover:bg-mixed-dark!;
@apply data-[state=selected]:bg-mixed-light dark:data-[state=selected]:bg-mixed-dark;
}

/* Truncate Variants */

.table-head--truncate,
.table-cell--truncate {
@apply max-w-0 overflow-hidden text-ellipsis whitespace-nowrap;
}

.table-caption {
@apply text-text-secondary mt-4 text-sm;
}
Expand Down
Loading
Loading