Chapter 20: Responsive Tables

Stacked / Card Layout Tables

Transforming rigid multi-column HTML tables into native-feeling mobile cards using pure CSS `display: block`, accessible off-screen header hiding, and pseudo-element `attr(data-label)` label generation.

LEARNING OBJECTIVES
  • Understand the mechanics of transforming tabular 2D structures into vertical card stacks using CSS media queries.
  • Implement pseudo-element content injection with td::before { content: attr(data-label); } to retain field context on mobile.
  • Apply accessible visual hiding techniques (clip-path: inset(50%) or position: absolute) to remove <thead> visually without breaking screen readers.
  • Address the WebKit/Blink accessibility tree degradation bug where display: block strips native table semantics.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Imagine a medical patient chart in an emergency room. On the doctor's wide desktop monitor, the chart is displayed as an expansive horizontal spreadsheet with columns for Time, Heart Rate, Blood Pressure, Medication, and Nurse Notes.

Desktop Spreadsheet (Horizontal Matrix):
+----------+------------+----------------+------------+---------------------+
| Time     | Heart Rate | Blood Pressure | Medication | Nurse Notes         |
+----------+------------+----------------+------------+---------------------+
| 08:00 AM | 72 bpm     | 120/80 mmHg    | Saline IV  | Stable post-op      |
| 09:30 AM | 88 bpm     | 135/85 mmHg    | Morphine 5mg| Pain reported      |
+----------+------------+----------------+------------+---------------------+

Mobile Transformation -> Stacked Patient Cards (Vertical Units):
+------------------------------------------+
| RECORD #1 (08:00 AM)                     |
|  • Time:           08:00 AM              |
|  • Heart Rate:     72 bpm                |
|  • Blood Pressure: 120/80 mmHg           |
|  • Medication:     Saline IV             |
|  • Notes:          Stable post-op        |
+------------------------------------------+
+------------------------------------------+
| RECORD #2 (09:30 AM)                     |
|  • Time:           09:30 AM              |
|  • Heart Rate:     88 bpm                |
|  • Blood Pressure: 135/85 mmHg           |
|  • Medication:     Morphine 5mg          |
|  • Notes:          Pain reported         |
+------------------------------------------+

When a triage nurse walks the floor with a handheld smartphone, they don't want to pan horizontally across 5 columns. Instead, each row (<tr>) is transformed into an isolated, beautifully padded index card, and each cell (<td>) becomes a discrete vertical key-value line item. The column header labels are dynamically stamped in front of each value using CSS.


Technical Deep Dive & Specifications

The Pure CSS Stacked-Card Transformation Algorithm

To transform a table into vertical cards below a specific viewport breakpoint (e.g., @media (max-width: 768px)), we systematically change the layout display properties of all structural table tags:

+-------------------------------------------------------------------------------+
| DOM Node   | Desktop Default Display          | Mobile Transformation Display |
+-------------------------------------------------------------------------------+
| <table>    | display: table                   | display: block                |
| <thead>    | display: table-header-group      | display: none (or sr-only)    |
| <tbody>    | display: table-row-group         | display: block                |
| <tr>       | display: table-row               | display: block (Card Box)     |
| <td>       | display: table-cell              | display: flex / block (Row)   |
+-------------------------------------------------------------------------------+

The attr(data-label) Pseudo-Element Pattern

Because <thead> is hidden on mobile screens, data cells lose their visual column context. To fix this, we attach custom HTML5 data attributes (data-label="...") to each <td>, and retrieve them via CSS using the attr() function inside a ::before pseudo-element:

<!-- HTML -->
<td data-label="Blood Pressure">120/80 mmHg</td>
/* CSS */
@media (max-width: 768px) {
  td {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 12px;
  }

  td::before {
    content: attr(data-label);
    font-weight: 700;
    color: #475569;
    text-align: left;
    margin-right: 12px;
  }
}
Rendered Mobile Cell Box:
+------------------------------------------------------------------+
| [::before pseudo-element]             | [Native Text Node]       |
| "Blood Pressure"                      | "120/80 mmHg"            |
| (content: attr(data-label))           |                          |
+------------------------------------------------------------------+

The Accessibility Tree Stripping Hazard & Fix

Historically, browser layout engines (notably Safari WebKit and Google Chrome Blink) tied accessibility tree roles directly to CSS display modes. When you set display: block or display: flex on a <table>, <tr>, or <td>, the engine would strip role="table" and role="cell", degrading the table into generic <div> blocks. Screen readers would announce "list of text" instead of "Table: 5 columns, 3 rows".

