Open demo

CSS code

HTML
<h2 class="rt-title" id="rt-title">Recent orders</h2>
<p class="rt-sub">Resize the window: below 620px the rows restack into labelled cards — no JavaScript, just CSS and <code>data-label</code>.</p>
<table class="rt" aria-labelledby="rt-title">
  <thead><tr><th scope="col">Order</th><th scope="col">Customer</th><th scope="col">Total</th><th scope="col">Placed</th><th scope="col">Status</th></tr></thead>
  <tbody>
    <tr><th scope="row" data-label="Order">#20431</th><td data-label="Customer">Ines Duarte</td><td data-label="Total">$182.40</td><td data-label="Placed">Jun 30</td><td data-label="Status"><span class="rt-pill rt-ship">Shipped</span></td></tr>
    <tr><th scope="row" data-label="Order">#20430</th><td data-label="Customer">Kofi Mensah</td><td data-label="Total">$64.00</td><td data-label="Placed">Jun 30</td><td data-label="Status"><span class="rt-pill rt-proc">Processing</span></td></tr>
    <tr><th scope="row" data-label="Order">#20429</th><td data-label="Customer">Hana Sato</td><td data-label="Total">$319.95</td><td data-label="Placed">Jun 29</td><td data-label="Status"><span class="rt-pill rt-deliv">Delivered</span></td></tr>
    <tr><th scope="row" data-label="Order">#20428</th><td data-label="Customer">Lucas Meyer</td><td data-label="Total">$47.10</td><td data-label="Placed">Jun 29</td><td data-label="Status"><span class="rt-pill rt-deliv">Delivered</span></td></tr>
    <tr><th scope="row" data-label="Order">#20427</th><td data-label="Customer">Priya Raman</td><td data-label="Total">$228.60</td><td data-label="Placed">Jun 28</td><td data-label="Status"><span class="rt-pill rt-ship">Shipped</span></td></tr>
  </tbody>
</table>
CSS
/* Responsive stack table: a normal table on wide screens; below 620px every row becomes a card
   and each cell prints its column name from data-label. No JavaScript. */
.rt-title{font-size:1.15rem;font-weight:700;letter-spacing:-.01em;margin:0 0 4px}
.rt-sub{color:#5b6a64;font-size:.87rem;margin:0 0 20px}
.rt{--rt-gap:#f5f7f6;width:100%;border-collapse:collapse;font-size:.9rem;background:#fff;border-radius:14px;overflow:hidden;
  box-shadow:0 1px 3px rgba(23,42,36,.07),0 12px 30px -16px rgba(23,42,36,.14)}
.rt th,.rt td{padding:13px 16px;text-align:left}
.rt thead th{background:#0f766e;color:#eafaf7;font-size:.74rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase}
.rt tbody th,.rt td{border-bottom:1px solid #eef3f1;color:#35443f}
.rt tbody tr:last-child>*{border-bottom:0}
.rt tbody th{font-weight:600;color:#12201c}
.rt-pill{font-size:.74rem;font-weight:700;padding:4px 11px;border-radius:999px}
.rt-ship{background:#e0f4ef;color:#0f766e}
.rt-proc{background:#fdf3dc;color:#92660c}
.rt-deliv{background:#e8eefc;color:#3450b0}
@media (max-width:620px){
  .rt thead{display:none}
  .rt,.rt tbody,.rt tr,.rt tbody th,.rt td{display:block;width:100%}
  .rt tr{border-bottom:8px solid var(--rt-gap)}
  .rt tbody th,.rt td{display:flex;justify-content:space-between;align-items:center;gap:16px;text-align:right;border-bottom:1px dashed #e6edea}
  .rt tbody th::before,.rt td::before{content:attr(data-label);font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#5b6a64;text-align:left}
}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<h2 class="rt-title" id="rt-title">Recent orders</h2>
<p class="rt-sub">Resize the window: below 620px the rows restack into labelled cards — no JavaScript, just CSS and <code>data-label</code>.</p>
<table class="table rt align-middle mb-0" aria-labelledby="rt-title">
  <thead><tr><th scope="col">Order</th><th scope="col">Customer</th><th scope="col">Total</th><th scope="col">Placed</th><th scope="col">Status</th></tr></thead>
  <tbody>
    <tr><th scope="row" data-label="Order">#20431</th><td data-label="Customer">Ines Duarte</td><td data-label="Total">$182.40</td><td data-label="Placed">Jun 30</td><td data-label="Status"><span class="badge rounded-pill rt-ship">Shipped</span></td></tr>
    <tr><th scope="row" data-label="Order">#20430</th><td data-label="Customer">Kofi Mensah</td><td data-label="Total">$64.00</td><td data-label="Placed">Jun 30</td><td data-label="Status"><span class="badge rounded-pill rt-proc">Processing</span></td></tr>
    <tr><th scope="row" data-label="Order">#20429</th><td data-label="Customer">Hana Sato</td><td data-label="Total">$319.95</td><td data-label="Placed">Jun 29</td><td data-label="Status"><span class="badge rounded-pill rt-deliv">Delivered</span></td></tr>
    <tr><th scope="row" data-label="Order">#20428</th><td data-label="Customer">Lucas Meyer</td><td data-label="Total">$47.10</td><td data-label="Placed">Jun 29</td><td data-label="Status"><span class="badge rounded-pill rt-deliv">Delivered</span></td></tr>
    <tr><th scope="row" data-label="Order">#20427</th><td data-label="Customer">Priya Raman</td><td data-label="Total">$228.60</td><td data-label="Placed">Jun 28</td><td data-label="Status"><span class="badge rounded-pill rt-ship">Shipped</span></td></tr>
  </tbody>
</table>
CSS
/* Bootstrap .table with a stacked layout below 620px (Bootstrap has no stacked table, so the media query is ours).
   Each cell prints its column name from data-label. */
.rt-title{font-size:1.15rem;font-weight:700;letter-spacing:-.01em;line-height:normal;margin:0 0 4px}
.rt-sub{color:#5b6a64;font-size:.87rem;margin:0 0 20px}
.rt-sub code{color:inherit}
.rt{--rt-gap:#f5f7f6;--bs-table-bg:#fff;--bs-table-color:#35443f;--bs-table-border-color:#eef3f1;font-size:.9rem;
  border-radius:14px;overflow:hidden;box-shadow:0 1px 3px rgba(23,42,36,.07),0 12px 30px -16px rgba(23,42,36,.14)}
.rt>:not(caption)>*>*{padding:13px 16px}
.rt>thead>tr>th{--bs-table-bg:#0f766e;--bs-table-color:#eafaf7;border-bottom:0;font-size:.74rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase}
.rt>tbody>tr:last-child>*{border-bottom:0}
.rt tbody th{font-weight:600;--bs-table-color:#12201c}
.rt .badge{--bs-badge-padding-x:11px;--bs-badge-padding-y:4px;--bs-badge-font-size:.74rem;--bs-badge-font-weight:700;line-height:normal;display:inline}
.rt-ship{background:#e0f4ef;color:#0f766e}
.rt-proc{background:#fdf3dc;color:#92660c}
.rt-deliv{background:#e8eefc;color:#3450b0}
@media (max-width:620px){
  .rt thead{display:none}
  .rt,.rt tbody,.rt tr,.rt tbody th,.rt td{display:block;width:100%}
  .rt>tbody>tr{border-bottom:8px solid var(--rt-gap)}
  .rt>tbody>tr>*{display:flex;justify-content:space-between;align-items:center;gap:16px;text-align:right;border-bottom:1px dashed #e6edea}
  .rt>tbody>tr:last-child>*{border-bottom:0}
  .rt tbody th::before,.rt td::before{content:attr(data-label);font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#5b6a64;text-align:left}
}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<h2 id="rt-title" class="mb-1 text-[1.15rem] font-bold tracking-[-.01em]">Recent orders</h2>
<p class="mb-5 text-[.87rem] text-[#5b6a64]">Resize the window: below 620px the rows restack into labelled cards — no JavaScript, just CSS and <code>data-label</code>.</p>
<!-- Below 620px (max-[620px]:) each row turns into a card and every cell prints its data-label. -->
<table aria-labelledby="rt-title" class="w-full border-collapse overflow-hidden rounded-[14px] bg-white text-[.9rem] shadow-[0_1px_3px_rgba(23,42,36,.07),0_12px_30px_-16px_rgba(23,42,36,.14)] max-[620px]:block">
  <thead class="bg-[#0f766e] text-left text-[.74rem] font-bold tracking-[.06em] text-[#eafaf7] uppercase max-[620px]:hidden [&_th]:px-4 [&_th]:py-[13px]"><tr><th scope="col">Order</th><th scope="col">Customer</th><th scope="col">Total</th><th scope="col">Placed</th><th scope="col">Status</th></tr></thead>
  <tbody class="text-[#35443f] max-[620px]:block [&>tr]:max-[620px]:block [&>tr]:max-[620px]:border-b-8 [&>tr]:max-[620px]:border-[#f5f7f6] [&>tr>*]:border-b [&>tr>*]:border-[#eef3f1] [&>tr>*]:px-4 [&>tr>*]:py-[13px] [&>tr>*]:text-left [&>tr>*]:max-[620px]:flex [&>tr>*]:max-[620px]:w-full [&>tr>*]:max-[620px]:items-center [&>tr>*]:max-[620px]:justify-between [&>tr>*]:max-[620px]:gap-4 [&>tr>*]:max-[620px]:border-dashed [&>tr>*]:max-[620px]:border-[#e6edea] [&>tr>*]:max-[620px]:text-right [&>tr>*]:max-[620px]:before:text-left [&>tr>*]:max-[620px]:before:text-[.72rem] [&>tr>*]:max-[620px]:before:font-bold [&>tr>*]:max-[620px]:before:tracking-[.06em] [&>tr>*]:max-[620px]:before:text-[#5b6a64] [&>tr>*]:max-[620px]:before:uppercase [&>tr>*]:max-[620px]:before:content-[attr(data-label)] [&>tr:last-child>*]:border-b-0 [&_th]:font-semibold [&_th]:text-[#12201c]">
    <tr><th scope="row" data-label="Order">#20431</th><td data-label="Customer">Ines Duarte</td><td data-label="Total">$182.40</td><td data-label="Placed">Jun 30</td><td data-label="Status"><span class="rounded-full bg-[#e0f4ef] px-[11px] py-1 text-[.74rem] font-bold text-[#0f766e]">Shipped</span></td></tr>
    <tr><th scope="row" data-label="Order">#20430</th><td data-label="Customer">Kofi Mensah</td><td data-label="Total">$64.00</td><td data-label="Placed">Jun 30</td><td data-label="Status"><span class="rounded-full bg-[#fdf3dc] px-[11px] py-1 text-[.74rem] font-bold text-[#92660c]">Processing</span></td></tr>
    <tr><th scope="row" data-label="Order">#20429</th><td data-label="Customer">Hana Sato</td><td data-label="Total">$319.95</td><td data-label="Placed">Jun 29</td><td data-label="Status"><span class="rounded-full bg-[#e8eefc] px-[11px] py-1 text-[.74rem] font-bold text-[#3450b0]">Delivered</span></td></tr>
    <tr><th scope="row" data-label="Order">#20428</th><td data-label="Customer">Lucas Meyer</td><td data-label="Total">$47.10</td><td data-label="Placed">Jun 29</td><td data-label="Status"><span class="rounded-full bg-[#e8eefc] px-[11px] py-1 text-[.74rem] font-bold text-[#3450b0]">Delivered</span></td></tr>
    <tr><th scope="row" data-label="Order">#20427</th><td data-label="Customer">Priya Raman</td><td data-label="Total">$228.60</td><td data-label="Placed">Jun 28</td><td data-label="Status"><span class="rounded-full bg-[#e0f4ef] px-[11px] py-1 text-[.74rem] font-bold text-[#0f766e]">Shipped</span></td></tr>
  </tbody>
</table>

About this set

An orders table that stays a normal table on wide screens and turns every row into a labelled card below 620 pixels. There is no JavaScript: each cell carries a data-label attribute with its column name, and inside the media query the table parts switch to display:block, the header row is hidden and each cell prints its label with content: attr(data-label) on the left and its value on the right. An 8 pixel border in the page colour separates the cards. Use this pattern when a table has five or six short columns and people will read it one record at a time on a phone, such as orders, bookings or tickets; for wide numeric tables a horizontal scroll works better. The example has an order number as the row header, customer, total, date and a coloured status pill for Shipped, Processing and Delivered. The Bootstrap version keeps .table and .badge and adds the stacking media query, since Bootstrap has no stacked table of its own; the Tailwind version expresses the same rules with max-[620px]: utilities and before:content-[attr(data-label)].

What’s included

  • Rows become labelled cards below 620px with CSS only
  • Column names come from data-label via content: attr()
  • Order number marked up as a row header
  • Status pills for Shipped, Processing and Delivered
  • Bootstrap .table and Tailwind max-[620px]: variants of the same layout

Accessibility

The table is labelled by its heading, with scope on column and row headers. The status pill text and the stacked labels meet 4.5:1 contrast. Nothing is interactive, so there is no focus handling to add. Some older screen reader and browser combinations drop table semantics when table elements are set to display:block, so test with your audience's tools if the stacked view is critical.

Customise it

The breakpoint is the 620px media query (max-[620px]: in Tailwind). Colours sit on .rt: header #0f766e, card gap --rt-gap, and the .rt-ship, .rt-proc and .rt-deliv pills; Bootstrap uses --bs-table-* variables on .rt.