webdevpuneet.com

Shift-Click Range Select in a Table — Gmail/Sheets-Style Multi-Row Selection

Shift-Click Range Select in a Table (Gmail/Sheets-Style) · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="demo">
  <div class="table-toolbar">
    <span class="sel-count" id="selCount">0 selected</span>
    <span class="sel-hint">Click a row to select it, Shift-click another to select the range between them, Cmd/Ctrl-click to toggle one row</span>
  </div>
  <table class="sel-table">
    <thead>
      <tr><th class="th-check"></th><th>Name</th><th>Status</th><th>Owner</th></tr>
    </thead>
    <tbody id="tbody">
      <tr data-row><td class="td-check"><input type="checkbox" tabindex="-1" /></td><td>Landing page redesign</td><td><span class="status live">Live</span></td><td>Mia Chen</td></tr>
      <tr data-row><td class="td-check"><input type="checkbox" tabindex="-1" /></td><td>Checkout flow v2</td><td><span class="status draft">Draft</span></td><td>Sam Okoye</td></tr>
      <tr data-row><td class="td-check"><input type="checkbox" tabindex="-1" /></td><td>Pricing page copy</td><td><span class="status review">Review</span></td><td>Jules Park</td></tr>
      <tr data-row><td class="td-check"><input type="checkbox" tabindex="-1" /></td><td>Onboarding emails</td><td><span class="status live">Live</span></td><td>Mia Chen</td></tr>
      <tr data-row><td class="td-check"><input type="checkbox" tabindex="-1" /></td><td>Mobile nav revamp</td><td><span class="status draft">Draft</span></td><td>Ravi Singh</td></tr>
      <tr data-row><td class="td-check"><input type="checkbox" tabindex="-1" /></td><td>API docs refresh</td><td><span class="status review">Review</span></td><td>Sam Okoye</td></tr>
      <tr data-row><td class="td-check"><input type="checkbox" tabindex="-1" /></td><td>Dark mode support</td><td><span class="status live">Live</span></td><td>Jules Park</td></tr>
    </tbody>
  </table>
</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

Genuine anchor-based Shift-click range selection, matching Sheets/Gmail/Finder behavior exactly
Cmd/Ctrl-click toggles individual rows without disturbing the rest of the selection
Anchor updates on plain click and toggle click, but deliberately not on Shift-click — enabling correct mode composition
Checkboxes are a pure visual reflection of selection state, not an independent interactive control (pointer-events: none)
Live selection count updates in the toolbar on every interaction
Selected rows get a distinct background and the current anchor row gets a visible outline
A range selection always replaces the prior selection rather than merging with it, matching real spreadsheet semantics

About this UI Snippet

Shift-Click Range Selection — The Anchor-Based Pattern Behind Sheets and Gmail

Screenshot of the Shift-Click Range Select in a Table (Gmail/Sheets-Style) snippet rendered live

Most custom-built admin tables only support one selection mode: click a checkbox, one row at a time. Power users expect the selection behavior every spreadsheet and email client has trained them on for decades — plain click selects one row, Shift-click selects everything between that row and the last one clicked, and Cmd/Ctrl-click toggles a single row without disturbing the rest of the selection. This snippet implements all three modes correctly, built around the one concept that makes range selection possible: a persistent anchor.

What the anchor actually is

anchorIndex tracks the index of the last row clicked *without* the Shift modifier — not simply "the last row clicked" in general. This distinction is what makes range selection predictable: Shift-clicking always measures a range from that fixed anchor point, regardless of how many Shift-clicks happen afterward. Click row 2, Shift-click row 6 (selects 2–6), then Shift-click row 4 — the range recalculates as 2–4, not "extend from 6 to 4," because the anchor never moved during any of the Shift-click steps. This exactly matches how Sheets and Gmail behave, and is the detail most hand-rolled implementations get wrong by treating "last clicked row" and "anchor" as the same thing.

Why Shift-click replaces the selection instead of adding to it

selectRange() calls selected.clear() before filling in the new range — a fresh Shift-click always defines a brand-new contiguous selection from the anchor, discarding whatever was selected before. This mirrors real spreadsheet behavior: Shift-click is for *defining a range*, not *appending to an existing arbitrary selection* (that's what Cmd/Ctrl-click is for). Conflating the two would make it impossible to ever shrink a range back down after over-shooting it with a previous Shift-click.

Why Cmd/Ctrl-click moves the anchor, but plain click also moves it

Both the toggle path and the plain-click path update anchorIndex to the row that was just clicked — but Shift-click deliberately does not. This means a user can Cmd-click three unrelated rows, and if they then Shift-click a fourth, the range is measured from the *last Cmd-clicked row* (since that click updated the anchor), letting range and toggle selection compose naturally in the same interaction sequence, exactly as it does in real file managers and spreadsheets.

Checkboxes reflect state, they don't drive it

Every row's checkbox has tabindex="-1" and pointer-events: none in the CSS — they're rendered purely as a *visual reflection* of selected, not an independent interactive control a user could click directly. All selection logic lives in the row's own click handler, so there's exactly one source of truth for whether a row is selected, and the checkbox can never fall out of sync with the row's actual selection state.

Step by step

How to Use

  1. 1
    Click a rowSelects just that row and sets it as the anchor for any future Shift-click range.
  2. 2
    Shift-click a different rowSelects every row between the anchor and the clicked row, replacing the previous selection with this new contiguous range.
  3. 3
    Cmd/Ctrl-click a rowToggles just that one row on or off without disturbing the rest of the selection, and moves the anchor to it.
  4. 4
    Combine the twoCmd-click a few individual rows, then Shift-click another — the range is measured from the last Cmd-clicked row, letting the two modes compose naturally.
  5. 5
    Wire up bulk actionsRead the selected Set (or the checked checkboxes) to drive a bulk-action toolbar — the selection state is already fully tracked and ready to use.

Real-world uses

Common Use Cases

ADMIN
Admin data tables
Any internal tool listing rows a user needs to bulk-select — users, orders, content items — benefits from familiar range selection.
EMAIL
Inbox-style list views
Message or notification lists where selecting a contiguous block for a bulk action (archive, delete, mark read) is common.
FILES
File and asset managers
File browser grids and lists where users expect the exact same Shift/Cmd-click conventions from their OS file manager.
SPREADSHEET
Spreadsheet-like data grids
Any grid UI aiming to feel native to users coming from Excel, Sheets, or Airtable benefits from matching this exact interaction model.
Related: Conditional Formatting Table
See the Conditional Formatting Table for a related tables pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

The anchor is the last row clicked without holding Shift — it updates on a plain click or a Cmd/Ctrl-click, but not on a Shift-click. This is what lets repeated Shift-clicks recalculate a range from a stable, fixed point rather than accidentally chaining forward from whichever row was Shift-clicked most recently.

This matches how spreadsheets and file managers behave — Shift-click defines a fresh contiguous range from the anchor. If you want to add a separate non-contiguous row to an existing selection, that is what Cmd/Ctrl-click is for; combining both modifiers in sequence lets you compose ranges and individual toggles together.

Yes — Cmd-click a few individual rows first (each one becomes the new anchor as you go), then Shift-click another row: the range will be measured from the most recently Cmd-clicked row, letting you build up complex selections the same way a spreadsheet allows.

They have pointer-events: none and are purely a visual reflection of the selected Set — this guarantees there is exactly one source of truth for selection state (the row click handler), so the checkbox display can never fall out of sync with the actual selection.

The selected Set holds the currently selected row indices at any time — iterate it (or check each row's .selected class) to build a bulk-action payload referencing the underlying data for those rows.

Shift-click and Ctrl-click are keyboard-modifier-dependent and are primarily a desktop pattern. For touch, pair this with a long-press-to-enter-selection-mode pattern so mobile users have an equivalent way to multi-select without keyboard modifiers.