Sticky Header Table — Frozen Panes HTML CSS Snippet

Sticky Header Table · Tables · Plain HTML & CSS · Live preview

Share & Support

What's included

Features

Frozen header row
Header cells use position: sticky; top: 0 inside the scroll container, so column labels stay visible while rows scroll.
Frozen first column
The name cells use position: sticky; left: 0, keeping the row label visible during horizontal scroll.
Locked corner cell
The top-left cell pins with both top: 0 and left: 0 at the highest z-index, anchoring both frozen panes.
Correct z-index stacking
Header (2), column (1), corner (3) ordering prevents body cells from bleeding over the frozen header or column.
Opaque sticky backgrounds
Each sticky cell has a solid background so scrolled content never shows through the frozen header or column.
Two-axis scroll
overflow: auto plus width: max-content; min-width: 100% enables both vertical and horizontal scrolling that fills the card.
Sticky-safe borders
border-collapse: separate with per-cell borders and a 1px ::after edge avoids the broken sticky behaviour of collapsed borders.
Zero JavaScript
The entire frozen-pane behaviour is pure CSS position: sticky — no scroll listeners, no layout thrash, fully accessible.

About this UI Snippet

Sticky Header Table — Frozen Header Row & Frozen First Column With position:sticky

Screenshot of the Sticky Header Table snippet rendered live

Wide data tables are unreadable the moment you scroll: the column headers disappear off the top and you lose track of which row you are on as the first column scrolls off the left. The fix every dashboard and admin panel needs is frozen panes — a header row pinned to the top and a first column pinned to the left, both staying put while the rest of the table scrolls in two dimensions. This snippet implements that in pure HTML and CSS using position: sticky, with zero JavaScript.

The two-axis sticky setup

The table lives in a .sht-scroll container with overflow: auto and a fixed max-height, which is what creates the scroll context. Three sticky rules do the work: the header cells use position: sticky; top: 0 so they pin to the top of that scroll container; the first-column cells use position: sticky; left: 0 so they pin to the left; and the top-left corner cell uses both top: 0 and left: 0 so it stays locked in place as the anchor of both frozen panes.

Why z-index ordering matters

Sticky cells overlap as you scroll, so stacking order is critical. The header row sits at z-index: 2, the frozen first column at z-index: 1, and the corner cell at z-index: 3 so it stays above both when they cross. Without this ordering the body cells would bleed over the frozen header, or the header would slide under the first column — the classic broken-frozen-pane look. Each sticky cell also needs an opaque background (transparent sticky cells let scrolled content show through), which is why the header is #f8fafc and the first column is white.

Border-collapse caveat

position: sticky does not work reliably with border-collapse: collapse because collapsed borders are owned by the table, not the cells. This snippet uses border-collapse: separate; border-spacing: 0 and draws row dividers with per-cell border-bottom, plus a 1px ::after on the frozen column to render the vertical edge that visually separates it from the scrolling area.

Sizing for horizontal scroll

The table uses width: max-content; min-width: 100% so it grows as wide as its content needs (triggering horizontal scroll) but still fills the container when there are few columns. Numeric columns are right-aligned with tabular-nums for clean figure alignment, while the frozen name column is left-aligned.

The result is a robust, dependency-free data grid that scrolls in both directions with headers and the key column always visible. Pair this with a sortable table for column sorting, a pagination table for paging, or a data table with search and row actions.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to puzzle out the z-index stacking order by trial and error. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the corner cell needs both top and left stickiness at z-index 3 while the header sits at 2 and the frozen column at 1, and why border-collapse separate is required instead of the usual collapse. The same assistant can help optimize it — for instance whether a table with hundreds of rows should virtualize its body instead of relying on the browser to lay out every row, or whether the frozen-column shadow effect needs a scroll listener at all. It's also useful for extending the table: ask it to freeze a second column, add a scrolled-state box-shadow cue on the frozen edges, or make columns resizable while keeping the sticky behavior intact. 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 data table with a frozen header row and a frozen first column in plain HTML and CSS using only position: sticky — no JavaScript, no virtual scrolling library.

