HTML Tables & Data Display Responsive and Mobile-Friendly Tables in HTML

You spent time building a proper HTML table. Clean structure, a <caption> on top, correct <thead>, <tbody>, and <tfoot> sections. It looks really good on your desktop.

Then you open it on your phone.

The table explodes sideways. Half the columns are off-screen. Your user has to awkwardly side-scroll just to read the last cell. Or worse, the whole page layout breaks because the table refuses to stay in its lane.

This has happened to basically every developer who has worked with HTML tables. And it will keep happening unless you know the right techniques to handle it. That is exactly what this article covers.

We have spent four solid articles in this batch learning how to build proper tables, from the core table structure and structural sections to colspan and rowspan and meaningful captions. Now it is time to make all of that work on every screen size.

This is the last article in Batch 6, and it is a big one. Let us get into it.


01Why Tables Struggle on Small Screens

Here is the honest reason: tables are not flexible by default. A table with six columns needs enough horizontal space to show all six columns side by side. That is just how they work.

When you view that same table on a 375px mobile screen, the browser has two choices: squish everything (which makes it unreadable) or let it overflow (which breaks the layout). Most browsers choose overflow.

Unlike text, which wraps and reflows naturally, table cells do not wrap onto the next line by default. Each column holds its ground. So if your table needs 700px of horizontal space and the screen is only 375px wide, something has to give.

The good news: CSS gives us full control over this. There are three main approaches to fixing it, and each one fits different situations. Let us go through all of them.

Live Demo

See the Overflow Problem in Action

Viewport: Full width
⚠ No fix applied
Product Category Price Stock Rating Status
Wireless Headphones Electronics $79.99 142 units 4.7 / 5 Active
Leather Notebook Stationery $24.00 38 units 4.5 / 5 Low Stock
Mechanical Keyboard Electronics $149.00 0 units 4.9 / 5 Out of Stock

Scroll sideways to see all columns

Click “Simulate Mobile (340px)” in the demo above, then toggle between “Without Fix” and “With overflow-x: auto” to see the difference. That single CSS property is the basis for the first technique.


02Fix One: Horizontal Scroll, The Quick and Reliable Method

The simplest way to make a responsive HTML table is to wrap it in a container div and tell that container to scroll horizontally when the table is wider than the screen.

The table itself stays exactly as it is. You are not changing anything about the table structure. You are just putting it inside a container that handles the overflow gracefully instead of breaking the layout.

Coding the Scroll Wrapper

The HTML change is minimal:

<!-- Wrap your table in a div -->
<div class="table-scroll-wrapper">
  <table>
    <thead>
      <tr>
        <th scope="col">Product</th>
        <th scope="col">Category</th>
        <th scope="col">Price</th>
        <th scope="col">Stock</th>
        <th scope="col">Rating</th>
        <th scope="col">Status</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Wireless Headphones</td>
        <td>Electronics</td>
        <td>$79.99</td>
        <td>142 units</td>
        <td>4.7 / 5</td>
        <td>Active</td>
      </tr>
    </tbody>
  </table>
</div>

And the CSS is just three lines:

.table-scroll-wrapper {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch; /* smooth scroll on iOS */
  width: 100%;
}

That is it. The overflow-x: auto tells the browser: if the content inside this container is wider than the container itself, show a horizontal scrollbar. The -webkit-overflow-scrolling: touch makes that scrolling feel smooth and native on iOS devices.

Adding a Scroll Hint So Users Know to Scroll

Here is a real problem with this approach: many users do not realize they can scroll. They see a table, assume it is cut off, and leave.

A simple fade effect on the right edge is a great visual cue. It tells users: there is more here, keep going.

.table-scroll-wrapper {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  width: 100%;
  position: relative;
}

/* Fade edge hint */
.table-scroll-wrapper::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  width: 50px;
  height: 100%;
  background: linear-gradient(to right, transparent, #your-bg-color);
  pointer-events: none; /* so it doesn't block clicks */
}

/* Hide the hint once the user has scrolled to the end */
.table-scroll-wrapper.at-end::after {
  opacity: 0;
}

/* Optional: visible scroll label for extra clarity */
.scroll-hint {
  font-size: 0.78rem;
  color: #888;
  margin-top: 6px;
  display: flex;
  align-items: center;
  gap: 5px;
}

You can also use a small JavaScript snippet to remove the fade once the user has scrolled all the way to the right, so it does not look like the data is still cut off when it is not.

💡
The scroll wrapper approach is best for data-heavy tables where all columns are equally important, like reports, dashboards, price comparison tables, and financial data. The user needs all the data, so hiding columns is not an option. Let them scroll.

Live Demo

Scrollable Table with Fade Hint

This is the scroll wrapper technique in action. On a small screen, scroll the table sideways to see all columns.

Order ID Customer Product Amount Date Status Shipping
#ORD-1042 Sarah Chen Noise-Cancelling Earbuds $89.00 Apr 7, 2026 Shipped Express
#ORD-1043 James Okafor Mechanical Keyboard $149.00 Apr 8, 2026 Processing Standard
#ORD-1044 Maria Santos USB-C Hub (7-in-1) $55.00 Apr 8, 2026 Shipped Express
#ORD-1045 Alex Müller Wireless Mouse $39.00 Apr 9, 2026 Cancelled N/A

Scroll the table to see all columns on small screens

03Fix Two: Card-Based Table Layout for Mobile

The scroll wrapper works well. But there is a more elegant approach for many situations: transform the table into a card layout on mobile screens.

Instead of showing all columns side by side in one wide row, each table row becomes its own card. The column header appears as a label next to each data value. It is easier to read, it feels natural on mobile, and it keeps all the data visible without any side-scrolling.

Here is the visual difference:

Live Demo

Table vs Card Layout, Toggle Between Them

Name Role Department Joined Status
Priya Nair Frontend Developer Engineering Jan 2024 Active
Carlos Rivera UX Designer Product Mar 2023 Active
Yuki Tanaka Data Analyst Analytics Sep 2022 On Leave
Omar Farooq Backend Developer Engineering Nov 2021 Active

Notice how switching to card mode makes each row a self-contained unit? Each data point has its label right next to it. No header row needed. A user on a 375px phone can read this without any trouble at all.

Adding data-label to Your HTML

The technique relies on a custom data-* attribute on each table cell. You add data-label="Column Name" to every <td>. This stores the column header as a data attribute directly on the cell, so CSS can pull it out and display it on mobile.

Here is how the HTML looks:

<table class="responsive-table">
  <thead>
    <tr>
      <th>Name</th>
      <th>Role</th>
      <th>Department</th>
      <th>Joined</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <!-- Each td gets data-label matching the th above it -->
      <td data-label="Name">Priya Nair</td>
      <td data-label="Role">Frontend Developer</td>
      <td data-label="Department">Engineering</td>
      <td data-label="Joined">Jan 2024</td>
      <td data-label="Status">Active</td>
    </tr>
    <tr>
      <td data-label="Name">Carlos Rivera</td>
      <td data-label="Role">UX Designer</td>
      <td data-label="Department">Product</td>
      <td data-label="Joined">Mar 2023</td>
      <td data-label="Status">Active</td>
    </tr>
  </tbody>
</table>

There is nothing fancy going on in the HTML itself. The data-label attribute is just storing a string. The magic happens entirely in CSS.

The CSS That Transforms Your Table

This is the part most tutorials rush through, but let me walk you through every line so it actually makes sense:

