HTML Table Styling

Table Styling Overview

An unstyled HTML table is functional but honestly not great to look at - no borders between cells by default, inconsistent spacing, header cells that look almost the same as data cells - and the gap between a bare table and a table that actually looks professional is mostly just a few CSS rules. This guide goes through both the old HTML attribute approach (because you will encounter it in older code) and the CSS approach that you should use for anything you build today. By the end you'll know how to add borders, control spacing, stripe rows for readability, handle hover states, and make tables that don't fall apart on a phone screen.

Styling Approaches

  • HTML Attributes - The old approach - attributes like border, cellpadding, and bgcolor written directly on the table element. Deprecated in HTML5 but still works in browsers and still shows up in older codebases.
  • CSS - The current approach - all styling in a stylesheet, full control over every visual property, much easier to maintain and update.
  • Responsive Design - Techniques for making tables that work on small screens, where wide tables are one of the more common layout problems beginners run into.

Traditional HTML Styling Attributes

Before CSS became the standard way to style everything, table appearance was controlled with attributes written directly on the HTML elements - border for the border thickness, cellpadding for the space inside cells, cellspacing for the gap between cells, bgcolor for background color, and align for text alignment. These are all deprecated now which means they're not valid HTML5 and you shouldn't write them in new code, but browsers still support them because the web has a strong backward compatibility principle and breaking old pages is considered worse than carrying legacy features indefinitely. You'll encounter these attributes in older HTML templates, CMS themes, and email HTML - yes email, because HTML email essentially still lives in the early 2000s for compatibility reasons and bgcolor on table cells is still a legitimate email technique.

html
1<!-- Attribute-based styling - works but deprecated -->
2<table border="1" cellpadding="5" cellspacing="0" width="100%">
3  <tr>
4    <th bgcolor="#f2f2f2">Product</th>
5    <th bgcolor="#f2f2f2">Price</th>
6    <th bgcolor="#f2f2f2">In Stock</th>
7  </tr>
8  <tr>
9    <td>Widget A</td>
10    <td align="right">$12.99</td>
11    <td align="center">Yes</td>
12  </tr>
13  <tr bgcolor="#f9f9f9">
14    <td>Widget B</td>
15    <td align="right">$24.99</td>
16    <td align="center">No</td>
17  </tr>
18</table>

Common HTML Table Attributes

  • border - Sets the border width in pixels around and between cells. border="1" gives you a thin border, higher numbers make it thicker.
  • cellpadding - Sets the space between cell content and the cell's border - what CSS padding does, but as an HTML attribute applied to the whole table.
  • cellspacing - Sets the gap between cells - what CSS border-spacing does. Setting it to 0 removes the double-border gap you get by default.
  • width - Sets table width in pixels or percentage. width="100%" makes it span its container.
  • bgcolor - Sets background color on a table, tr, td, or th element. Still used in HTML email.
  • align - Sets horizontal text alignment within a cell. Use CSS text-align in new code instead.

Basic CSS Table Styling

The difference between an unstyled table and one with even just five or six CSS declarations is significant - border-collapse alone changes how the borders look completely, and adding some padding to the cells makes everything feel less cramped. The basic CSS table pattern that I use as a starting point for almost every project is: collapse the borders, add padding to th and td, give the header a background color, and add alternating row colors with nth-child. Those four things get you 80% of the way to a table that looks intentional.

html
1<style>
2  .basic-table {
3    width: 100%;
4    border-collapse: collapse;
5    font-family: Arial, sans-serif;
6    margin: 1em 0;
7  }
8  
9  .basic-table th, .basic-table td {
10    border: 1px solid #ddd;
11    padding: 8px;
12    text-align: left;
13  }
14  
15  .basic-table th {
16    background-color: #f2f2f2;
17    font-weight: bold;
18  }
19  
20  .basic-table tr:nth-child(even) {
21    background-color: #f9f9f9;
22  }
23  
24  .basic-table tr:hover {
25    background-color: #e6f7ff;
26  }
27</style>
28
29<table class="basic-table">
30  <tr>
31    <th>Product</th>
32    <th>Price</th>
33    <th>In Stock</th>
34  </tr>
35  <tr>
36    <td>Widget A</td>
37    <td>$12.99</td>
38    <td>Yes</td>
39  </tr>
40  <tr>
41    <td>Widget B</td>
42    <td>$24.99</td>
43    <td>No</td>
44  </tr>
45</table>

Key CSS Properties

  • border-collapse: collapse - The most important table CSS property - merges adjacent cell borders into one. Without it you get double borders with a gap between them which looks wrong on every table.
  • padding on th and td - Controls the space between cell content and the cell border. The default is usually too tight - 8px to 12px is a common starting point.
  • tr:nth-child(even) - Selects every other row for zebra striping. Use :nth-child(odd) for the opposite pattern. Scope it to tbody if you don't want the header row counted.
  • tr:hover - Highlights the row the user is currently over - useful for wide tables where tracking which row you're reading can be difficult.

Advanced CSS Table Styling

Once the basics are in place you can layer in more polish - rounded corners, box shadows, uppercase headers, color-coded status cells - and the difference between a basic styled table and a properly designed one is mostly just these details stacked up. The one gotcha with rounded corners on tables is that border-collapse: collapse and border-radius don't play nicely together, so you need to switch to border-collapse: separate with border-spacing: 0 and add overflow: hidden to the table element to clip the corners. I kept hitting this problem and adding border-radius and seeing nothing happen before I looked up why.

html
1<style>
2  .advanced-table {
3    width: 100%;
4    border-collapse: separate;
5    border-spacing: 0;
6    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
7    box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
8    border-radius: 6px;
9    overflow: hidden;
10  }
11  
12  .advanced-table th, .advanced-table td {
13    padding: 12px 15px;
14    text-align: left;
15    border-bottom: 1px solid #e1e1e1;
16  }
17  
18  .advanced-table th {
19    background-color: #4CAF50;
20    color: white;
21    font-weight: 600;
22    text-transform: uppercase;
23    letter-spacing: 0.5px;
24  }
25  
26  .advanced-table tr:last-child td {
27    border-bottom: none;
28  }
29  
30  .advanced-table tr:nth-child(even) {
31    background-color: #f8f8f8;
32  }
33  
34  .advanced-table tr:hover {
35    background-color: #f1f1f1;
36    transition: background-color 0.2s ease;
37  }
38  
39  .price {
40    text-align: right;
41    font-family: monospace;
42    font-weight: bold;
43    color: #2c3e50;
44  }
45  
46  .in-stock {
47    color: #27ae60;
48    font-weight: bold;
49  }
50  
51  .out-of-stock {
52    color: #e74c3c;
53    font-weight: bold;
54  }
55</style>
56
57<table class="advanced-table">
58  <thead>
59    <tr>
60      <th>Product</th>
61      <th>Description</th>
62      <th>Price</th>
63      <th>Availability</th>
64    </tr>
65  </thead>
66  <tbody>
67    <tr>
68      <td>Widget A</td>
69      <td>Basic model with standard features</td>
70      <td class="price">$12.99</td>
71      <td class="in-stock">In Stock</td>
72    </tr>
73    <tr>
74      <td>Widget B</td>
75      <td>Premium model with advanced capabilities</td>
76      <td class="price">$24.99</td>
77      <td class="out-of-stock">Backordered</td>
78    </tr>
79  </tbody>
80</table>

Advanced Techniques

  • box-shadow - Adds a subtle shadow around the table that lifts it off the page visually. A small, soft shadow looks more professional than a hard border around the whole table.
  • border-radius - Rounds the table corners. Requires border-collapse: separate and overflow: hidden on the table element to actually work - won't do anything with border-collapse: collapse.
  • text-transform: uppercase - A common pattern for column header labels - makes them feel more like UI labels and less like document headings.
  • transition - Animates the hover background change so it fades in smoothly instead of snapping. transition: background-color 0.2s ease is the standard value.

Responsive Tables for Mobile

Tables are one of the harder HTML elements to make work on small screens because they have intrinsic width requirements - a table with six columns needs a certain amount of horizontal space and there's no easy way to make that work on a phone screen the way you'd reflow text. There are two main approaches and they suit different situations. The simple one is the scrollable wrapper: wrap the table in a div with overflow-x: auto and the table keeps its full structure while users can scroll horizontally. The more complex one is the stacking approach where at small screen sizes you switch each row into a card-style block using display: block, hide the header row, and use data-label attributes combined with CSS ::before pseudo-elements to show the column names as labels next to each value - it takes more setup but looks much better on narrow screens for tables that users need to read rather than just scan.

html
1<style>
2  .table-container {
3    overflow-x: auto;
4    max-width: 100%;
5    margin: 1em 0;
6  }
7
8  .responsive-table {
9    width: 100%;
10    border-collapse: collapse;
11  }
12  
13  .responsive-table th, 
14  .responsive-table td {
15    padding: 12px 15px;
16    border: 1px solid #ddd;
17  }
18  
19  .responsive-table th {
20    background-color: #f2f2f2;
21    font-weight: bold;
22  }
23  
24  @media screen and (max-width: 600px) {
25    .responsive-table thead {
26      display: none;
27    }
28    
29    .responsive-table tbody,
30    .responsive-table tr,
31    .responsive-table td {
32      display: block;
33      width: 100%;
34    }
35    
36    .responsive-table tr {
37      margin-bottom: 1em;
38      border: 1px solid #ddd;
39    }
40    
41    .responsive-table td {
42      text-align: right;
43      padding-left: 50%;
44      position: relative;
45      border: none;
46      border-bottom: 1px solid #eee;
47    }
48    
49    .responsive-table td::before {
50      content: attr(data-label);
51      position: absolute;
52      left: 15px;
53      width: 45%;
54      text-align: left;
55      font-weight: bold;
56    }
57  }
58</style>
59
60<div class="table-container">
61  <table class="responsive-table">
62    <thead>
63      <tr>
64        <th>Product</th>
65        <th>Description</th>
66        <th>Price</th>
67        <th>Status</th>
68      </tr>
69    </thead>
70    <tbody>
71      <tr>
72        <td data-label="Product">Widget A</td>
73        <td data-label="Description">Basic model with standard features</td>
74        <td data-label="Price">$12.99</td>
75        <td data-label="Status" class="in-stock">In Stock</td>
76      </tr>
77      <tr>
78        <td data-label="Product">Widget B</td>
79        <td data-label="Description">Premium model with advanced capabilities</td>
80        <td data-label="Price">$24.99</td>
81        <td data-label="Status" class="out-of-stock">Backordered</td>
82      </tr>
83    </tbody>
84  </table>
85</div>

Responsive Techniques

  • overflow-x: auto on wrapper - The simplest responsive table fix - the table keeps its layout and users scroll horizontally. Works well for tables users need to compare across columns.
  • Media query stacking - At small screen widths, switch table elements to display: block so rows become stacked cards instead of a grid.
  • data-label attribute - Added to each td containing the column name. Used by the CSS ::before pseudo-element to show column labels when the header row is hidden on mobile.
  • thead display: none - Hides the column header row on mobile when using the stacking approach, since the column names are shown via data-label instead.

Zebra Striping and Hover Effects

Zebra striping - alternating row background colors - is one of those table styling decisions that looks like a small cosmetic choice but has a real usability effect on wider tables where tracking which row you're reading across multiple columns is genuinely hard without some visual guide. The CSS for it is one line: tr:nth-child(even) with a background color on your tbody rows. The hover highlight is a similar idea - a slightly different background when the user is over a row helps them keep their place, especially on interactive tables. A transition property on the background-color change makes the hover feel smooth rather than jarring. One thing to watch with !important on highlighted cells: it's sometimes necessary to override the zebra striping on a specific cell, but the more you rely on !important the harder your CSS becomes to maintain, so use it sparingly.

html
1<style>
2  .zebra-table {
3    width: 100%;
4    border-collapse: collapse;
5    font-family: Arial, sans-serif;
6  }
7  
8  .zebra-table th, .zebra-table td {
9    padding: 12px 15px;
10    text-align: left;
11    border-bottom: 1px solid #ddd;
12  }
13  
14  .zebra-table th {
15    background-color: #4CAF50;
16    color: white;
17  }
18  
19  .zebra-table tbody tr:nth-child(odd) {
20    background-color: #f9f9f9;
21  }
22  
23  .zebra-table tbody tr:nth-child(even) {
24    background-color: #f2f2f2;
25  }
26  
27  .zebra-table tbody tr:hover {
28    background-color: #e6f7ff;
29    transition: background-color 0.2s ease;
30  }
31  
32  .highlight {
33    background-color: #fffacd !important;
34    font-weight: bold;
35  }
36</style>
37
38<table class="zebra-table">
39  <thead>
40    <tr>
41      <th>Product</th>
42      <th>Q1 Sales</th>
43      <th>Q2 Sales</th>
44      <th>Growth</th>
45    </tr>
46  </thead>
47  <tbody>
48    <tr>
49      <td>Widget A</td>
50      <td>$5,000</td>
51      <td>$6,200</td>
52      <td class="highlight">+24%</td>
53    </tr>
54    <tr>
55      <td>Widget B</td>
56      <td>$3,500</td>
57      <td>$4,100</td>
58      <td class="highlight">+17%</td>
59    </tr>
60    <tr>
61      <td>Widget C</td>
62      <td>$2,800</td>
63      <td>$2,500</td>
64      <td>-11%</td>
65    </tr>
66  </tbody>
67</table>

Visual Enhancement Techniques

  • tbody tr:nth-child - Scoping nth-child to tbody means the header row isn't counted in the alternating pattern, so the first data row always gets the correct color.
  • tr:hover - Highlights the row under the cursor. Useful on any table where users need to read across multiple columns.
  • transition on background-color - Makes the hover color change animate smoothly. 0.2s ease is a good default - fast enough to feel responsive, slow enough to look intentional.
  • !important for highlights - Sometimes needed to override zebra striping on a specific cell. Use it sparingly - the more !important declarations you have the harder your CSS becomes to reason about.

Best Practices for Table Styling

Keep all styling in CSS rather than on HTML attributes - if you ever need to update the look of your tables across a whole site, one CSS rule change handles everything versus hunting through every table element in your HTML. Make sure there's enough contrast between your text and background colors, especially on the zebra-striped rows where a very light background can make light text hard to read. Test on a narrow screen early rather than at the end - tables that look fine on a desktop often need specific mobile handling, and discovering that late is more work than designing for it from the start. Use consistent table classes across your project so the visual language is uniform, and avoid inline styles on table elements since they create specificity problems down the line.

Key Guidelines

  • CSS over HTML attributes - All table styling belongs in CSS. HTML attributes are deprecated, harder to maintain, and can't be overridden cleanly with stylesheets.
  • Contrast and readability - Make sure text is legible against all background colors used in the table - zebra stripe colors, header backgrounds, and hover states all need adequate contrast.
  • Consistent classes - Use the same table class names across your project so all tables share the same baseline styles and look intentionally related.
  • Test on mobile early - Table responsiveness problems are much easier to design for from the start than to fix after a table is already built with many columns.
  • Avoid inline styles - Inline styles on td and th elements create CSS specificity problems - a class-based approach is much easier to override and maintain.

CSS Is the Right Tool for Table Styling

The old HTML attribute approach for styling tables is something worth knowing because you'll encounter it, but it's not something worth using for anything new - CSS gives you cleaner separation of content and presentation, easier site-wide updates, responsive flexibility, and the full power of pseudo-classes and pseudo-elements that make things like zebra striping and hover effects possible. The gap between a default HTML table and a well-styled one isn't a large amount of CSS - border-collapse, some padding, a header background, and maybe row striping gets you most of the way there, and from that foundation you can add as much polish as the design needs

Frequently Asked Questions

Are HTML table styling attributes completely obsolete?

They're deprecated in HTML5, which means they're not valid in current HTML standards, but browsers still support them for backward compatibility. You shouldn't write them in new code - use CSS instead - but you'll still find them in older templates, CMS themes, and HTML email code where the attribute approach is sometimes still the practical choice because email clients have inconsistent CSS support.

How do I add rounded corners to a table?

You need border-collapse: separate with border-spacing: 0 on the table (not border-collapse: collapse which is the usual recommendation), plus overflow: hidden to clip the corners. Then border-radius on the table element works. The reason is that border-collapse: collapse merges borders in a way that overrides border-radius, so you have to use the separate model and manually set spacing to zero to get the same visual result.

What is the best way to make tables responsive?

For tables that users primarily compare data across - financial tables, feature comparisons - the overflow-x: auto scroll wrapper is usually the right choice because it preserves the column structure. For tables users primarily read row by row, the stacking approach using media queries and data-label attributes gives a better mobile experience since each row becomes a readable card. Most projects need one or the other depending on the table type, and occasionally both.

How do I style specific rows or cells differently?

Add a class to the tr or td you want to target and write a CSS rule for that class. For pattern-based selection like every other row, use tr:nth-child(even) or tr:nth-child(odd). For the first or last row specifically, tr:first-child and tr:last-child work well. If you need to override the zebra striping on a specific cell you may need !important, but try to minimize how often you use that.

Can I use a CSS framework like Bootstrap for table styling?

Yes, and it's a reasonable choice for projects that are already using Bootstrap - the table classes are well-tested, handle basic responsiveness, and match the rest of the framework's design language. The tradeoff is that customizing Bootstrap table styles to match a specific design can take about as long as writing your own CSS, and you're loading the full framework for something that's achievable with a handful of CSS rules. For a project not already using a framework, writing your own table styles is usually faster and gives you more control.

Why doesn't my hover effect work on mobile?

Hover states don't behave the same way on touch screens as they do on devices with a mouse cursor - on mobile, a hover state might trigger on tap and then stick, or might not trigger at all depending on the browser. For tables on mobile the hover row highlight is less useful anyway since users aren't moving a cursor across rows. The common approach is to apply the hover style only on devices that actually support hover using the @media (hover: hover) media query, which leaves mobile unaffected.