webdevpuneet.com

Table with Scroll Shadow Indicators — Free HTML CSS JS Snippet

Table with Scroll Shadow Indicators · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="wrap">
  <div class="table-head">
    <h2 class="table-title">Quarterly Revenue by Region</h2>
    <span class="hint" id="hint">Scroll to see more columns →</span>
  </div>
  <div class="table-scroll" id="scrollArea">
    <div class="shadow-left" id="shadowLeft"></div>
    <div class="shadow-right" id="shadowRight"></div>
    <table class="tbl">
      <thead>
        <tr>
          <th class="pin-col">Region</th>
          <th>Q1 2024</th>
          <th>Q2 2024</th>
          <th>Q3 2024</th>
          <th>Q4 2024</th>
          <th>Q1 2025</th>
          <th>Q2 2025</th>
          <th>Q3 2025</th>
          <th>Q4 2025</th>
          <th>YoY growth</th>
        </tr>
      </thead>
      <tbody id="tbody"></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, 7 days free), 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

Edge shadows toggle based on real scrollLeft position, never showing on a side with nothing left to scroll to
Small tolerance (2px) on start/end scroll checks absorbs sub-pixel rounding noise across browsers and zoom levels
Sticky first column via pure CSS position: sticky, no JavaScript repositioning required
Explicit background on pinned cells prevents scrolling columns from visually bleeding through underneath
One-time "scroll to see more" hint that fades out permanently after the first scroll interaction
Shadow visibility recalculates on window resize in case the table's overflow state changes
Passive scroll listener avoids blocking scroll performance
Works with any number of columns — cells are generated per row from a plain values array

About this UI Snippet

Table with Scroll Shadow Indicators — Sticky Row Labels & Direction-Aware Edge Fades

Screenshot of the Table with Scroll Shadow Indicators snippet rendered live

A wide table that simply gets clipped by its container gives no visual hint that there's more to see off to the side — a user has to accidentally discover horizontal scrolling exists. This snippet solves that with two things working together: a sticky first column so row labels stay readable no matter how far the table is scrolled, and a pair of subtle edge-fade shadows that appear and disappear based on actual scroll position, telling the user exactly which direction still has more content.

Shadows reflect real scroll state, not a static hint

updateShadows() runs on every scroll event (and on load, and on resize) and computes atStart/atEnd by comparing el.scrollLeft against 0 and against el.scrollWidth - el.clientWidth (the maximum possible scroll offset). The left shadow's .visible class is toggled to exactly !atStart, and the right shadow's to !atEnd — so at the very start of the table, only the right shadow shows (there's more to the right, nothing to the left); scrolled to the middle, both shadows show; scrolled fully right, only the left shadow shows. A shadow that stayed visible on both sides regardless of actual position would be actively misleading rather than helpful.

A tiny threshold, not an exact zero comparison

The atStart/atEnd checks use <= 2 and >= maxScroll - 2 rather than comparing to exactly 0 or maxScroll. Sub-pixel rounding in how browsers report scrollLeft on high-density displays or with certain zoom levels can leave a value like 0.4 instead of a clean 0 — a strict equality check would then leave a shadow incorrectly visible at the true edge. The small tolerance absorbs that rounding noise.

Sticky first column via position: sticky, not JS positioning

The .pin-col cells use position: sticky; left: 0 — a pure-CSS technique that keeps the region-name column pinned to the left edge of the scrollable container while every other column scrolls beneath it, with no JavaScript needed to reposition anything on scroll. An explicit background on the pinned cells (matching the row's hover or default state) is required so the scrolling columns behind it don't visually bleed through as they pass underneath.

The one-time hint that gets out of the way

A small "Scroll to see more columns →" hint appears next to the table title on load and fades out the first time scrollLeft moves past a few pixels — a nudge for users who might not notice the shadow alone on first glance, without persisting as visual clutter once the user has already discovered they can scroll.

Step by step

How to Use

  1. 1
    Scroll the table horizontallyShadows fade in on whichever side still has more content, and fade out once you reach that edge — try scrolling all the way right, then back.
  2. 2
    Notice the sticky first columnThe Region column stays pinned to the left as you scroll, so row labels are never lost off-screen.
  3. 3
    Resize the browser windowThe shadow visibility recalculates automatically on resize in case the table's scrollability changes.
  4. 4
    Replace REGIONS with real dataUpdate the array with your own row labels, column values, and growth figures — extra columns work automatically since cells are generated from r.values.
  5. 5
    Adjust the shadow width or intensityEdit the width and background gradient opacity on .shadow-left / .shadow-right in the CSS panel.
  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

Financial and analytics reporting tables
The core use case — a wide table of quarterly or monthly figures across many periods, where row labels (accounts, regions, products) must stay readable while scrolling through time periods.
Admin dashboards with wide data grids
Any admin table with more columns than comfortably fit a viewport benefits from both the sticky label column and the scroll affordance.
Spreadsheet-style comparison views
Pair with the Frozen Columns Table pattern for comparing many data points per row across a wide, scrollable data set.
Learn scroll-position-aware UI affordances
A clean example of deriving a UI hint (shadow visibility) directly from real scroll state rather than a static always-on or always-off visual.
Mobile-responsive wide table fallback
A lighter-weight alternative to a full mobile card layout for wide tables that still need to remain tabular on small screens.

Got questions?

Frequently Asked Questions

updateShadows() compares el.scrollLeft to 0 (the start) and to el.scrollWidth minus el.clientWidth (the maximum scrollable offset, the end). The left shadow becomes visible whenever the table is not at the start, and the right shadow whenever it is not at the end — so the shadows always reflect exactly how much scrollable content remains on each side.

Browsers can report scrollLeft as a non-integer value (like 0.4px) due to sub-pixel rendering, especially on high-density displays or certain zoom levels. A strict === 0 check could then fail to detect the true start position, leaving the left shadow incorrectly visible. The small numeric tolerance (<= 2) absorbs that rounding noise reliably.

The .pin-col cells use the CSS position: sticky with left: 0, which keeps them pinned to the left edge of their nearest scrolling ancestor (the .table-scroll container) as the user scrolls horizontally. An explicit background color is set on those cells specifically so the other columns do not show through as they scroll underneath the pinned column.

Yes — a window resize event listener also calls updateShadows(), since resizing can change whether the table even has overflow at all (a table that fit inside a wide viewport with no shadows might suddenly need them once the viewport narrows).

Add more numbers to each region's values array and a matching <th> header cell in the HTML — the row-rendering code maps over r.values to generate one <td> per entry automatically, so no other JavaScript changes are needed for additional columns.

Yes — the scroll-shadow logic in updateShadows() is entirely independent of the sticky column CSS. Remove the position: sticky rules from .pin-col if you only want the fading edge-shadow scroll indicator without a pinned column.