Source Code

<div class="wrap">
  <div class="table-head">
    <h2 class="table-title">Course Schedule by Department</h2>
    <span class="hint">Click a department cell to collapse its group</span>
  </div>
  <table class="tbl" id="tbl">
    <thead>
      <tr>
        <th>Department</th>
        <th>Course</th>
        <th>Instructor</th>
        <th class="num">Seats</th>
      </tr>
    </thead>
    <tbody id="tbody"></tbody>
  </table>
</div>

Table with Merged Row Groups (rowspan) — Free HTML CSS JS Snippet

Table with Merged Row Groups (rowspan) · Tables · Plain HTML, CSS & JS · Live preview

What's included

Features

True HTML rowspan merges the category column across each group, not a CSS-only visual approximation
rowspan value computed automatically from each group's row count, never hardcoded per group
Click-to-collapse toggle per group hides its rows while keeping the merged category cell visible
Rotating chevron communicates collapse state using the same convention as this library's accordion patterns
Row visibility toggled via a numeric group-index data attribute, avoiding fragile string-based selector matching
Per-row seat availability color-coding composes independently of the grouping and collapse mechanics
Course count shown inline in the merged department cell for quick scanning even while collapsed
Works with any number of groups or rows per group — all HTML is generated from the data structure

About this UI Snippet

Table with Merged Row Groups — rowspan Category Column & Click-to-Collapse

Screenshot of the Table with Merged Row Groups (rowspan) snippet rendered live

Repeating the same department or category name on every row of a grouped table wastes visual space and makes it harder to see where one group ends and the next begins — a merged cell that spans the whole group communicates both facts at once: what the group is, and exactly how many rows belong to it. This table builds that merged-cell layout directly with the HTML rowspan attribute, and layers a click-to-collapse toggle on top so any group can be tucked away without leaving the table.

rowspan computed from group length, not hardcoded

render() only emits a <td class="dept-cell" rowspan="..."> on the *first* row of each group (ri === 0), and its rowspan value is set directly from group.rows.length — so a department with three courses gets rowspan="3", a department with two gets rowspan="2", with no manual counting required anywhere. Every subsequent row in that group simply omits the department <td> entirely, letting the browser's native table layout algorithm handle the vertical merge — this is what rowspan does structurally, not a CSS visual trick layered on top of a normal grid of cells.

Collapse state lives on the DOM, driven by a data attribute match

Clicking a .dept-cell toggles its own .collapsed class and then queries every tr[data-group="N"].grouped-row sharing that group's index to hide or show them via a .hidden class (display: none). Every row — including the first row that holds the merged department cell — carries a data-group attribute, but only rows *after* the first get the additional .grouped-row class, since the first row (which holds the always-visible merged department cell) should never itself be hidden when its own group collapses — only the rows beneath it.

A rotating chevron indicates collapse state without extra text

The department cell's (down-caret) character is wrapped in a .chev span with a CSS transition: transform 0.2s; adding .collapsed to the parent .dept-cell rotates it -90deg to point sideways, the same expand/collapse convention used throughout this library's accordion and tree patterns, applied here inside a table cell instead of a standalone button.

Seat availability color-coded independently of the grouping

seatsHtml() renders each row's seat count as "Full" (red) at zero seats, a red "N left" warning at three or fewer, or a green "N open" otherwise — a status signal layered on top of, and entirely independent from, the department grouping and collapse mechanics. This demonstrates that the rowspan-merge pattern composes cleanly with other per-row styling; grouping the rows visually doesn't require flattening or simplifying what each individual row can show.

Why the group index, not the department name, drives the query

Hiding rows uses data-group="N" (a numeric array index) rather than matching on the department name string — avoiding any ambiguity if two groups happened to share a display name, and avoiding the need to escape a name value into a CSS attribute-selector string, which is a more fragile approach once department names can contain quotes or other characters that need selector-escaping.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the rowspan value is computed from group.rows.length rather than hardcoded, and why only rows after the first row in each group receive the class used for hiding. The same assistant can help optimize it — for instance asking whether using a numeric group index for the data-group attribute is more robust than using the department name directly, and why. It's also useful for extending the table: ask it to support nested sub-groups with their own rowspan levels, add a "collapse all" / "expand all" control, or persist which groups are collapsed across a page reload using localStorage. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a grouped data table using the HTML rowspan attribute to merge a category column, with a per-group click-to-collapse toggle, in plain HTML, CSS, and JavaScript — no framework, no library.

