Skip to content
Open
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
63 changes: 60 additions & 3 deletions packages/demo/src/components/demo/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
TableHeader,
TableRow,
} from "@eqtylab/equality";
import { useState } from "react";
import { type CSSProperties, useState } from "react";

interface TableDemoProps {
variant?:
Expand All @@ -24,12 +24,22 @@ interface TableDemoProps {
| "column-sizing"
| "truncation"
| "responsive"
| "sticky-header";
| "sticky-header"
| "sticky-header-page"
| "sticky-header-page-offset";
elevation?: Elevation;
}

const defaultCols = "1fr 1fr auto auto auto";

const roles = ["Admin", "User", "Viewer"] as const;
const longTableData = Array.from({ length: 10 }, (_, i) => ({
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length],
active: i % 4 !== 0,
}));

export const TableDemo = ({
variant = "default",
elevation,
Expand Down Expand Up @@ -216,7 +226,7 @@ export const TableDemo = ({
columns={defaultCols}
border
>
<TableHeader sticky>
<TableHeader sticky="container">
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
Expand Down Expand Up @@ -295,6 +305,53 @@ export const TableDemo = ({
);
}

if (
variant === "sticky-header-page" ||
variant === "sticky-header-page-offset"
) {
const offset = variant === "sticky-header-page-offset";
return (
<TableContainer
elevation={elevation}
columns={defaultCols}
border
style={
offset
? ({ "--table-sticky-top": "57px" } as CSSProperties)
: undefined
}
>
<TableHeader sticky="page">
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{/* sticky="page" keeps the header pinned to the viewport as the page scrolls */}
{longTableData.map((user) => (
<TableRow key={user.name}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
<TableCell>
<Badge variant={user.active ? "success" : "neutral"}>
{user.active ? "Active" : "Inactive"}
</Badge>
</TableCell>
<TableCell>
<IconButton name="EllipsisVertical" label="Row actions" />
</TableCell>
</TableRow>
))}
</TableBody>
</TableContainer>
);
}

if (variant === "with-sorter") {
return (
<TableContainer elevation={elevation} columns={defaultCols}>
Expand Down
35 changes: 29 additions & 6 deletions packages/demo/src/content/components/table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -149,22 +149,45 @@ The empty state cell accepts any `ReactNode`, so you can use a custom component

### 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.
Use the `sticky` prop on `<TableHeader>` to keep column headers visible while scrolling. It accepts two modes:

- **`container`** — the header sticks within the table's own scroll area. The `<TableContainer>` must have a constrained height (e.g. `max-h-*`), which makes it scroll internally.
- **`page`** — the header sticks against the document as the whole page scrolls. No height constraint is needed; use this for full-page data tables. Pin offset can be adjusted with the `--table-sticky-top` CSS variable to clear fixed app chrome.

#### On container

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

#### On page

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

#### Usage

```tsx
{
/* Sticks within a height-constrained, internally scrolling table */
}
<TableContainer columns="1fr 1fr auto" className="max-h-[400px]">
<TableHeader sticky>
<TableHeader sticky="container">
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
</TableRow>
</TableHeader>
<TableBody>{/* rows */}</TableBody>
</TableContainer>
</TableContainer>;

{
/* Sticks against the document as the page scrolls */
}
<TableContainer
columns="1fr 1fr auto"
style={{ "--table-sticky-top": "100px" }}
>
<TableHeader sticky="page">{/* … */}</TableHeader>
<TableBody>{/* rows */}</TableBody>
</TableContainer>;
```

## Column Sizing
Expand Down Expand Up @@ -309,9 +332,9 @@ Use [container queries](https://tailwindcss.com/docs/responsive-design#what-are-

### TableHeader

| Name | Description | Type | Default | Required |
| -------- | ------------------------------------------------ | --------- | ------- | -------- |
| `sticky` | Keeps the header visible while the table scrolls | `boolean` | `false` | ❌ |
| Name | Description | Type | Default | Required |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------- | -------- |
| `sticky` | Pins the header while scrolling. `container` sticks within a height-constrained table; `page` sticks against the document as the page scrolls. | `false \| 'container' \| 'page'` | `false` | ❌ |

### TableRow

Expand Down
20 changes: 15 additions & 5 deletions packages/ui/src/components/table/table-components.module.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
@reference '../../theme/theme.module.css';

.table {
@apply w-full overflow-auto;
@apply w-full;
}

.table:has(.table-header--sticky-container) {
@apply overflow-auto;
}

.table-inner {
Expand All @@ -17,10 +21,17 @@
grid-column: 1 / -1;
}

.table-header--sticky {
/* Sticks to the top of the scrollable container (requires a constrained height). */
.table-header--sticky-container {
@apply sticky top-0 z-10;
}

/* Sticks to the document/viewport as the page scrolls. */
.table-header--sticky-page {
@apply sticky z-10;
top: var(--table-sticky-top, 0);
}

.table-body {
@apply [&_tr:last-child]:border-0;
@apply grid;
Expand Down Expand Up @@ -88,7 +99,7 @@
}

.table-row--linked:has(.table-row-link:focus-visible) {
@apply outline-brand-primary outline outline-2 outline-offset-[-2px];
@apply outline-brand-primary outline-2 outline-offset-[-2px];
}

.table-body .table-row--clickable[data-state='selected'] {
Expand All @@ -105,8 +116,7 @@
/* BORDER */

.table-border {
@apply border;
@apply overflow-hidden rounded-md;
@apply overflow-clip rounded-md border;
}

/* ELEVATION */
Expand Down
13 changes: 10 additions & 3 deletions packages/ui/src/components/table/table-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,18 @@ TableContainer.displayName = 'Table';

const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement> & { sticky?: boolean }
>(({ className, sticky, ...props }, ref) => (
React.HTMLAttributes<HTMLTableSectionElement> & {
sticky?: false | 'container' | 'page';
}
>(({ className, sticky = false, ...props }, ref) => (
<thead
ref={ref}
className={cn(styles['table-header'], sticky && styles['table-header--sticky'], className)}
className={cn(
styles['table-header'],
sticky === 'container' && styles['table-header--sticky-container'],
sticky === 'page' && styles['table-header--sticky-page'],
className
)}
{...props}
/>
));
Expand Down
Loading