/* On small screens, transform the table into cards */
@media (max-width: 600px) {

  /* Step 1: Remove the default table layout */
  .responsive-table,
  .responsive-table thead,
  .responsive-table tbody,
  .responsive-table th,
  .responsive-table td,
  .responsive-table tr {
    display: block;
  }

  /* Step 2: Hide the header row completely */
  /* (we'll show the labels with CSS instead) */
  .responsive-table thead tr {
    display: none;
  }

  /* Step 3: Each row becomes a card */
  .responsive-table tbody tr {
    background: #f9f9f9;
    border-radius: 8px;
    margin-bottom: 1rem;
    padding: 0.5rem 0;
    border: 1px solid #e5e5e5;
    box-shadow: 0 2px 6px rgba(0,0,0,0.06);
  }

  /* Step 4: Each cell becomes a row with two columns */
  .responsive-table td {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 16px;
    border-bottom: 1px solid #f0f0f0;
    text-align: right;
    font-size: 0.9rem;
  }

  .responsive-table tr td:last-child {
    border-bottom: none;
  }

  /* Step 5: Pull the column name from data-label and display it */
  .responsive-table td::before {
    content: attr(data-label); /* This is the key line */
    font-weight: 600;
    font-size: 0.8rem;
    color: #555;
    text-align: left;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
}

/* Desktop: normal table layout (no changes needed) */
.responsive-table {
  width: 100%;
  border-collapse: collapse;
}

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

.responsive-table th {
  background: #f5f5f5;
  font-weight: 600;
}

The key line is content: attr(data-label). The CSS attr() function reads the value of any HTML attribute and uses it as content for the ::before pseudo-element. So when the screen is small, every cell in the table automatically shows its own label, read directly from the data-label attribute you wrote in the HTML.

💡
This is a pure HTML and CSS technique. No JavaScript needed at all for the mobile card layout. That said, CSS is introduced here just to support the topic. We will cover CSS media queries and all of this in full detail in the CSS series later. For now, you just need to understand what the CSS is doing.

04Fix Three: Hiding Table Columns on Small Screens

Sometimes the table has more columns than a mobile user actually needs. Think of a product table with columns for Name, SKU, Description, Price, Discount, Inventory, Warehouse Location, Last Updated, and Status.

A mobile shopper cares about Name, Price, and Status. The rest is mostly for admins or desktop users. So rather than making them scroll through ten columns, you can hide the less important ones on small screens.

Column Priority Classes

The cleanest way to handle this is to assign priority classes to your columns. Here is a simple system that works really well in practice:

<table class="priority-table">
  <thead>
    <tr>
      <!-- Essential: always visible -->
      <th class="col-essential">Product</th>

      <!-- Important: visible on medium screens and up -->
      <th class="col-important">Category</th>

      <!-- Essential: always visible -->
      <th class="col-essential">Price</th>

      <!-- Optional: only on large screens -->
      <th class="col-optional">SKU</th>

      <!-- Optional: only on large screens -->
      <th class="col-optional">Warehouse</th>

      <!-- Essential: always visible -->
      <th class="col-essential">Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td class="col-essential">Wireless Headphones</td>
      <td class="col-important">Electronics</td>
      <td class="col-essential">$79.99</td>
      <td class="col-optional">WH-XM5-BLK</td>
      <td class="col-optional">Seattle WH-B</td>
      <td class="col-essential">Active</td>
    </tr>
  </tbody>
</table>

And the CSS to control visibility at each breakpoint:

/* Hide optional columns on small and medium screens */
@media (max-width: 768px) {
  .col-optional {
    display: none;
  }
}

/* Hide important columns on very small screens only */
@media (max-width: 480px) {
  .col-important {
    display: none;
  }
}

/* col-essential columns are always visible: no rule needed */

The idea is simple: decide which columns are truly essential (show always), which are important (show on medium screens and up), and which are optional (show only on desktop). Apply the matching class to both the <th> and every <td> in that column.

Interactive Demo

Toggle Column Visibility

Click the checkboxes to hide or show columns. This simulates what different screen sizes would see:






Product Category Price SKU Warehouse Status
Wireless Headphones Electronics $79.99 WH-XM5-BLK Seattle WH-B Active
Leather Notebook A5 Stationery $24.00 NB-LTH-A5-BR Austin WH-C Low Stock
Mechanical Keyboard Electronics $149.00 KB-MCH-TKL Seattle WH-B Out of Stock

Try unchecking SKU and Warehouse (the optional columns), then category too. This is exactly what CSS media queries do automatically for different screen sizes.

Deciding Which Columns to Keep Visible

When you are deciding column priority, think like your mobile user. Ask yourself: if someone is looking at this table on their phone, what is the one thing they are trying to find out?

  • For a product table, they want the name and price.
  • For an order table, they want the order ID and status.
  • For a team directory, they want the name and role.

Everything else can either be hidden on mobile or revealed when the user taps/clicks to expand. Start with two or three essential columns visible on mobile, and add more as screen space grows.


05Responsive Table Examples: A Full Build

Let us put everything together in one complete, production-ready example. This combines the scroll wrapper for medium screens and the card layout for small screens.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Responsive Table Example</title>
  <style>

    /* Base table styles */
    .orders-table-wrap {
      overflow-x: auto;
      -webkit-overflow-scrolling: touch;
      width: 100%;
      border-radius: 10px;
      border: 1px solid #e5e7eb;
    }

    .orders-table {
      width: 100%;
      min-width: 560px;
      border-collapse: collapse;
      font-size: 0.9rem;
      color: #374151;
    }

    .orders-table th {
      background: #f9fafb;
      font-weight: 600;
      padding: 12px 16px;
      text-align: left;
      border-bottom: 2px solid #e5e7eb;
      white-space: nowrap;
      color: #6b7280;
      font-size: 0.78rem;
      text-transform: uppercase;
      letter-spacing: 0.06em;
    }

    .orders-table td {
      padding: 12px 16px;
      border-bottom: 1px solid #f3f4f6;
    }

    .orders-table tr:last-child td {
      border-bottom: none;
    }

    .orders-table tr:hover td {
      background: #f9fafb;
    }

    /* Status badges */
    .badge {
      display: inline-block;
      padding: 3px 10px;
      border-radius: 999px;
      font-size: 0.72rem;
      font-weight: 600;
    }

    .badge-green { background: #dcfce7; color: #16a34a; }
    .badge-yellow { background: #fef9c3; color: #ca8a04; }
    .badge-red { background: #fee2e2; color: #dc2626; }

    /* Card layout for small screens */
    @media (max-width: 560px) {

      .orders-table-wrap {
        border: none;
        overflow-x: visible;
      }

      .orders-table,
      .orders-table thead,
      .orders-table tbody,
      .orders-table th,
      .orders-table td,
      .orders-table tr {
        display: block;
      }

      .orders-table thead tr {
        display: none;
      }

      .orders-table tbody tr {
        background: #ffffff;
        border: 1px solid #e5e7eb;
        border-radius: 10px;
        margin-bottom: 14px;
        box-shadow: 0 2px 6px rgba(0,0,0,0.05);
        padding: 4px 0;
      }

      .orders-table td {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 10px 16px;
        border-bottom: 1px solid #f3f4f6;
        text-align: right;
      }

      .orders-table tr td:last-child {
        border-bottom: none;
      }

      .orders-table td::before {
        content: attr(data-label);
        font-weight: 600;
        font-size: 0.75rem;
        color: #9ca3af;
        text-transform: uppercase;
        letter-spacing: 0.05em;
        text-align: left;
      }
    }

  </style>
</head>
<body>

  <div class="orders-table-wrap">
    <table class="orders-table">
      <thead>
        <tr>
          <th>Order ID</th>
          <th>Customer</th>
          <th>Amount</th>
          <th>Date</th>
          <th>Status</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td data-label="Order ID">#1042</td>
          <td data-label="Customer">Sarah Chen</td>
          <td data-label="Amount">$89.00</td>
          <td data-label="Date">Apr 7, 2026</td>
          <td data-label="Status"><span class="badge badge-green">Shipped</span></td>
        </tr>
        <tr>
          <td data-label="Order ID">#1043</td>
          <td data-label="Customer">James Okafor</td>
          <td data-label="Amount">$149.00</td>
          <td data-label="Date">Apr 8, 2026</td>
          <td data-label="Status"><span class="badge badge-yellow">Processing</span></td>
        </tr>
        <tr>
          <td data-label="Order ID">#1044</td>
          <td data-label="Customer">Maria Santos</td>
          <td data-label="Amount">$55.00</td>
          <td data-label="Date">Apr 8, 2026</td>
          <td data-label="Status"><span class="badge badge-green">Shipped</span></td>
        </tr>
        <tr>
          <td data-label="Order ID">#1045</td>
          <td data-label="Customer">Alex Müller</td>
          <td data-label="Amount">$39.00</td>
          <td data-label="Date">Apr 9, 2026</td>
          <td data-label="Status"><span class="badge badge-red">Cancelled</span></td>
        </tr>
      </tbody>
    </table>
  </div>

</body>
</html>

Resize your browser window to under 560px and you will see this table transform from a standard table layout into individual cards, with the column labels appearing automatically from the data-label attributes.


This is a complete, working responsive table example you can copy and adapt right now. The only thing you need to adjust is the breakpoint (560px), the colors to match your site, and the column count. The technique scales to any number of columns.

06Mobile Data Presentation Strategies: When to Use What

Three techniques, and each one fits different situations. Here is a quick guide to picking the right one:

Horizontal Scroll

All columns are equally important, user needs to compare across them

  • Financial reports
  • Price comparison tables
  • Dashboard data grids
  • Sports statistics
  • Schedule tables

Card Layout

Each row is a standalone item that makes sense on its own

  • Order history
  • Team directories
  • Product listings
  • Invoice line items
  • Search results

Column Hiding

Some columns are supplementary or only relevant to power users

  • Admin panels
  • Data management tables
  • Product inventory
  • CRM tables
  • Any table with 7+ columns

You can also mix techniques. A table might use column hiding to drop optional columns on mobile, plus a scroll wrapper for medium screens where some columns come back but the table is still a bit wide. That combination works really well.

⚠️
One thing to keep in mind: accessibility. When you hide columns or transform the layout, make sure screen readers still get the full data. The card layout technique using CSS display: block keeps all the data in the HTML, so screen readers can still access it. The data-label approach also helps, since the labels provide context even when the header row is hidden. We covered table accessibility in depth in the table attributes article and the captions article, so refer back to those if you need a refresher.

07Common Mistakes That Make Tables Break on Mobile

  • Setting a fixed width directly on the table
    Writing width: 800px or width: 1200px on the <table> element defeats all responsive fixes. The table will always be that fixed size regardless of the container. Use width: 100% and a min-width if you need a minimum, then let the scroll wrapper handle overflow.
  • Putting overflow-x: auto on the table itself instead of a wrapper
    This is a common mistake. overflow-x: auto on a <table> element does not work the way you expect because tables are inherently block elements with complex formatting context. Always put the overflow property on a wrapper <div>.
  • Forgetting the meta viewport tag
    Without <meta name="viewport" content="width=device-width, initial-scale=1.0"> in your <head>, mobile browsers will zoom out and render your page at desktop width. All your responsive CSS will still fire at the wrong size. This is one of those foundational things we covered back in the HTML document structure article.
  • Using tables for page layout instead of data
    This one is not about responsiveness specifically, but it makes responsive fixes much harder. Tables should display tabular data, not create page layouts. If you are trying to make a two-column page layout with a table, stop and use CSS flexbox or grid instead (coming up in the CSS series).
  • Not testing on an actual mobile device
    Browser DevTools mobile simulation is useful, but it is not perfect. Touch scrolling, font rendering, and layout behavior can differ on real hardware. Always test your mobile-friendly tables on at least one real phone before shipping.
  • Missing data-label attributes on some cells
    In the card layout technique, if even one <td> is missing its data-label, that cell will have no label on mobile. The data will show up as an unlabeled value with no context. Always add data-label to every single <td> in every row.

08Responsive HTML Tables Quick Reference Cheat Sheet

Responsive Tables: Everything You Need

Scroll Wrapper
.wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; width: 100%; }
Card Layout: HTML
<td data-label="Column Name"> value </td>
Card Layout: Key CSS
td::before { content: attr(data-label); }
Hide Column CSS
@media (max-width: 600px) { .col-optional { display: none; } }
Card Mode: Block
table, thead, tbody, th, td, tr { display: block; }
Hide Header Row
thead tr { display: none; }
iOS Smooth Scroll
-webkit-overflow-scrolling: touch;
Fade Edge Hint
.wrap::after { content: ""; position: absolute; right: 0; background: linear-gradient(to right, transparent, #bg-color); }

09This Closes Out Batch 6

You now know how to build tables properly and make them work on every screen size. That is a genuinely useful combination. Most developers learn one or the other, and the gap shows up in their projects.

To quickly recap everything from Batch 6: you started by understanding what tables are and when to use them, then learned to structure them properly with thead, tbody, and tfoot, then tackled colspan, rowspan, and accessibility attributes, added meaningful captions and summary information, and now you know how to make all of it work on mobile too.

Up next is Batch 7, Forms and User Input. Forms are how your pages start listening to users, and it is one of the most important parts of learning HTML. You will build real interactive forms from the ground up. See you there.

Responsive and Mobile-Friendly Tables in HTML

Choose a difficulty to load your challenge

Loading challenge…

Ready to Practice?

Select a difficulty level above to load your challenge.

Easy

1
Live Preview
Great Work!