Skip to content

feat (DataTable): add anchored header for groups#686

Merged
paanSinghCoder merged 5 commits intomainfrom
feat/anchored-group-header
Mar 12, 2026
Merged

feat (DataTable): add anchored header for groups#686
paanSinghCoder merged 5 commits intomainfrom
feat/anchored-group-header

Conversation

@paanSinghCoder
Copy link
Contributor

@paanSinghCoder paanSinghCoder commented Mar 10, 2026

Description

feat (data-table): add anchored group header. Toggled with stickyGroupHeader. Default false

Test it here: https://apsara-git-feat-anchored-group-header-raystack.vercel.app/examples/datatable

Screen.Recording.2026-03-10.at.4.36.37.PM.mov

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactor (no functional changes, no bug fixes just code improvements)
  • Chore (changes to the build process or auxiliary tools and libraries such as documentation generation)
  • Style (changes that do not affect the meaning of the code (white-space, formatting, etc))
  • Test (adding missing tests or correcting existing tests)
  • Improvement (Improvements to existing code)
  • Other (please specify)

How Has This Been Tested?

[Describe the tests that you ran to verify your changes]

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (.mdx files)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works

Screenshots (if appropriate):

[Add screenshots here]

Related Issues

[Link any related issues here using #issue-number]

Summary by CodeRabbit

  • New Features

    • Sticky group header anchoring for DataTable to keep group labels visible while scrolling
    • Interactive DataTable example demonstrating infinite scroll functionality
  • Documentation

    • Updated DataTable component documentation with sticky group header usage guide
  • Style

    • Improved accessibility in sidebar navigation components

@vercel
Copy link

vercel bot commented Mar 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
apsara Ready Ready Preview, Comment Mar 12, 2026 7:26am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 364413a9-ab84-42a2-97d4-03139809e72b

📥 Commits

Reviewing files that changed from the base of the PR and between 6301c79 and da3b152.

📒 Files selected for processing (2)
  • apps/www/src/content/docs/components/datatable/props.ts
  • packages/raystack/components/data-table/data-table.tsx

📝 Walkthrough

Walkthrough

The PR introduces a stickyGroupHeader feature for the DataTable component that anchors group labels during scrolling in both virtualized and non-virtualized modes. The feature is threaded through the component architecture via context, accompanied by CSS styling, type definitions, documentation, and a new example page demonstrating infinite scroll with sample data.

Changes

Cohort / File(s) Summary
DataTable Core Implementation
packages/raystack/components/data-table/data-table.tsx, data-table.types.tsx
Added stickyGroupHeader?: boolean prop to DataTableRoot and DataTableProps; threaded prop through context value for consumption by child components.
Content Rendering
packages/raystack/components/data-table/components/content.tsx, components/virtualized-content.tsx
Implemented sticky group header rendering: Content propagates stickyGroupHeader to GroupHeader with stickySectionHeader flag; VirtualizedContent introduces scroll-based state management (stickyGroup, headerRef, headerHeight) and renders sticky anchor rows with group labels during scroll.
Styling
packages/raystack/components/data-table/data-table.module.css
Added .stickySectionHeader and .stickyGroupAnchor CSS classes with positioning, z-index, background, padding, and subtle border/shadow styling.
Documentation
apps/www/src/content/docs/components/datatable/index.mdx, props.ts
Added "Anchor group title" documentation section describing stickyGroupHeader behavior with TSX usage examples; included prop definition with default false behavior.
Example Implementation
apps/www/src/app/examples/datatable/page.tsx
Added new full-screen DataTable example page with sidebar, navbar, infinite scroll via loadMore function, multi-column configuration with filters and grouping, and sample dataset with pagination controls (PAGE_SIZE=20, MAX_ROWS=200).
Minor Accessibility Fix
packages/raystack/sidebar/sidebar-item.tsx
Corrected ARIA attribute naming from ariaHidden to aria-hidden for standards compliance.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • rsbh
  • rohanchkrabrty

Poem

🐰 A hop through sticky headers fair,
Where group labels dance in scrolling air,
VirtualRows anchor'd, anchors fixed with grace,
DataTable rows find their resting place!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature added: an anchored (sticky) group header for the DataTable component.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/anchored-group-header

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@paanSinghCoder paanSinghCoder changed the title feat: add anchored header for groups feat (DataTable): add anchored header for groups Mar 10, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/raystack/components/data-table/data-table.module.css (1)

172-179: Fixed header height assumption may be fragile.

The top: var(--rs-space-10) assumes a fixed table header height. If the header height varies (e.g., multi-line headers, different font sizes), the sticky section header may not align correctly under it. Consider whether this could be made more dynamic or documented as a constraint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/data-table/data-table.module.css` around lines
172 - 179, .stickySectionHeader currently uses a fixed top: var(--rs-space-10)
which breaks if the table header height changes; update the approach so the
sticky section uses a dynamic header height: either introduce and use a
dedicated CSS custom property (e.g. top: var(--table-header-height,
var(--rs-space-10))) and ensure the table header sets --table-header-height to
its computed height, or add a small script that measures the actual header
element height (querySelector for the table header element) and sets the
.stickySectionHeader style.top (or document.documentElement
style.setProperty('--table-header-height', `${height}px`)) on mount and window
resize; reference .stickySectionHeader and the new --table-header-height
variable (or the header measurement logic) when applying the change.
packages/raystack/components/data-table/components/virtualized-content.tsx (3)

341-354: Consider extracting duplicated group label rendering.

The sticky anchor content (lines 347-352) duplicates the rendering logic from VirtualGroupHeader (lines 74-78). Consider extracting the shared label+badge rendering into a reusable component:

♻️ Suggested extraction
// Extract shared rendering
function GroupLabelContent({ data }: { data: GroupedData<unknown> }) {
  return (
    <Flex gap={3} align='center'>
      {data.label}
      {data.showGroupCount ? (
        <Badge variant='neutral'>{data.count}</Badge>
      ) : null}
    </Flex>
  );
}

// Then use in both places:
// In VirtualGroupHeader:
<div role='row' className={styles.virtualSectionHeader} style={style}>
  <GroupLabelContent data={data} />
</div>

// In sticky anchor:
{stickyGroupHeader && isGrouped && stickyGroup && (
  <div role='row' className={styles.stickyGroupAnchor} style={{ top: headerHeight }}>
    <GroupLabelContent data={stickyGroup} />
  </div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/data-table/components/virtualized-content.tsx`
around lines 341 - 354, The sticky group anchor duplicates the label+badge
render logic found in VirtualGroupHeader; extract that shared markup into a
small presentational component (e.g., GroupLabelContent) that accepts the
grouped data shape and renders the label and optional Badge, then replace the
inline JSX in both VirtualGroupHeader (use GroupLabelContent with the existing
data prop) and the sticky anchor block (use GroupLabelContent with stickyGroup)
so both places reference the same component and remove the duplicated code.

267-278: Performance: virtualizer in dependency array causes callback recreation on every render.

useVirtualizer returns a new object reference each render, so including virtualizer in the dependency array means updateStickyGroup is recreated every render. This cascades to handleVirtualScroll and the useLayoutEffect at line 305-307, causing unnecessary work.

Consider reading the virtualizer via a ref or accessing getVirtualItems() directly within the scroll handler without memoizing this function separately:

♻️ Suggested refactor using ref pattern
+  const virtualizerRef = useRef(virtualizer);
+  virtualizerRef.current = virtualizer;
+
   const updateStickyGroup = useCallback(() => {
     if (!stickyGroupHeader || !isGrouped || groupHeaderList.length === 0) {
       setStickyGroup(null);
       return;
     }
-    const items = virtualizer.getVirtualItems();
+    const items = virtualizerRef.current.getVirtualItems();
     const firstIndex = items[0]?.index ?? 0;
     const current = groupHeaderList
       .filter(g => g.index <= firstIndex)
       .pop()?.data;
     setStickyGroup(current ?? null);
-  }, [stickyGroupHeader, isGrouped, groupHeaderList, virtualizer]);
+  }, [stickyGroupHeader, isGrouped, groupHeaderList]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/data-table/components/virtualized-content.tsx`
around lines 267 - 278, The callback updateStickyGroup is being recreated every
render because virtualizer (from useVirtualizer) is a new object reference each
time; remove virtualizer from the dependency array and instead read
virtualizer.getVirtualItems() via a stable ref or directly inside the scroll
handler so updateStickyGroup only depends on stable values (stickyGroupHeader,
isGrouped, groupHeaderList). Concretely: create a ref (e.g., virtualizerRef) and
assign the virtualizer to it after initialization, update updateStickyGroup to
call virtualizerRef.current?.getVirtualItems() rather than referencing
virtualizer in the closure, and update any consumers (handleVirtualScroll and
the useLayoutEffect) to use the ref so the callbacks are no longer recreated
each render.

305-307: Minor: Redundant dependencies in effect.

groupHeaderList and isGrouped are already captured in updateStickyGroup's dependency array, making them redundant here. If the callback recreation issue from the previous comment is addressed, you can simplify:

✨ Suggested simplification
   useLayoutEffect(() => {
     if (stickyGroupHeader) updateStickyGroup();
-  }, [stickyGroupHeader, updateStickyGroup, groupHeaderList, isGrouped]);
+  }, [stickyGroupHeader, updateStickyGroup]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/data-table/components/virtualized-content.tsx`
around lines 305 - 307, The useLayoutEffect dependency array includes redundant
entries: remove groupHeaderList and isGrouped from the dependencies since they
are already captured by the updateStickyGroup callback; keep stickyGroupHeader
and updateStickyGroup in the useLayoutEffect dependency list (and ensure
updateStickyGroup's own dependencies correctly include groupHeaderList and
isGrouped) so the effect only runs when stickyGroupHeader or the callback
changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/www/src/content/docs/components/datatable/props.ts`:
- Around line 39-43: Update the JSDoc for the grouping sticky-label prop in
apps/www/src/content/docs/components/datatable/props.ts so the human-readable
sentence matches the actual default: remove or correct the "(default)" token in
the phrase "When true (default)" and instead state the behavior with the
true/false semantics and keep the `@defaultValue` false unchanged; ensure the
description reads something like "When true, the current group label sticks
under the table header while scrolling (anchor group title). `@defaultValue`
false" so docs and implementation align.

---

Nitpick comments:
In `@packages/raystack/components/data-table/components/virtualized-content.tsx`:
- Around line 341-354: The sticky group anchor duplicates the label+badge render
logic found in VirtualGroupHeader; extract that shared markup into a small
presentational component (e.g., GroupLabelContent) that accepts the grouped data
shape and renders the label and optional Badge, then replace the inline JSX in
both VirtualGroupHeader (use GroupLabelContent with the existing data prop) and
the sticky anchor block (use GroupLabelContent with stickyGroup) so both places
reference the same component and remove the duplicated code.
- Around line 267-278: The callback updateStickyGroup is being recreated every
render because virtualizer (from useVirtualizer) is a new object reference each
time; remove virtualizer from the dependency array and instead read
virtualizer.getVirtualItems() via a stable ref or directly inside the scroll
handler so updateStickyGroup only depends on stable values (stickyGroupHeader,
isGrouped, groupHeaderList). Concretely: create a ref (e.g., virtualizerRef) and
assign the virtualizer to it after initialization, update updateStickyGroup to
call virtualizerRef.current?.getVirtualItems() rather than referencing
virtualizer in the closure, and update any consumers (handleVirtualScroll and
the useLayoutEffect) to use the ref so the callbacks are no longer recreated
each render.
- Around line 305-307: The useLayoutEffect dependency array includes redundant
entries: remove groupHeaderList and isGrouped from the dependencies since they
are already captured by the updateStickyGroup callback; keep stickyGroupHeader
and updateStickyGroup in the useLayoutEffect dependency list (and ensure
updateStickyGroup's own dependencies correctly include groupHeaderList and
isGrouped) so the effect only runs when stickyGroupHeader or the callback
changes.

In `@packages/raystack/components/data-table/data-table.module.css`:
- Around line 172-179: .stickySectionHeader currently uses a fixed top:
var(--rs-space-10) which breaks if the table header height changes; update the
approach so the sticky section uses a dynamic header height: either introduce
and use a dedicated CSS custom property (e.g. top: var(--table-header-height,
var(--rs-space-10))) and ensure the table header sets --table-header-height to
its computed height, or add a small script that measures the actual header
element height (querySelector for the table header element) and sets the
.stickySectionHeader style.top (or document.documentElement
style.setProperty('--table-header-height', `${height}px`)) on mount and window
resize; reference .stickySectionHeader and the new --table-header-height
variable (or the header measurement logic) when applying the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb45d7be-42cc-4ee9-9ce5-155e6f27122c

📥 Commits

Reviewing files that changed from the base of the PR and between 14de0ba and c931f21.

📒 Files selected for processing (9)
  • apps/www/src/app/examples/datatable/page.tsx
  • apps/www/src/content/docs/components/datatable/index.mdx
  • apps/www/src/content/docs/components/datatable/props.ts
  • packages/raystack/components/data-table/__tests__/data-table.test.tsx
  • packages/raystack/components/data-table/components/content.tsx
  • packages/raystack/components/data-table/components/virtualized-content.tsx
  • packages/raystack/components/data-table/data-table.module.css
  • packages/raystack/components/data-table/data-table.tsx
  • packages/raystack/components/data-table/data-table.types.tsx

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/www/src/content/docs/components/datatable/index.mdx (1)

219-230: Make the example show the anchored header immediately.

Right now the snippet only flips stickyGroupHeader; it never enters a grouped state, so readers still need extra setup before anything sticky appears. If this section is meant to build on the category grouping example above, seed query.group_by here (or show the grouping controls) so the behavior is visible on first render.

📝 One way to make the example self-demonstrating
 <DataTable
   data={data}
   columns={columns}
+  query={{ group_by: ["category"] }}
   defaultSort={{ name: "name", order: "asc" }}
-  stickyGroupHeader={true}
+  stickyGroupHeader
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/datatable/index.mdx` around lines 219 -
230, The example toggles stickyGroupHeader but never enables grouping, so
nothing appears anchored; update the DataTable instantiation (the snippet using
DataTable, DataTable.Content, DataTable.VirtualizedContent and
stickyGroupHeader) to include a grouping configuration—for example set
query.group_by to the grouping key used elsewhere (e.g., "category") or pre-seed
the data with a grouped field—so the table renders in a grouped state on first
render and the anchored group header is visible immediately.
packages/raystack/components/data-table/data-table.module.css (1)

173-176: Avoid a fixed sticky offset here.

Line 175 assumes the header is always var(--rs-space-10) tall. Since consumers can restyle header cells, this can drift under supported customizations and either overlap the header or leave a visible gap. Prefer sourcing the offset from a measured or CSS-variable header height instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/data-table/data-table.module.css` around lines
173 - 176, The .stickySectionHeader rule uses a fixed top: var(--rs-space-10)
which can break when consumers restyle headers; change this to derive the offset
from a header-height CSS variable or a measured value at runtime instead. Update
the .stickySectionHeader selector to use a fallback CSS var (e.g. top:
var(--data-table-header-height, var(--rs-space-10))) and/or add code in the
DataTable component (e.g. on mount in the component that renders the header) to
measure the actual header element height and set a --data-table-header-height
custom property (or set style.top) so the sticky offset always matches the
rendered header height. Ensure you reference .stickySectionHeader and the header
element/class used by the DataTable when implementing the measurement and
setting the variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/raystack/components/data-table/data-table.tsx`:
- Around line 51-52: In the props destructuring for the DataTable component, add
the missing comma between stickyGroupHeader = false and getRowId to fix the
syntax error; locate the destructured parameter where stickyGroupHeader and
getRowId are listed (in data-table.tsx) and insert the comma so the
destructuring reads with stickyGroupHeader = false, getRowId, ensuring the
component compiles.

---

Nitpick comments:
In `@apps/www/src/content/docs/components/datatable/index.mdx`:
- Around line 219-230: The example toggles stickyGroupHeader but never enables
grouping, so nothing appears anchored; update the DataTable instantiation (the
snippet using DataTable, DataTable.Content, DataTable.VirtualizedContent and
stickyGroupHeader) to include a grouping configuration—for example set
query.group_by to the grouping key used elsewhere (e.g., "category") or pre-seed
the data with a grouped field—so the table renders in a grouped state on first
render and the anchored group header is visible immediately.

In `@packages/raystack/components/data-table/data-table.module.css`:
- Around line 173-176: The .stickySectionHeader rule uses a fixed top:
var(--rs-space-10) which can break when consumers restyle headers; change this
to derive the offset from a header-height CSS variable or a measured value at
runtime instead. Update the .stickySectionHeader selector to use a fallback CSS
var (e.g. top: var(--data-table-header-height, var(--rs-space-10))) and/or add
code in the DataTable component (e.g. on mount in the component that renders the
header) to measure the actual header element height and set a
--data-table-header-height custom property (or set style.top) so the sticky
offset always matches the rendered header height. Ensure you reference
.stickySectionHeader and the header element/class used by the DataTable when
implementing the measurement and setting the variable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6465f69f-daf1-42e2-b5af-1a556e9fd935

📥 Commits

Reviewing files that changed from the base of the PR and between a513165 and 6301c79.

📒 Files selected for processing (8)
  • apps/www/src/content/docs/components/datatable/index.mdx
  • apps/www/src/content/docs/components/datatable/props.ts
  • packages/raystack/components/data-table/__tests__/data-table.test.tsx
  • packages/raystack/components/data-table/components/content.tsx
  • packages/raystack/components/data-table/components/virtualized-content.tsx
  • packages/raystack/components/data-table/data-table.module.css
  • packages/raystack/components/data-table/data-table.tsx
  • packages/raystack/components/data-table/data-table.types.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/raystack/components/data-table/data-table.types.tsx
  • packages/raystack/components/data-table/tests/data-table.test.tsx
  • packages/raystack/components/data-table/components/virtualized-content.tsx

@paanSinghCoder paanSinghCoder merged commit 6a23e71 into main Mar 12, 2026
5 checks passed
@paanSinghCoder paanSinghCoder deleted the feat/anchored-group-header branch March 12, 2026 07:32
@coderabbitai coderabbitai bot mentioned this pull request Mar 12, 2026
16 tasks
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