Table

A table displays data in a tabular format. It is used to organize information and make it easier to read and understand.

NameEmailAccess
John Doejohn.doe@example.comOwner
Jane Smithjane.smith@example.comMember
Alice Johnsonalice.johnson@example.comAdmin
Bob Williamsbob.williams@example.comMember
Charlie Browncharlie.brown@example.comAdmin
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'cwl-ui'
 
export function Default() {
  return (
    <Table>
      <TableHead>
        <TableRow>
          <TableHeader>Name</TableHeader>
          <TableHeader>Email</TableHeader>
          <TableHeader>Access</TableHeader>
        </TableRow>
      </TableHead>
      <TableBody>
        <TableRow>
          <TableCell>John Doe</TableCell>
          <TableCell>john.doe@example.com</TableCell>
          <TableCell>Owner</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Jane Smith</TableCell>
          <TableCell>jane.smith@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Alice Johnson</TableCell>
          <TableCell>alice.johnson@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Bob Williams</TableCell>
          <TableCell>bob.williams@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Charlie Brown</TableCell>
          <TableCell>charlie.brown@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  )
}

Component API

PropDefaultDescription
bleedfalseWhether the table should bleed into the gutter.
densefalseWhether the table uses condensed spacing.
gridfalseWhether to display vertical grid lines
stripedfalseWhether to display striped rows

This is a custom Table component that does not use react-aria-components like other components in this library.

Examples

Responsive Table

Tables automatically overflow horizontally when the content is wider than the container.

NameHandleRoleEmailAccess
John Doe@johndoeCo-Founder / CEOjohn.doe@example.comAdmin
Jane Smith@janesmithCo-Founder / CTOjane.smith@example.comOwner
Alice Johnson@alicejohnsonBusiness Relationsalice.johnson@example.comMember
Bob Williams@bobwilliamsFront-end Developerbob.williams@example.comMember
Charlie Brown@charliebrownDesignercharlie.brown@example.comAdmin
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'cwl-ui'
 
export function Overflow() {
  return (
    <Table>
      <TableHead>
        <TableRow>
          <TableHeader>Name</TableHeader>
          <TableHeader>Handle</TableHeader>
          <TableHeader>Role</TableHeader>
          <TableHeader>Email</TableHeader>
          <TableHeader>Access</TableHeader>
        </TableRow>
      </TableHead>
      <TableBody>
        <TableRow>
          <TableCell>John Doe</TableCell>
          <TableCell>@johndoe</TableCell>
          <TableCell>Co-Founder / CEO</TableCell>
          <TableCell>john.doe@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Jane Smith</TableCell>
          <TableCell>@janesmith</TableCell>
          <TableCell>Co-Founder / CTO</TableCell>
          <TableCell>jane.smith@example.com</TableCell>
          <TableCell>Owner</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Alice Johnson</TableCell>
          <TableCell>@alicejohnson</TableCell>
          <TableCell>Business Relations</TableCell>
          <TableCell>alice.johnson@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Bob Williams</TableCell>
          <TableCell>@bobwilliams</TableCell>
          <TableCell>Front-end Developer</TableCell>
          <TableCell>bob.williams@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Charlie Brown</TableCell>
          <TableCell>@charliebrown</TableCell>
          <TableCell>Designer</TableCell>
          <TableCell>charlie.brown@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  )
}

Full Width Table

Use the bleed prop to make the table full width.

NameEmailAccess
John Doejohn.doe@example.comOwner
Jane Smithjane.smith@example.comMember
Alice Johnsonalice.johnson@example.comAdmin
Bob Williamsbob.williams@example.comMember
Charlie Browncharlie.brown@example.comAdmin
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'cwl-ui'
 
export function Bleed() {
  return (
    <Table bleed>
      <TableHead>
        <TableRow>
          <TableHeader>Name</TableHeader>
          <TableHeader>Email</TableHeader>
          <TableHeader>Access</TableHeader>
        </TableRow>
      </TableHead>
      <TableBody>
        <TableRow>
          <TableCell>John Doe</TableCell>
          <TableCell>john.doe@example.com</TableCell>
          <TableCell>Owner</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Jane Smith</TableCell>
          <TableCell>jane.smith@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Alice Johnson</TableCell>
          <TableCell>alice.johnson@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Bob Williams</TableCell>
          <TableCell>bob.williams@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Charlie Brown</TableCell>
          <TableCell>charlie.brown@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  )
}

Rows as links

Use the href props to make the rows clickable.

NameEmailAccess
John Doejohn.doe@example.comOwner
Jane Smithjane.smith@example.comMember
Alice Johnsonalice.johnson@example.comAdmin
Bob Williamsbob.williams@example.comMember
Charlie Browncharlie.brown@example.comAdmin
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'cwl-ui'
 
export function Links() {
  return (
    <Table>
      <TableHead>
        <TableRow>
          <TableHeader>Name</TableHeader>
          <TableHeader>Email</TableHeader>
          <TableHeader>Access</TableHeader>
        </TableRow>
      </TableHead>
      <TableBody>
        <TableRow href="#johndoe">
          <TableCell>John Doe</TableCell>
          <TableCell>john.doe@example.com</TableCell>
          <TableCell>Owner</TableCell>
        </TableRow>
        <TableRow href="#janesmith">
          <TableCell>Jane Smith</TableCell>
          <TableCell>jane.smith@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow href="#alicejohnson">
          <TableCell>Alice Johnson</TableCell>
          <TableCell>alice.johnson@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
        <TableRow href="#bobwilliams">
          <TableCell>Bob Williams</TableCell>
          <TableCell>bob.williams@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow href="#charliebrown">
          <TableCell>Charlie Brown</TableCell>
          <TableCell>charlie.brown@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  )
}

With condensed spacing

Use the dense prop to make the table use condensed spacing.

RankPlayerPosGPGAP+/-
1John DoeR80306999+18
2Jane SmithR82404787+10
3Alice JohnsonC74404585+31
4Bob WilliamsC80364480-7
5Charlie BrownL82232649+21
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'cwl-ui'
 
export function Dense() {
  return (
    <div className="w-[500px]">
      <Table dense>
        <TableHead>
          <TableRow>
            <TableHeader>Rank</TableHeader>
            <TableHeader>Player</TableHeader>
            <TableHeader>Pos</TableHeader>
            <TableHeader>GP</TableHeader>
            <TableHeader>G</TableHeader>
            <TableHeader>A</TableHeader>
            <TableHeader>P</TableHeader>
            <TableHeader>+/-</TableHeader>
          </TableRow>
        </TableHead>
        <TableBody>
          <TableRow>
            <TableCell>1</TableCell>
            <TableCell>John Doe</TableCell>
            <TableCell>R</TableCell>
            <TableCell>80</TableCell>
            <TableCell>30</TableCell>
            <TableCell>69</TableCell>
            <TableCell>99</TableCell>
            <TableCell>+18</TableCell>
          </TableRow>
          <TableRow>
            <TableCell>2</TableCell>
            <TableCell>Jane Smith</TableCell>
            <TableCell>R</TableCell>
            <TableCell>82</TableCell>
            <TableCell>40</TableCell>
            <TableCell>47</TableCell>
            <TableCell>87</TableCell>
            <TableCell>+10</TableCell>
          </TableRow>
          <TableRow>
            <TableCell>3</TableCell>
            <TableCell>Alice Johnson</TableCell>
            <TableCell>C</TableCell>
            <TableCell>74</TableCell>
            <TableCell>40</TableCell>
            <TableCell>45</TableCell>
            <TableCell>85</TableCell>
            <TableCell>+31</TableCell>
          </TableRow>
          <TableRow>
            <TableCell>4</TableCell>
            <TableCell>Bob Williams</TableCell>
            <TableCell>C</TableCell>
            <TableCell>80</TableCell>
            <TableCell>36</TableCell>
            <TableCell>44</TableCell>
            <TableCell>80</TableCell>
            <TableCell>-7</TableCell>
          </TableRow>
          <TableRow>
            <TableCell>5</TableCell>
            <TableCell>Charlie Brown</TableCell>
            <TableCell>L</TableCell>
            <TableCell>82</TableCell>
            <TableCell>23</TableCell>
            <TableCell>26</TableCell>
            <TableCell>49</TableCell>
            <TableCell>+21</TableCell>
          </TableRow>
          {/* Add more rows as needed */}
        </TableBody>
      </Table>
    </div>
  )
}

With grid lines

Use the grid prop to make the table display vertical grid lines.

NameEmailAccess
John Doejohn.doe@example.comOwner
Jane Smithjane.smith@example.comMember
Alice Johnsonalice.johnson@example.comAdmin
Bob Williamsbob.williams@example.comMember
Charlie Browncharlie.brown@example.comAdmin
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'cwl-ui'
 
export function Grid() {
  return (
    <Table grid>
      <TableHead>
        <TableRow>
          <TableHeader>Name</TableHeader>
          <TableHeader>Email</TableHeader>
          <TableHeader>Access</TableHeader>
        </TableRow>
      </TableHead>
      <TableBody>
        <TableRow>
          <TableCell>John Doe</TableCell>
          <TableCell>john.doe@example.com</TableCell>
          <TableCell>Owner</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Jane Smith</TableCell>
          <TableCell>jane.smith@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Alice Johnson</TableCell>
          <TableCell>alice.johnson@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Bob Williams</TableCell>
          <TableCell>bob.williams@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Charlie Brown</TableCell>
          <TableCell>charlie.brown@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  )
}

With striped lines

Use the striped prop to make the table display striped rows.

NameEmailAccess
John Doejohn.doe@example.comOwner
Jane Smithjane.smith@example.comMember
Alice Johnsonalice.johnson@example.comAdmin
Bob Williamsbob.williams@example.comMember
Charlie Browncharlie.brown@example.comAdmin
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'cwl-ui'
 
export function Striped() {
  return (
    <Table striped>
      <TableHead>
        <TableRow>
          <TableHeader>Name</TableHeader>
          <TableHeader>Email</TableHeader>
          <TableHeader>Access</TableHeader>
        </TableRow>
      </TableHead>
      <TableBody>
        <TableRow>
          <TableCell>John Doe</TableCell>
          <TableCell>john.doe@example.com</TableCell>
          <TableCell>Owner</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Jane Smith</TableCell>
          <TableCell>jane.smith@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Alice Johnson</TableCell>
          <TableCell>alice.johnson@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Bob Williams</TableCell>
          <TableCell>bob.williams@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Charlie Brown</TableCell>
          <TableCell>charlie.brown@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  )
}

With striped links

Use the striped and the href prop in combination with one another.

NameEmailAccess
John Doejohn.doe@example.comOwner
Jane Smithjane.smith@example.comMember
Alice Johnsonalice.johnson@example.comAdmin
Bob Williamsbob.williams@example.comMember
Charlie Browncharlie.brown@example.comAdmin
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'cwl-ui'
 
export function StripedLinks() {
  return (
    <Table striped>
      <TableHead>
        <TableRow>
          <TableHeader>Name</TableHeader>
          <TableHeader>Email</TableHeader>
          <TableHeader>Access</TableHeader>
        </TableRow>
      </TableHead>
      <TableBody>
        <TableRow href="#johndoe">
          <TableCell>John Doe</TableCell>
          <TableCell>john.doe@example.com</TableCell>
          <TableCell>Owner</TableCell>
        </TableRow>
        <TableRow href="#janesmith">
          <TableCell>Jane Smith</TableCell>
          <TableCell>jane.smith@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow href="#alicejohnson">
          <TableCell>Alice Johnson</TableCell>
          <TableCell>alice.johnson@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
        <TableRow href="#bobwilliams">
          <TableCell>Bob Williams</TableCell>
          <TableCell>bob.williams@example.com</TableCell>
          <TableCell>Member</TableCell>
        </TableRow>
        <TableRow href="#charliebrown">
          <TableCell>Charlie Brown</TableCell>
          <TableCell>charlie.brown@example.com</TableCell>
          <TableCell>Admin</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  )
}

Installation

To install, copy the code below and paste into your project.

'use client'
 
import { createContext, useContext, useState } from 'react'
 
import { Link } from 'react-aria-components'
 
import { cn } from './lib/utils'
 
interface TableProps extends React.ComponentPropsWithoutRef<'table'> {
  bleed?: boolean
  dense?: boolean
  grid?: boolean
  striped?: boolean
}
 
const TableContext = createContext<TableProps>({
  bleed: false,
  dense: false,
  grid: false,
  striped: false,
})
 
export const Table = ({ children, bleed, dense, grid, striped, ...props }: TableProps) => {
  return (
    <TableContext.Provider
      value={{
        bleed,
        dense,
        grid,
        striped,
      }}
    >
      <div className="overflow-x-auto whitespace-nowrap">
        <table className="min-w-full text-left text-sm/6" {...props}>
          {children}
        </table>
      </div>
    </TableContext.Provider>
  )
}
 
export const TableHead = ({ children, ...props }: React.ComponentPropsWithoutRef<'thead'>) => {
  return (
    <thead className="text-zinc-500" {...props}>
      {children}
    </thead>
  )
}
 
export const TableBody = ({ ...props }: React.ComponentPropsWithoutRef<'tbody'>) => {
  return <tbody {...props} />
}
 
const TableRowContext = createContext<{
  href?: string
  target?: string
  title?: string
}>({
  href: undefined,
  target: undefined,
  title: undefined,
})
 
interface TableRowProps extends React.ComponentPropsWithoutRef<'tr'> {
  href?: string
  target?: string
  title?: string
}
 
export const TableRow = ({ children, href, target, title, className, ...props }: TableRowProps) => {
  const { striped } = useContext(TableContext)
 
  return (
    <TableRowContext.Provider value={{ href, title, target }}>
      <tr
        {...props}
        className={cn(
          className,
          href &&
            'relative rounded-md has-[[data-row-link]]:cursor-pointer has-[[data-row-link][data-focused]]:outline has-[[data-row-link][data-focused]]:outline-2 has-[[data-row-link][data-focused]]:-outline-offset-2 has-[[data-row-link][data-focused]]:outline-blue-500',
          striped && 'even:bg-zinc-950/[2.5%]',
          href && striped && 'hover:bg-zinc-950/5',
          href && !striped && 'hover:bg-zinc-950/[2.5%]',
        )}
      >
        {children}
      </tr>
    </TableRowContext.Provider>
  )
}
 
export const TableHeader = ({
  className,
  children,
  ...props
}: React.ComponentPropsWithoutRef<'th'>) => {
  const { bleed, grid } = useContext(TableContext)
 
  return (
    <th
      className={cn(
        className,
        'border-b border-b-zinc-950/10 px-4 font-medium first:pl-2 last:pr-2',
        grid && 'border-l border-l-zinc-950/5 first:border-l-0 first:pl-0',
        !bleed && 'sm:first:pl-2 sm:last:pr-2',
      )}
      {...props}
    >
      {children}
    </th>
  )
}
 
export const TableCell = ({
  className,
  children,
  ...props
}: React.ComponentPropsWithoutRef<'td'>) => {
  const { bleed, dense, grid, striped } = useContext(TableContext)
  const { href, target, title } = useContext(TableRowContext)
  const [cellRef, setCellRef] = useState<HTMLElement | null>(null)
 
  return (
    <td
      ref={href ? setCellRef : undefined}
      {...props}
      className={cn(
        className,
        'px-4 tabular-nums first:pl-2 last:pr-2',
        !striped && 'border-b border-zinc-950/5',
        grid && 'border-l border-l-zinc-950/5 first:border-l-0',
        dense ? 'py-2.5' : 'py-4',
        !bleed && 'sm:first:pl-2 sm:last:pr-2',
      )}
    >
      {cellRef?.previousElementSibling === null && href && (
        // Since the href attribute is passed in at the TableRow level we need a way to make sure only the first
        // TableCell is rendered as an anchor.
        // We then use the data attributes exposed by react-aria on the TableRow level to make the entire row look
        // Focusable when in reality it is just the first row. That is why we've got focus:outline-none set below.
        <Link
          data-row-link
          href={href}
          target={target}
          aria-label={title}
          className="absolute inset-0 focus:outline-none"
        />
      )}
      {children}
    </td>
  )
}