`) that accepts `onClick` for clickable rows.
+- **TableHead:** A column header cell (`| `) with a `truncate` prop for overflow control.
+- **TableCell:** A data cell (` | `) 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
-
+
+
+
+ Name
+ Email
+
+
+
+
+ Alice Cooper
+ alice@example.com
+
+
+
```
## Variants
+---
+
### Default
### Clickable Rows
-Rows accept an `onClick` handler, which applies hover and cursor styles.
+Rows accept an `onClick` handler, which enables hover interactions.
### Sortable Columns
-Use [``](/components/sort-button) in column headers to add interactive sort controls.
+Use [``](/components/sort-button) inside `` cells to add interactive sort controls.
#### Usage
```tsx
-import { SortButton, Table } from "@eqtylab/equality";
-
-
+
+
+
Name
- ),
- },
- ]}
- rows={rows}
-/>;
+
+
+
+;
```
### With Border
-Apply a border to tables with the `border` prop. This should be added most places `` 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.
### 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 `` that spans all columns with `colSpan`.
### 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`.
+### Sticky Header
+
+Use the `sticky` prop on `` to keep column headers visible while scrolling. The height of the `` must be constrained for this to work as expected.
+
+
+
+#### Usage
+
+```tsx
+
+
+
+ Name
+ Email
+
+
+ {/* rows */}
+
+```
+
+## Column Sizing
+
+---
+
+Use the `tableLayout` prop on `TableContainer` to control how column widths are calculated. Set explicit widths on `` 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.
+
+
+
+#### Usage
+
+```tsx
+
+
+
+ Name
+ Email
+ Role
+ Status
+
+
+
+
+```
+
+### Min and Max Width
+
+Use `style={{ minWidth }}` or `style={{ maxWidth }}` on `` 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 */
+}
+Description;
+
+{
+ /* Column won't grow beyond 300px — pair with truncate */
+}
+
+ Email
+;
+```
+
+### 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
+{/* Actions */}
+```
+
+## Truncation
+
+---
+
+Use the `truncate` prop on `` and `` 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.
+
+
+
+#### Usage
+
+```tsx
+
+
+
+ Name
+
+ Email
+
+
+
+
+
+ Alice Cooper
+ alice.cooper.very.long.email@example.com
+
+
+
+```
+
+## 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 `` and `` for columns that should collapse.
+
+
+
+#### Usage
+
+```tsx
+
+
+
+
+ Name
+ Email
+ Role
+ Status
+
+
+
+
+ Alice Cooper
+
+ alice@example.com
+
+ Admin
+
+ Active
+
+
+
+
+
+```
+
## Elevations
+---
+
### Sunken
@@ -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` | ❌ |
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 964112b..daf91ff 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -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",
diff --git a/packages/ui/src/components/badge/badge.module.css b/packages/ui/src/components/badge/badge.module.css
index 6c722b2..92dc3ea 100644
--- a/packages/ui/src/components/badge/badge.module.css
+++ b/packages/ui/src/components/badge/badge.module.css
@@ -5,6 +5,7 @@
@apply rounded-full;
@apply flex items-center;
@apply w-max;
+ @apply tabular-nums;
}
/* Size Variants */
diff --git a/packages/ui/src/components/table/table-components.module.css b/packages/ui/src/components/table/table-components.module.css
index ec303d5..f052c32 100644
--- a/packages/ui/src/components/table/table-components.module.css
+++ b/packages/ui/src/components/table/table-components.module.css
@@ -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;
}
@@ -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;
}
diff --git a/packages/ui/src/components/table/table-components.tsx b/packages/ui/src/components/table/table-components.tsx
index def0c2c..3c5656f 100644
--- a/packages/ui/src/components/table/table-components.tsx
+++ b/packages/ui/src/components/table/table-components.tsx
@@ -9,19 +9,32 @@ const tableElevationVariants = generateElevationVariants(styles, 'table', ELEVAT
const TableContainer = React.forwardRef<
HTMLTableElement,
- React.HTMLAttributes & VariantProps
->(({ className, elevation = ELEVATION.RAISED, ...props }, ref) => (
-
-
+ React.HTMLAttributes &
+ VariantProps & { tableLayout?: 'auto' | 'fixed' }
+>(({ className, style, elevation = ELEVATION.RAISED, tableLayout, ...props }, ref) => (
+
));
TableContainer.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
+ React.HTMLAttributes & { sticky?: boolean }
+>(({ className, sticky, ...props }, ref) => (
+
));
TableHeader.displayName = 'TableHeader';
@@ -41,26 +54,39 @@ const TableFooter = React.forwardRef<
));
TableFooter.displayName = 'TableFooter';
-const TableRow = React.forwardRef>(
- ({ className, ...props }, ref) => (
-
- )
-);
+const TableRow = React.forwardRef<
+ HTMLTableRowElement,
+ React.HTMLAttributes & { clickable?: boolean }
+>(({ className, clickable, ...props }, ref) => (
+
+));
TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef<
HTMLTableCellElement,
- React.ThHTMLAttributes
->(({ className, ...props }, ref) => (
- |
+ React.ThHTMLAttributes & { truncate?: boolean }
+>(({ className, truncate, ...props }, ref) => (
+ |
));
TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef<
HTMLTableCellElement,
- React.TdHTMLAttributes
->(({ className, ...props }, ref) => (
- |
+ React.TdHTMLAttributes & { truncate?: boolean }
+>(({ className, truncate, ...props }, ref) => (
+ |
));
TableCell.displayName = 'TableCell';
diff --git a/packages/ui/src/components/table/table.module.css b/packages/ui/src/components/table/table.module.css
index a8f9155..aa05c79 100644
--- a/packages/ui/src/components/table/table.module.css
+++ b/packages/ui/src/components/table/table.module.css
@@ -4,15 +4,6 @@
@apply shadow-sm;
}
-.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;
-}
-
.table-border {
@apply border;
@apply overflow-hidden rounded-md;
diff --git a/packages/ui/src/components/table/table.tsx b/packages/ui/src/components/table/table.tsx
index 58fd486..63e224c 100644
--- a/packages/ui/src/components/table/table.tsx
+++ b/packages/ui/src/components/table/table.tsx
@@ -39,6 +39,7 @@ interface TableProps extends VariantProps {
className?: string;
border?: boolean;
emptyState?: React.ReactNode;
+ tableLayout?: 'auto' | 'fixed';
}
const tableElevationVariants = generateElevationVariants(styles, 'table', ELEVATION.BASE);
@@ -50,6 +51,7 @@ const Table = ({
border = false,
elevation = ELEVATION.BASE,
emptyState,
+ tableLayout,
}: TableProps) => {
const isEmpty = rows.length === 0;
@@ -62,7 +64,7 @@ const Table = ({
className
)}
>
-
+
{columns.map((column) => (
@@ -85,7 +87,8 @@ const Table = ({
{rows.map((row) => (
{row.cells.map((cell) => (
|