Requirements:
- A scroll container with a fixed max-height and overflow auto wrapping a table sized with width: max-content and min-width: 100%, so it can scroll both vertically and horizontally.
- The table must use border-collapse: separate with border-spacing: 0, not border-collapse: collapse, and draw row separators using per-cell border-bottom, because collapsed borders break position: sticky on table cells.
- All header cells must be position: sticky with top: 0 so they remain pinned to the top of the scroll container while the body scrolls vertically underneath them.
- Every row's first cell (the row label) must be position: sticky with left: 0 so it remains pinned to the left edge while the remaining columns scroll horizontally.
- The single top-left corner cell must be position: sticky with both top: 0 and left: 0 simultaneously, so it stays anchored in both directions as the visual anchor of the two frozen panes.
- Assign explicit, correctly ordered z-index values so the corner cell always renders above the header row, and the header row always renders above the frozen column, preventing scrolled body cells from bleeding over either frozen edge.
- Every sticky cell must have an explicit opaque background color, since transparent sticky cells let scrolled content show through underneath them.
- Add a thin vertical divider (e.g. a 1px pseudo-element edge) on the frozen column so its boundary with the scrolling area is visually clear.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA "Team performance" table appears inside a fixed-height, scrollable card with many columns and rows.
  2. 2
    Scroll downThe header row (Role, Deals, Revenue…) stays pinned at the top while the rows scroll beneath it.
  3. 3
    Scroll rightThe first "Member" column stays pinned to the left while the metric columns scroll horizontally.
  4. 4
    Watch the cornerThe top-left "Member" header cell stays locked in both directions as the anchor of the frozen panes.
  5. 5
    Hover a rowThe whole row — including the frozen name cell — highlights together, so the pinned column tracks the hovered row.
  6. 6
    Adjust the freeze heightChange max-height on .sht-scroll to control how many rows show before vertical scrolling kicks in.

Real-world uses

Common Use Cases

Dashboards and admin data grids
The core use — wide metric tables where headers and the key column must stay visible. Pair with a pagination table for large datasets.
Analytics and reporting tables
Revenue, traffic, or KPI breakdowns with many columns; freeze the entity column so figures always have a label.
Spreadsheets and finance apps
Ledgers and statements where the row label and column headers anchor a wide grid of numbers.
Comparison and pricing matrices
Freeze the feature column while plans scroll; combine with a comparison table for plan rows.
Schedules and timetables
Pin the time/room column while days scroll horizontally — works alongside a schedule table.
Leaderboards and rosters
Keep the name column visible while stats scroll; complements a leaderboard table or sortable table.

Got questions?

Frequently Asked Questions

The two most common causes: (1) border-collapse: collapse breaks sticky on table cells — switch to border-collapse: separate; border-spacing: 0 and draw borders per cell (this snippet does). (2) An ancestor has overflow: hidden/auto other than your intended scroll container, which captures the stickiness. Also ensure the scroll container has a constrained height/width so there is something to scroll within.

Make each additional column sticky with left set to the cumulative width of the columns before it (e.g. second frozen column left: 160px if the first is 160px wide). Give them descending z-indexes and opaque backgrounds. Because left offsets must be exact, fixed widths on the frozen columns make this reliable.

Pure CSS cannot detect scroll position, so add a tiny scroll listener that toggles a class on the container when scrollLeft > 0 (and scrollTop > 0), then apply a box-shadow to the frozen column/header in that state. This "scrolled" shadow is a nice cue that content is hidden behind the frozen panes, used by Google Sheets and Airtable.

Yes — it is a real semantic <table> with <thead>, <th> headers (including row headers via <th scope="row">, which you should add), so screen readers announce the structure correctly. position: sticky is purely visual and does not change the DOM order or semantics, so assistive tech is unaffected by the freezing.

It is mostly CSS, so it ports almost verbatim: render the <table> from your data with the same class names and position: sticky rules. In React, map rows from an array and add the sticky classes; in Vue use v-for; in Angular *ngFor. No lifecycle hooks are needed since there is no JavaScript — only add a hook if you implement the optional scroll-shadow cue.