Requirements:
- Structure the source data as an array of group objects, each with a category name and an array of row objects (for example a course name, an instructor, and a numeric seats-remaining value).
- Render one actual HTML table row per data row, but only emit the category table cell on the first row of each group, giving that cell a rowspan attribute whose value is computed automatically from the number of rows in that group (never hardcoded) so the browser's native table layout visually merges it down the full height of the group.
- Every row within a group (including the first) must carry a data attribute identifying which group index it belongs to; only rows after the first row in a group should carry an additional class marking them as hideable, since the first row holds the always-visible merged category cell.
- Make each merged category cell clickable: clicking it must toggle a "collapsed" state that hides every hideable row belonging to that specific group (using a CSS class, not removing them from the DOM) while leaving the merged category cell and its row visible, and must rotate a chevron icon inside that cell to indicate the current collapsed/expanded state. Clicking a collapsed group's cell must expand it again.
- Independently of the grouping, render each row's numeric seats value with its own color-coded status text (for example a red "Full" state at zero, a red low-availability warning under a small threshold, and a green "open" state otherwise) to demonstrate that per-row styling composes cleanly with the row-merging structure.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Click any department cellCollapses that department's course rows, leaving only the merged department cell visible with its course count.
  2. 2
    Click again to expandThe chevron rotates back and the group's rows reappear.
  3. 3
    Notice the seat availability colorsZero seats shows red "Full," three or fewer shows a red "N left" warning, and anything above shows a green "N open."
  4. 4
    Replace GROUPS with real dataUpdate the array with your own department/category names and per-row course, instructor, and seat data — rowspan values compute automatically from each group's row count.
  5. 5
    Adjust the seat-warning thresholdChange the "seats <= 3" condition in seatsHtml() to whatever count should trigger the warning styling.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a Tailwind CSS version.

Real-world uses

Common Use Cases

Course catalogs and academic scheduling tools
The core use case — group courses by department with a merged label column, letting students collapse departments they are not interested in.
Categorized product or inventory listings
Any table grouped by a category (product line, region, team) benefits from a merged category column instead of a repeated value on every row.
Grouped financial or reporting tables
Pair with the Grouped Rows Table pattern for reports organized by account category, cost center, or region.
Staff scheduling and shift assignment grids
Group shifts or assignments by team or location with a collapsed default view for teams not currently being reviewed.
Learn rowspan-based table grouping
A clean example of computing rowspan values from data length and correctly scoping a collapse toggle to only the rows that should hide, leaving the merged cell's own row always visible.

Got questions?

Frequently Asked Questions

render() sets the rowspan attribute on each group's first-row department cell directly to group.rows.length — the number of course rows in that group. A department with three courses automatically gets rowspan="3" with no manual counting or hardcoded values anywhere in the code.

The .grouped-row class is only applied to rows after the first row in each group (ri > 0). The first row holds the merged department cell via rowspan and must always stay visible even when its own group is collapsed, since that cell is what the user clicks to expand the group again — only the rows beneath it should ever be hidden.

Matching hidden rows against a numeric array index (data-group="2") avoids two problems a name-based approach would have: ambiguity if two groups ever shared the same display name, and the need to safely escape a name string for use inside a CSS attribute selector, which becomes fragile once names can contain quotes or special characters.

The department cell's .chev span has a CSS transition on transform. When the parent .dept-cell gains the .collapsed class (toggled on click), a rule rotates the chevron -90 degrees; removing the class rotates it back to its default downward-pointing orientation. This is the same rotating-indicator convention used by this library's accordion and tree-menu patterns.

No — the click handler reads only the clicked cell's own data-group value and queries rows matching that exact group index, so collapsing or expanding one department has no effect on any other department's rows or collapse state.

Add more objects to the GROUPS array (each with a dept name and a rows array), or add more row objects to an existing group's rows array. The rowspan value, row rendering, and collapse-toggle logic all derive from the array structure automatically, requiring no other code changes.