webdevpuneet.com

Table Density Toggle — Free Compact Row Spacing JS Snippet

Table Density Toggle · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="dt-card">
  <div class="dt-bar">
    <h3>Team members</h3>
    <div class="dt-seg" id="dtSeg" role="group" aria-label="Row density">
      <button type="button" data-d="compact" aria-pressed="false">Compact</button>
      <button type="button" data-d="cozy" aria-pressed="true">Cozy</button>
      <button type="button" data-d="comfy" aria-pressed="false">Comfortable</button>
      <span class="dt-glide" id="dtGlide"></span>
    </div>
  </div>
  <div class="dt-scroll">
    <table class="dt-table cozy" id="dtTable">
      <thead><tr><th>Name</th><th>Role</th><th>Status</th></tr></thead>
      <tbody id="dtBody"></tbody>
    </table>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Three densities
Compact, cozy, and comfortable defined purely in CSS.
Single class swap
JS only toggles one class — spacing lives in CSS.
Sliding highlight
A glide pill animates under the active button.
Geometry-measured
offsetLeft/Width adapt to any label widths.
Accessible group
Real buttons with aria-pressed in a labelled group.
rAF first paint
The highlight starts under the default option correctly.
Easy to persist
One class name maps cleanly to localStorage.
No dependency
Pure HTML/CSS/JS — no table or UI library.

About this UI Snippet

Table Density Toggle — Compact / Cozy / Comfortable Rows

Screenshot of the Table Density Toggle snippet rendered live

A density toggle lets users switch a data table between compact, cozy, and comfortable row spacing — the control Gmail, Notion, Linear, and admin dashboards offer so power users can fit more rows on screen while others get breathing room. This snippet builds one in plain HTML, CSS, and vanilla JavaScript: a segmented control with a sliding highlight that swaps a single class on the table to change padding and font size. No dependency.

Density as a single class swap

All three densities are defined in CSS as classes — .compact, .cozy, .comfy — each setting the th/td padding and, for the extremes, a font size. Changing density is just removing the others and adding one: setDensity() does exactly that. Because the spacing lives entirely in CSS, the JavaScript never touches per-cell styles, so the table re-flows instantly and the rule stays the single source of truth for how each mode looks.

A sliding segmented control

The segmented control is the buttons plus one absolutely-positioned .dt-glide pill behind them. On selection, moveGlide() reads the active button's offsetLeft and offsetWidth and sets the pill's left and width, which animate via a CSS transition — so the highlight glides under whichever button you pick rather than snapping. Measuring the real button geometry means it adapts automatically if the labels change width or the control is resized.

Accessible toggle semantics

Each option is a real <button> with aria-pressed reflecting the active density, grouped in a role="group" labelled "Row density". That makes the control keyboard-operable and announces which mode is selected, without needing radio inputs or custom roles. The text color also shifts on the active button so the state is visible as well as audible.

Initial position with rAF

The glide pill needs the active button's measurements, which are only correct after layout. A single requestAnimationFrame defers the first moveGlide() until the browser has laid the buttons out, so the highlight starts under the default "Cozy" option instead of at left: 0.

Persisting the preference

Density is a per-user preference, so in production you'd save the chosen class to localStorage (or a user setting) and apply it on load before first paint to avoid a flash. Since the whole state is one class name on the table, persisting and restoring it is a two-line addition around setDensity().

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA data table renders with a Compact / Cozy / Comfortable switcher.
  2. 2
    Click a densityThe row padding and font size change instantly across the table.
  3. 3
    Watch the highlightA pill glides under the selected segment button.
  4. 4
    Compare the modesCompact fits more rows; comfortable adds breathing room.
  5. 5
    Use the keyboardTab to the buttons and press Enter — aria-pressed tracks the active mode.
  6. 6
    Persist the choiceSave the active class to localStorage and apply it on load.

Real-world uses

Common Use Cases

Admin record tables
Let users densify a data table of records, with compact, cozy and comfortable spacing defined purely in CSS.
Inbox and email lists
Offer Gmail-style spacing in an email inbox, where JavaScript only swaps one class and the stylesheet does the rest.
View option menus
Pair with a data table column toggle so people tune both which columns show and how tightly rows are packed.
Sortable grid controls
Combine with a sortable table for power users who scan many rows, with the active choice shown by aria-pressed.
Segmented control reuse
Reuse the sliding pill from a segmented control, measuring offsetLeft and width so any label length works.
Related: Row-Level Diff Table
See the Row-Level Diff Table for a related tables pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Each density is a CSS class on the table — .compact, .cozy, .comfy — that sets the th and td padding and font size. setDensity() removes the other classes and adds one, so the browser reflows all rows from the new rule. The JavaScript never sets per-cell styles, keeping CSS the single source of truth.

A single absolutely-positioned pill sits behind the buttons. On each selection, moveGlide() reads the active button's offsetLeft and offsetWidth and sets the pill's left and width, which animate via a CSS transition. Measuring the real geometry means it adapts to any label width without hardcoded positions.

The pill needs the active button's measured size, which is only correct after the browser lays the buttons out. Calling moveGlide() inside a requestAnimationFrame defers it one frame until layout is done, so the highlight starts under the default option instead of jumping from left: 0.

Because the entire state is one class name on the table, save it to localStorage in setDensity() and read it back on load, applying the class before first paint to avoid a flash. For logged-in users, store it as a profile setting and hydrate it the same way.

Hold the active density in state and bind it as the table's class. Render the segment buttons from the density list with aria-pressed tied to state. For the sliding pill, use a ref to measure the active button in a layout effect (useLayoutEffect, onMounted, ngAfterViewInit) and set the pill's style. In Tailwind, map each density to a set of padding utilities.