Styling Tables

Difficulty: Intermediate

By default, HTML tables have no visible borders and minimal styling. Browsers add small padding inside cells and separate borders for each cell, resulting in double borders where cells meet. CSS provides powerful properties to transform plain tables into clean, readable data displays. Understanding these properties is essential for any frontend developer who works with tabular data.

The border-collapse property is the most important table-specific CSS property. It has two values: separate (the default) and collapse. When set to collapse, adjacent cell borders are merged into a single border, eliminating the double-border effect. When set to separate (the default), each cell maintains its own border, creating visible gaps between cells. In the vast majority of production designs, you want border-collapse: collapse.

The border-spacing property only applies when border-collapse is set to separate. It controls the gap between cell borders, functioning similarly to the gap property in Flexbox and Grid. You can provide one value for uniform spacing or two values for horizontal and vertical spacing independently: border-spacing: 10px 5px sets 10px horizontal and 5px vertical spacing. This property is ignored when border-collapse is collapse.

Striped rows (zebra striping) significantly improve readability for tables with many rows. The standard approach uses the nth-child pseudo-selector: tr:nth-child(even) or tr:nth-child(odd) to apply a subtle background color to alternating rows. You should apply zebra striping to <tbody> rows specifically (tbody tr:nth-child(even)) to avoid accidentally styling header or footer rows. Adding a hover highlight (tbody tr:hover) further improves usability by helping users track which row they are looking at.

Beyond these fundamentals, modern table styling typically involves: setting consistent padding on th and td cells, left-aligning text by default (headers are sometimes centered), adding a bottom border to each row for horizontal separation, applying a distinct background to <thead> to visually separate it from data, and ensuring the table is responsive. For responsive tables, common strategies include wrapping the table in a container with overflow-x: auto to enable horizontal scrolling on small screens, or restructuring the table into a stacked card layout at narrow widths.

Code examples

border-collapse vs separate

/* Default: borders are separate with gaps */
.table-separate {
  border-collapse: separate;
  border-spacing: 4px;
}
.table-separate th,
.table-separate td {
  border: 1px solid #ccc;
  padding: 8px;
}

/* Collapsed: borders merge into single lines */
.table-collapsed {
  border-collapse: collapse;
}
.table-collapsed th,
.table-collapsed td {
  border: 1px solid #ccc;
  padding: 8px;
}

With border-collapse: separate, each cell has its own border and gaps appear between them. With border-collapse: collapse, adjacent borders merge into one, creating a clean grid. Most production tables use collapse.

Striped Rows with nth-child

.styled-table {
  border-collapse: collapse;
  width: 100%;
}

.styled-table th,
.styled-table td {
  padding: 12px 16px;
  text-align: left;
  border-bottom: 1px solid #e5e7eb;
}

.styled-table thead th {
  background-color: #f9fafb;
  font-weight: 600;
  color: #374151;
  border-bottom: 2px solid #d1d5db;
}

/* Zebra striping on tbody rows only */
.styled-table tbody tr:nth-child(even) {
  background-color: #f9fafb;
}

/* Hover highlight */
.styled-table tbody tr:hover {
  background-color: #eff6ff;
}

This is a production-ready table style. The thead has a distinct background and heavier bottom border. Body rows alternate between white and a light gray, and hovering a row highlights it in light blue. Borders are only on the bottom of each row for a clean, modern look.

Responsive Table with Horizontal Scroll

/* Wrapper enables horizontal scroll on small screens */
.table-container {
  width: 100%;
  overflow-x: auto;
}

.responsive-table {
  border-collapse: collapse;
  width: 100%;
  min-width: 600px;
}

.responsive-table th,
.responsive-table td {
  padding: 10px 14px;
  white-space: nowrap;
  border: 1px solid #e5e7eb;
}

Wrapping the table in a div with overflow-x: auto enables horizontal scrolling when the table is wider than the viewport. Setting min-width on the table prevents columns from collapsing too small. The white-space: nowrap on cells prevents text from wrapping to keep column widths predictable.

Sticky Header Table

.sticky-table-container {
  max-height: 400px;
  overflow-y: auto;
}

.sticky-table {
  border-collapse: collapse;
  width: 100%;
}

.sticky-table thead th {
  position: sticky;
  top: 0;
  background-color: #1f2937;
  color: white;
  padding: 12px 16px;
  z-index: 1;
}

.sticky-table td {
  padding: 10px 16px;
  border-bottom: 1px solid #e5e7eb;
}

By placing the table in a scrollable container and applying position: sticky with top: 0 to the thead cells, the header row stays visible as users scroll through long tables. The z-index ensures the header renders above the scrolling body content. Note that sticky headers require border-collapse: collapse and an opaque background on the header to work correctly.

Key points

Concepts covered

border-collapse, border-spacing, Striped Rows, Table Styling, CSS Tables