To safeguard accessibility across all browsers, we attach explicit ARIA tabular roles whenever non-table CSS display transformations are applied:

<!-- Robust Accessible Markup with Fallback ARIA Roles -->
<table role="table">
  <thead role="rowgroup">
    <tr role="row">
      <th role="columnheader">Metric</th>
      <th role="columnheader">Value</th>
    </tr>
  </thead>
  <tbody role="rowgroup">
    <tr role="row">
      <td role="cell" data-label="Metric">Latency</td>
      <td role="cell" data-label="Value">14ms</td>
    </tr>
  </tbody>
</table>

Visually Hiding <thead> Without Breaking Screen Readers

Never use display: none on <thead> if you rely on standard table navigation for screen reader users on desktop. Instead, use an accessible screen-reader-only utility class:

@media (max-width: 768px) {
  .responsive-card-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    clip-path: inset(50%);
    border: 0;
    white-space: nowrap;
  }
}

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL example.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

💻 Interactive Code Playground

Starter Code

Line-by-Line Code Breakdown

  • Lines 55–60: Below $768\text{px}$, table, tbody, tr, and td are assigned display: block, stripping the native rigid column alignment and allowing each element to stack vertically.
  • Lines 63–73: The thead element is not removed with display: none (which could affect certain screen reader modes), but is visually clipped to $1\text{px} \times 1\text{px}$ off-screen using clip-path: inset(50%).
  • Lines 76–83: Each <tr> receives margin-bottom: 16px; border-radius: 8px; box-shadow: ...; converting the row into a clean, modern card container.
  • Lines 85–92: Each <td> is styled as display: flex; justify-content: space-between; align-items: center;. This creates a two-column key/value row within the card.
  • Lines 100–109: td::before reads attr(data-label) from the HTML attribute and prints the uppercase label on the left side of the card line.
  • Lines 120–152: Every <td> tag is explicitly annotated with data-label="..." and ARIA roles (role="table", role="rowgroup", role="row", role="cell") to maintain structural semantics.

Expected Browser Render Output

  • Desktop Screens ($> 768\text{px}$): A standard dark-header enterprise data table.
  • Mobile Screens ($\le 768\text{px}$): The table collapses into 3 floating white cards. Each card displays 5 key-value lines with grey uppercase labels on the left and bold values/status badges aligned to the right.

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

🏋️ Hands-On Exercise

🎯 The Challenge: Refactor an Employee Directory Table to Responsive Cards

Instructions:

  1. Populate each <td> in the starter code with matching data-label attributes.
  2. In the media query, convert the table structure to cards below 640px.
  3. Add a highlight style to the first <td> in each card so the employee's name acts as a prominent card header.

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Mismatched data-label strings: Hardcoding data-label="Department" on the wrong <td> creates confusing misinformation (e.g., displaying the email address with a "Department" label). Generate data-label dynamically via your templating engine (React, Vue, Jinja, or Blade).
  2. Forgetting to style long content: In display: flex; justify-content: space-between, a very long value (like an address or URL) can crush the ::before pseudo-element. Always set min-width or flex-shrink: 0 on td::before.

💡 Pro Tips

  1. Automate data-label Injection via JavaScript (if HTML is static): If rendering legacy HTML without data-label, inject them once on DOM load:
    document.querySelectorAll('.card-table tbody tr').forEach(row => {
      const headers = Array.from(row.closest('table').querySelectorAll('thead th')).map(th => th.textContent);
      row.querySelectorAll('td').forEach((td, i) => td.setAttribute('data-label', headers[i] || ''));
    });
    
  2. Avoid Table Structure on Non-Tabular Data: If data is always cards on both mobile and desktop (e.g., product listings), use semantic <ul role="list"> and <li> with CSS Grid instead of a <table> transformed via CSS.

📌 Key Takeaways

  • The stacked-card layout converts 2D table rows into standalone vertical card components on narrow viewports.
  • display: block applied to table, tbody, tr, and td breaks rigid column alignment for responsive reflow.
  • Pseudo-element td::before { content: attr(data-label); } restores lost column context on mobile.
  • Explicit ARIA roles (role="table", role="row", role="cell") ensure screen readers preserve semantic relationships even when CSS alters display types.
  • Visually hiding <thead> with clip-path: inset(50%) preserves desktop table accessibility while decluttering mobile screens.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

How does CSS retrieve the text stored inside <td data-label="Price">$49.99</td> to display it inside a pseudo-element?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

What accessibility issue can occur in older WebKit and Chromium browsers when display: block is applied to <table> elements?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

Why is display: none on <thead> discouraged compared to accessible screen-reader-only utility classes?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP