Skip to content

feat(DataGrid, Pagination): sticky footer with numbered pagination and rows per page - #1920

Merged
Stephen Watkins (stephenjwatkins) merged 14 commits into
mainfrom
feat/datagrid-sticky-footer
Sep 2, 2026
Merged

feat(DataGrid, Pagination): sticky footer with numbered pagination and rows per page#1920
Stephen Watkins (stephenjwatkins) merged 14 commits into
mainfrom
feat/datagrid-sticky-footer

Conversation

@stephenjwatkins

@stephenjwatkins Stephen Watkins (stephenjwatkins) commented Sep 2, 2026

Copy link
Copy Markdown
Member

📝 Changes

Adds a sticky footer to <DataGrid /> and the pagination pieces it's built from.

<DataGrid />

  • renderFooter renders a bar pinned to the bottom of the scroll container, modeled on the existing sticky header — it reuses the edge interceptors for its shadow-on-scroll and pins horizontally so it stays put while rows scroll sideways.
  • <DataGrid.Footer /> lays the bar out with purely positional start, center, and end slots, so it stays unaware of what it holds and the data grid isn't tied to any one pagination scheme.
  • <DataGrid.Pagination /> and <DataGrid.RowsPerPage /> are fully controlled presets for the common case. Either can be swapped for a custom control, or renderFooter can skip <DataGrid.Footer /> entirely.
  • Sticky shadows are lightened (opacity 0.50.35), which affects the frozen column shadows as well as the new footer's.

<Pagination />

  • <Pagination.Pages /> is a numbered scheme alongside the existing <Pagination.Dropdown />, for when the user should be able to see and reach nearby pages directly. The scheme is inferred from the child rather than configured. Optional First/Last jump buttons keep the ends of a long range reachable once it truncates.
  • <Pagination.RowsPerPage /> is a fully controlled page-size menu. It's a sibling of <Pagination /> rather than a child, since that slot takes a scheme and the two sit side by side in design.
  • Both take a size of sm or md, resolved to the nearest tokens on the space scale.

✅ Checklist

Easy UI has certain UX standards that must be met. In general, non-trivial changes should meet the following criteria:

  • Visuals match Design Specs in Figma
  • Stories accompany any component changes
  • Code is in accordance with our style guide
  • Design tokens are utilized
  • Unit tests accompany any component changes
  • TSDoc is written for any API surface area
  • Specs are up-to-date
  • Console is free from warnings
  • No accessibility violations are reported
  • Cross-browser check is performed (Chrome, Safari, Firefox)
  • Changeset is added

Strikethrough any items that are not applicable to this pull request.

Adds `<Pagination.Pages />` alongside the existing `<Pagination.Dropdown />`
for cases where the user should be able to see and reach nearby pages
directly. The scheme is inferred from the child rather than configured, since
the two schemes differ only in how they let the user reach a page.

Also adds optional `First`/`Last` jump buttons, which the numbered scheme
needs to keep the ends of a long range reachable once they truncate.
Adds a `renderFooter` prop that renders a sticky bar pinned to the bottom of
the data grid's scroll container, modeled on the existing sticky header: it
reuses the edge interceptors for its shadow-on-scroll and pins horizontally so
it stays put while the rows scroll sideways.

`<DataGrid.Footer />` lays the bar out with purely positional `start`,
`center`, and `end` slots, so it stays unaware of what it holds and the data
grid isn't tied to any one pagination scheme. `<DataGrid.Pagination />` and
`<DataGrid.RowsPerPageMenu />` are fully controlled presets for the common
case; either can be swapped for a custom control, or `renderFooter` can skip
`<DataGrid.Footer />` entirely.
The shadow is absolutely positioned, so its containing block is the shell's
padding box, which the 1px top border pushes down. That shifted the whole 8px
band down onto the border and painted over it, so while scrolled the crisp rule
vanished and the ramp terminated in nothing but the bar -- reading as a thicker,
mushier shadow than the header's, which has no border to clear.

Pulling the band up by the border width lands it on the border-box edge instead.
Measured against the header, both are now 8-row ramps over the same values, each
terminated by a hard edge.
Drops `sticky-shadow-opacity` from 0.5 to 0.35, which softens the header's and
footer's bands from a darkest value of rgb(188,199,219) to rgb(208,215,229).

The token is shared, so this lightens the frozen column shadows by the same
amount rather than leaving them heavier than the horizontal ones.
Design has two Page Toolbars: a 38px "Default" and the 24px "Small" the
data grid footer embeds. Only the small one was built, so `size` now
picks between them, defaulting to `md` to match design's naming.

The sizes are described as component tokens on `.sizeSm`/`.sizeMd` so the
squares, jump widths, and both gaps move together rather than each part
branching on the size. The chevron is the exception: `<Icon />` is sized
in React, so the nav button still branches, the way `<Button />` does.

Two things the design context turned up about the small variant along the
way: the page numbers are `small-button` type rather than `subtitle2`
(same 12px medium, but the semantic token), and the ellipsis is Blue 800
like the page numbers, not Gray 500. It also shouldn't take a pointer
cursor, so `cursor: pointer` moves off the shared box class onto the
three classes that are actually buttons.

At page 5 of 10 with first/last buttons, both sizes come out to design's
widths exactly: 594px for `md`, 396px for `sm`.
The larger size came straight from Figma, and four of its numbers weren't
on the space scale. Rather than leave literals in the size tokens, each
one moves to its nearest sensible step:

  control size    38px -> space.5  (40px)
  jump width      72px -> space.9  (72px, exact)
  gap between     18px -> space.2  (16px)
  control size    24px -> space.3  (24px, exact)  [small]
  jump width      54px -> space.7  (56px)         [small]

`space.*` for a control dimension follows what's already here, e.g.
`<NexusLayoutActions />`, `<Menu />`, and the `<ColorPicker />` swatches.

The page-row gap was the one value with no defensible neighbor: 6px sits
exactly between space.0.5 and space.1, and rounding it up to 8px would
loosen a run of numbers that reads as a single group. Both sizes now use
space.0.5 there, which turns the gap into a constant, so it drops out of
the size tokens entirely and sits on `.pageRow`.

The cost is that neither size is pixel-identical to Figma anymore: `md`
lands at 592px against design's 594, and `sm` at 400px against 396. The
squares, chevrons, radius, and type are all unchanged.
Design pairs the rows per page control with the numbered pagination scheme
and gives it the same size axis, and the control never depended on the data
grid, so it belongs alongside the scheme it's drawn to match.

It isn't a `<Pagination />` child: that slot takes a scheme, and the two are
siblings in design. `<DataGrid.RowsPerPage />` stays as a thin preset that
pins the size the footer bar is drawn to, mirroring `<DataGrid.Pagination />`.

The size values resolve to the nearest tokens on the space scale, which lands
the medium control within a quarter pixel of design's 209 and the small within
one and a third of its 178. The chevron holds a 24px box at both sizes, which
is what design's layout arithmetic closes on.
Design specifies gray-800 for both the label and the count, which maps to
`color.neutral.800`. The control was on `neutral.900`, pure black, so the text
rendered a shade darker than drawn.
@stephenjwatkins Stephen Watkins (stephenjwatkins) changed the title feat(DataGrid): add a sticky footer feat(DataGrid, Pagination): sticky footer with numbered pagination and rows per page Sep 2, 2026
…token

The gradient band the grid uses for its sticky edges reads heavier under the
footer than under the header: it stacks a hard-edged ramp on top of a white
backdrop that wipes the row it covers, and the footer's border sits right
beneath it, so the two compound into a thick smear rather than a shadow.

`shadow.stuck-from-bottom` is the token for exactly this -- an element stuck to
the bottom of a scroll port -- and `<Modal />`'s sticky footer already uses it.
Being a real blur, it decays to transparent over the rows instead of fading to
white, at roughly half the weight. It also lands on the border box, so the band
no longer needs pulling up past the border to avoid painting over it.

The header keeps the gradient band: its shadow is cast by each sticky cell, and
adjacent box shadows would seam at every column boundary. The footer is a single
full-width element, so it has no such constraint.
The header and frozen columns can't take `shadow.stuck-from-*` the way the
footer does: their shadows are cast by table cells, and box shadows don't paint
on those once the table's borders are collapsed. Their gradient band mimics the
token instead -- `color.neutral.300` at a quarter, which is what the token
resolves to -- so every sticky edge of the grid reads at the same weight.

Also drops the white backdrop the header's band laid under its ramp. It was
wiping the top of whichever row sat beneath the header, which read as the row
being cut short rather than passing under it. Without it the band decays to
transparent over the row, as the frozen columns' bands already did.
A shadow this light is imperceptible under the dark header: the navy-to-row
step is a hard, high-contrast edge, and it swamps a tint of twenty-odd values.
The header now carries its own opacity, at a weight that reads against the
default and emphasized backgrounds while staying well short of the band this
branch started from.

`.headerSecondary` is light enough that the edge does not compete, so it drops
back to the weight the frozen columns and the footer are drawn at.
Read against the navy, the shadow holds up better at a weight closer to what
the grid shipped with than to the rest of its sticky edges.
Design has two variants of the numbered scheme that neither the jump
buttons nor the chevrons cover: a bare run of pages, and a run with the
chevrons but no jump buttons. The chevrons now come and go with their
handlers the way the jump buttons already do, so both fall out of the
same API.
The blue text shift these picked up from the dropdown scheme's buttons
lands harder here, since these rest at primary.800 rather than
primary.700. They tint their box instead, which is what the page buttons
beside them already do.
@stephenjwatkins
Stephen Watkins (stephenjwatkins) marked this pull request as ready for review September 2, 2026 18:00

@jallen-cyber jallen-cyber left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥

@stephenjwatkins
Stephen Watkins (stephenjwatkins) merged commit 73e4ed2 into main Sep 2, 2026
6 checks passed
@stephenjwatkins
Stephen Watkins (stephenjwatkins) deleted the feat/datagrid-sticky-footer branch September 2, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants