Back to Blog
    Web Dev

    How to Create an HTML Table: Code, CSS and Accessibility

    Y
    Ytools Team
    October 2, 2026 7 min read
    Share →
    HTML table code beside the rendered table it produces

    HTML tables look simple: rows, cells, done. But most tables on the web are missing the two or three elements that make them readable on a phone and understandable to a screen reader, and many still use attributes deprecated years ago.

    This guide shows you how to create an HTML table properly: the elements and what each one is for, merged cells, modern CSS styling, two responsive patterns, accessible headers, and how to turn spreadsheet data into table code without retyping it.

    Quick answer: wrap the table in <table>, name it with <caption>, put header cells (<th scope="col">) in a <thead> row, put data rows (<tr> with <td> cells) in <tbody>, and style it with CSS rather than HTML attributes.

    html
    <table>
      <caption>Team plans compared</caption>
      <thead>
        <tr>
          <th scope="col">Plan</th>
          <th scope="col">Seats</th>
          <th scope="col">Price / month</th>
        </tr>
      </thead>
      <tbody>
        <tr><th scope="row">Free</th><td>1</td><td>$0</td></tr>
        <tr><th scope="row">Team</th><td>10</td><td>$12</td></tr>
      </tbody>
    </table>
    

    (Prices in examples are illustrative.)

    In this guide

    The elements of an HTML table

    Labelled diagram of an HTML table showing caption, thead with column headers, tbody with row headers, and tfoot
    Labelled diagram of an HTML table showing caption, thead with column headers, tbody with row headers, and tfoot
    ElementPurpose
    <table>Wraps the whole table
    <caption>A visible title that names the table; read first by screen readers
    <thead>Groups the header row(s)
    <tbody>Groups the data rows
    <tfoot>Groups footer rows such as totals or notes
    <tr>A row
    <th>A header cell (for a column or a row)
    <td>A data cell
    <colgroup> / <col>Lets you style whole columns, e.g. a width or background

    MDN's <table> reference lists the full set, and the normative rules are in the WHATWG HTML standard.

    How to create an HTML table step by step

    text
    Input (your data)        →   Markup                       →   Output
    Plan, Seats, Price           <table> + <caption>              A titled table
    Free, 1, $0                  <thead> with <th scope="col">    with headers
    Team, 10, $12                <tbody> rows with <th>/<td>      and data rows
    
    1. Start with the container and caption. The caption says what the table is about. If you don't want it visible, you can hide it visually with CSS, but keep it in the markup.
    2. Add the header row inside <thead>, one <th scope="col"> per column.
    3. Add data rows inside <tbody>. If the first cell of each row identifies the row (a name, a plan, a date), make it <th scope="row"> instead of <td>.
    4. Add a footer in <tfoot> if you have totals or notes.
    5. Style with CSS (next sections), never with border, cellpadding or bgcolor attributes.

    Prefer to build it visually? The HTML table generator lets you type into a grid and copy clean markup. For README files and docs, the Markdown table generator does the same in Markdown.

    Merging cells with colspan and rowspan

    colspan stretches a cell across columns; rowspan stretches it down rows.

    html
    <table>
      <caption>Quarterly sign-ups</caption>
      <thead>
        <tr>
          <th scope="col" rowspan="2">Region</th>
          <th scope="colgroup" colspan="2">2026</th>
        </tr>
        <tr>
          <th scope="col">Q1</th>
          <th scope="col">Q2</th>
        </tr>
      </thead>
      <tbody>
        <tr><th scope="row">North</th><td>120</td><td>140</td></tr>
        <tr><th scope="row">South</th><td>95</td><td>110</td></tr>
      </tbody>
    </table>
    

    Use merged cells sparingly. Every merge makes the table harder for assistive technology to describe; if a table needs several levels of merged headers, consider splitting it into two simpler tables.

    Styling an HTML table with CSS

    Attributes like border, cellpadding, cellspacing, bgcolor and width on tables are deprecated; MDN recommends CSS instead (MDN). A clean starting point:

    css
    table {
      border-collapse: collapse;   /* single borders between cells */
      width: 100%;
      font-variant-numeric: tabular-nums; /* numbers line up */
    }
    caption {
      text-align: left;
      font-weight: 600;
      padding-bottom: 0.5rem;
    }
    th, td {
      padding: 0.6rem 0.8rem;
      border-bottom: 1px solid #e5e7eb;
      text-align: left;
    }
    thead th {
      background: #f9fafb;
      position: sticky;            /* header stays visible while scrolling */
      top: 0;
    }
    tbody tr:nth-child(even) {
      background: #fafafa;         /* zebra rows */
    }
    td.num, th.num {
      text-align: right;           /* right-align numbers */
    }
    

    Tidy long stylesheets with the CSS formatter.

    Responsive HTML tables

    Wide tables are the most common mobile layout bug. There are two reliable fixes.

    Two phone mockups: a table inside a horizontal scroll container, and the same data shown as stacked cards
    Two phone mockups: a table inside a horizontal scroll container, and the same data shown as stacked cards

    Option 1: a scroll container (keeps the table a table)

    html
    <div class="table-scroll" role="region" aria-labelledby="plans-caption" tabindex="0">
      <table>
        <caption id="plans-caption">Team plans compared</caption>
        …
      </table>
    </div>
    
    css
    .table-scroll { overflow-x: auto; }
    

    tabindex="0" lets keyboard users focus the region and scroll it with arrow keys; the role and label tell screen-reader users what the scrollable area contains. This is the safest default for data people compare across columns.

    Option 2: stacked cards (for few columns, many rows)

    On narrow screens, each row becomes a card and each cell shows its column name:

    html
    <td data-label="Seats">10</td>
    
    css
    @media (max-width: 600px) {
      thead { position: absolute; left: -9999px; }   /* hide visually, keep for AT */
      tr { display: block; margin-bottom: 1rem; border: 1px solid #e5e7eb; }
      td, th { display: block; }
      td::before { content: attr(data-label) ": "; font-weight: 600; }
    }
    

    Changing the display of table elements can cause some browser and screen reader combinations to stop treating the content as a table. Test with a screen reader, or use Option 1 if the data is meant to be compared.

    Making tables accessible

    Screen-reader users navigate tables cell by cell and hear the matching row and column headers as they go, as long as the markup provides them. The W3C's tables tutorial sets out the essentials:

    • Mark headers with <th>, data with <td>. Bold text in a <td> is not a header.
    • Add scope. scope="col" for column headers, scope="row" for row headers.
    • Use <caption> to name the table.
    • For complex tables with multi-level headers, give header cells ids and reference them from data cells with headers="…", or split the table.
    • Don't use tables for page layout. Use CSS grid or flexbox; tables are for tabular data.

    Converting Excel, Google Sheets or CSV to an HTML table

    You rarely need to type table data by hand.

    1. From Excel or Google Sheets: select the cells, copy, and paste into the HTML table generator. Most grid editors accept tab-separated paste, which is what spreadsheets put on the clipboard.
    2. From a CSV file: paste the CSV into the generator, or convert CSV to JSON first if you're rendering the table from JavaScript.
    3. From code: loop over an array of objects and output <tr> elements, keeping the header row and scope attributes in your template.

    After conversion, add the caption and scope attributes if the tool didn't, and run the output through a code beautifier before committing it.

    Common mistakes

    • Missing <th> elements, so screen readers can't announce headers.
    • Using deprecated attributes (border="1", cellpadding) instead of CSS.
    • Fixed pixel widths that force horizontal page scroll on phones.
    • Tables for layout, which confuses assistive technology and breaks responsive design.
    • Over-merged headers, which make tables hard to follow for everyone.
    • Centred numbers. Right-align numeric columns so digits line up.

    Frequently asked questions

    How do I make a table in HTML?

    Use <table>, add a <caption>, then a header row of <th scope="col"> cells inside <thead>, then data rows of <td> cells inside <tbody>. Style it with CSS.

    How do I convert an Excel table to HTML?

    Copy the cells from Excel and paste them into an HTML table generator, which turns the tab-separated clipboard data into markup. Then add a caption and header scopes.

    How do I merge cells in an HTML table?

    Use colspan="n" on a cell to span n columns and rowspan="n" to span n rows.

    How do I make an HTML table responsive?

    Wrap it in a container with overflow-x: auto so it scrolls sideways on small screens, or restyle rows as stacked cards with CSS for tables with few columns.

    Should I use tables for page layout?

    No. Tables are for tabular data. Use CSS grid or flexbox for layout.

    How do I add borders to an HTML table?

    Use CSS: border-collapse: collapse; on the table and a border or border-bottom on th and td. The border HTML attribute is deprecated.

    Conclusion

    A good HTML table has a caption, real header cells with scope, data grouped in <thead> and <tbody>, CSS for every visual choice, and a plan for small screens. Get those right and the same markup works for sighted users, screen-reader users and search engines.

    Build your table visually with the HTML table generator, then copy the code →

    Related tools

    HTML table generator · Markdown table generator · CSV to JSON converter · CSS formatter

    Related articles

    Sources