diff --git a/apps/www/src/app/examples/dataview-beta/page.tsx b/apps/www/src/app/examples/dataview-beta/page.tsx
index e57df04f3..fcb7b6723 100644
--- a/apps/www/src/app/examples/dataview-beta/page.tsx
+++ b/apps/www/src/app/examples/dataview-beta/page.tsx
@@ -610,6 +610,7 @@ const Page = () => {
subHeading='Add your first teammate to get started.'
/>
+
diff --git a/apps/www/src/components/dataview-demo.tsx b/apps/www/src/components/dataview-demo.tsx
index 66c741ad0..d92246970 100644
--- a/apps/www/src/components/dataview-demo.tsx
+++ b/apps/www/src/components/dataview-demo.tsx
@@ -227,6 +227,7 @@ export function DataViewSearchDemo() {
No people match your search.
+
@@ -288,6 +289,7 @@ export function DataViewEmptyZeroDemo() {
Nothing here yet.
+
diff --git a/apps/www/src/content/docs/components/dataview/demo.ts b/apps/www/src/content/docs/components/dataview/demo.ts
index 596f6228e..5d4e4e16c 100644
--- a/apps/www/src/content/docs/components/dataview/demo.ts
+++ b/apps/www/src/content/docs/components/dataview/demo.ts
@@ -95,6 +95,7 @@ export const emptyZeroPreview = {
Nothing here yet.
+
`
}
]
@@ -325,6 +326,7 @@ export const searchPreview = {
No people match your search.
+
`
}
]
diff --git a/apps/www/src/content/docs/components/dataview/index.mdx b/apps/www/src/content/docs/components/dataview/index.mdx
index dd7be6dfb..21d35d158 100644
--- a/apps/www/src/content/docs/components/dataview/index.mdx
+++ b/apps/www/src/content/docs/components/dataview/index.mdx
@@ -49,6 +49,7 @@ import {
{/* no matches */}{/* no data yet */}
+
```
@@ -86,10 +87,30 @@ Empty/zero is computed once on context (`isEmptyState`, `isZeroState`) and expos
Nothing here yet.
+
```
Renderers return `null` when `!hasData` — siblings render the messaging.
+### Clear filters
+
+When rows are hidden by filters, `DataView.List` automatically renders a flat
+footer summarising the hidden count with a **Clear Filters** action — the
+default treatment. `DataView.ClearFilters` is a separate sibling entry that
+surfaces the same action as a **bordered panel** in the empty state (a query
+returned no rows). It reads context, resets `filters`/`search` on click, and
+renders nothing outside the empty state — so place it once alongside
+`DataView.List`, separate from `DataView.EmptyState`, and it self-manages
+visibility.
+
+```tsx
+
+
+ No matches for your filters.
+
+
+```
+
### Display Properties (column visibility)
Visibility is a **single global map** on context. `DataView.List` honours it for free (TanStack column visibility hides the grid track). For free-form renderers, wrap fields in `DataView.DisplayAccess`:
@@ -154,6 +175,12 @@ The popover housing the view switcher, Ordering, Grouping, and Display Propertie
+### DataView.ClearFilters
+
+A context-driven "Clear Filters" affordance for the empty state. Place it as a sibling of `DataView.List` (separate from `DataView.EmptyState`); it renders a bordered panel when a query returns no rows and nothing otherwise. The flat footer for the data state is rendered automatically by `DataView.List`.
+
+
+
## Examples
### Search
diff --git a/apps/www/src/content/docs/components/dataview/props.ts b/apps/www/src/content/docs/components/dataview/props.ts
index 5ddae3f84..ed92f4d1b 100644
--- a/apps/www/src/content/docs/components/dataview/props.ts
+++ b/apps/www/src/content/docs/components/dataview/props.ts
@@ -205,6 +205,11 @@ export interface DataViewZeroStateProps {
children: ReactNode;
}
+export interface DataViewClearFiltersProps {
+ /** Class applied to the filter-summary row. */
+ className?: string;
+}
+
export interface DataViewDisplayControlsProps {
/** Custom trigger element for the popover. */
trigger?: ReactNode;
diff --git a/packages/raystack/components/data-table/components/content.tsx b/packages/raystack/components/data-table/components/content.tsx
index 05fdc73bd..86d2ac505 100644
--- a/packages/raystack/components/data-table/components/content.tsx
+++ b/packages/raystack/components/data-table/components/content.tsx
@@ -313,7 +313,10 @@ export function Content({
{showFilterSummary ? (
diff --git a/packages/raystack/components/data-table/data-table.module.css b/packages/raystack/components/data-table/data-table.module.css
index 9d8b69d6b..42583f19c 100644
--- a/packages/raystack/components/data-table/data-table.module.css
+++ b/packages/raystack/components/data-table/data-table.module.css
@@ -63,6 +63,17 @@
box-sizing: border-box;
}
+/* Empty-state variant of the footer: the same row hugs its content and gains a
+ border so it reads as a compact panel instead of a full-width bar. */
+.filterSummaryFooterEmpty {
+ width: fit-content;
+ gap: var(--rs-space-3);
+ margin: var(--rs-space-6) auto var(--rs-space-9);
+ padding: var(--rs-space-3) var(--rs-space-4);
+ border: 0.5px solid var(--rs-color-border-base-primary);
+ border-radius: var(--rs-radius-2);
+}
+
.filterSummaryCount {
color: var(--rs-color-foreground-base-primary);
font-family: var(--rs-font-body);
diff --git a/packages/raystack/components/data-view/__tests__/data-view.test.tsx b/packages/raystack/components/data-view/__tests__/data-view.test.tsx
index 21091511e..052ce042b 100644
--- a/packages/raystack/components/data-view/__tests__/data-view.test.tsx
+++ b/packages/raystack/components/data-view/__tests__/data-view.test.tsx
@@ -648,12 +648,39 @@ describe('DataView', () => {
no matches
+
);
- // In empty state, filter summary is *not* shown by the List (renderer
- // returns null when !hasData). EmptyState sibling handles it. Just
- // confirm the empty state path is taken.
+ // In the empty state the List renders nothing; the sibling
+ // DataView.ClearFilters surfaces the affordance.
expect(screen.getByTestId('empty')).toBeInTheDocument();
+ const clearButton = screen.getByText('Clear Filters');
+ expect(clearButton).toBeInTheDocument();
+
+ // Clearing exits the empty state and reveals the rows.
+ await user.click(clearButton);
+ expect(screen.queryByTestId('empty')).not.toBeInTheDocument();
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
+ });
+
+ it('renders the flat footer summary when rows are hidden by filters', () => {
+ render(
+
+
+
+ );
+ // List auto-renders DataViewClearFilters as the footer (data present).
+ expect(screen.getByText('items hidden by filters')).toBeInTheDocument();
+ expect(screen.getByText('Clear Filters')).toBeInTheDocument();
});
});
diff --git a/packages/raystack/components/data-view/components/clear-filters.tsx b/packages/raystack/components/data-view/components/clear-filters.tsx
new file mode 100644
index 000000000..2a8b61ae1
--- /dev/null
+++ b/packages/raystack/components/data-view/components/clear-filters.tsx
@@ -0,0 +1,112 @@
+'use client';
+
+import { Cross2Icon } from '@radix-ui/react-icons';
+import { cx } from 'class-variance-authority';
+import { useCallback } from 'react';
+import { Button } from '../../button';
+import { Flex } from '../../flex';
+import styles from '../data-view.module.css';
+import { useDataView } from '../hooks/useDataView';
+import {
+ countLeafRows,
+ getClientHiddenLeafRowCount,
+ hasActiveTableFiltering
+} from '../utils';
+
+export interface DataViewClearFiltersProps {
+ className?: string;
+}
+
+/**
+ * The filter-summary row plus a "Clear Filters" action. Reads everything from
+ * `DataView` context and renders nothing when there is nothing to clear.
+ *
+ * Flat by default (the footer `DataView.List` renders when rows are hidden by
+ * filters); a bordered panel in the empty state. Shared between the List footer
+ * and `DataView.ClearFilters` so the markup lives in one place.
+ *
+ * Internal — not exported from the package.
+ */
+export function FilterSummary({ className }: DataViewClearFiltersProps) {
+ const {
+ table,
+ mode,
+ isLoading,
+ totalRowCount,
+ isEmptyState,
+ updateTableQuery
+ } = useDataView();
+
+ const rows = table?.getRowModel()?.rows ?? [];
+ const hiddenLeafRowCount =
+ mode === 'client'
+ ? getClientHiddenLeafRowCount(table)
+ : totalRowCount !== undefined
+ ? Math.max(0, totalRowCount - countLeafRows(rows))
+ : null;
+ const hasActiveFiltering = !isLoading && hasActiveTableFiltering(table);
+ const showFilterSummary =
+ hasActiveFiltering &&
+ (mode === 'server' ||
+ (typeof hiddenLeafRowCount === 'number' && hiddenLeafRowCount > 0));
+
+ const handleClearFilters = useCallback(() => {
+ updateTableQuery(prev => ({ ...prev, filters: [], search: '' }));
+ }, [updateTableQuery]);
+
+ // Matches DataTable: render only when rows are hidden by filters.
+ // `isEmptyState` controls styling (bordered panel), not visibility.
+ if (!showFilterSummary) return null;
+
+ return (
+
+ {mode === 'server' && hiddenLeafRowCount === null ? (
+
+ Some items might be hidden by filters
+
+ ) : (
+
+
+ {hiddenLeafRowCount}
+
+
+ items hidden by filters
+
+
+ )}
+ }
+ onClick={handleClearFilters}
+ >
+ Clear Filters
+
+
+ );
+}
+
+FilterSummary.displayName = 'DataView.FilterSummary';
+
+/**
+ * Surfaces the bordered "Clear Filters" panel in the empty state (a query
+ * returned no rows). Place it as a sibling of `DataView.List` — separate from
+ * `DataView.EmptyState`. Renders nothing outside the empty state; the flat
+ * footer for the data state is rendered automatically by `DataView.List`.
+ */
+export function DataViewClearFilters({ className }: DataViewClearFiltersProps) {
+ const { isEmptyState } = useDataView();
+ if (!isEmptyState) return null;
+ return ;
+}
+
+DataViewClearFilters.displayName = 'DataView.ClearFilters';
diff --git a/packages/raystack/components/data-view/components/list.tsx b/packages/raystack/components/data-view/components/list.tsx
index 6ed467009..e8dccbf53 100644
--- a/packages/raystack/components/data-view/components/list.tsx
+++ b/packages/raystack/components/data-view/components/list.tsx
@@ -1,14 +1,11 @@
'use client';
-import { Cross2Icon } from '@radix-ui/react-icons';
import type { Header, Row } from '@tanstack/react-table';
import { flexRender } from '@tanstack/react-table';
import { cx } from 'class-variance-authority';
import { CSSProperties, useCallback, useEffect, useMemo, useRef } from 'react';
import { Badge } from '../../badge';
-import { Button } from '../../button';
-import { Flex } from '../../flex';
import { Skeleton } from '../../skeleton';
import styles from '../data-view.module.css';
import {
@@ -22,11 +19,7 @@ import { useElementHeight } from '../hooks/useElementHeight';
import { useInfiniteScroll } from '../hooks/useInfiniteScroll';
import { useStickyGroupAnchor } from '../hooks/useStickyGroupAnchor';
import { useVirtualRows } from '../hooks/useVirtualRows';
-import {
- countLeafRows,
- getClientHiddenLeafRowCount,
- hasActiveTableFiltering
-} from '../utils';
+import { FilterSummary } from './clear-filters';
function formatGridWidth(width: string | number | undefined) {
if (width === undefined) return '1fr';
@@ -60,8 +53,6 @@ export function DataViewList({
loadingRowCount = 3,
loadMoreData,
tableQuery,
- totalRowCount,
- updateTableQuery,
activeView,
registerFieldsForView,
hasData
@@ -188,22 +179,6 @@ export function DataViewList({
onLoadMore: loadMoreData
});
- const hiddenLeafRowCount =
- mode === 'client'
- ? getClientHiddenLeafRowCount(table)
- : totalRowCount !== undefined
- ? Math.max(0, totalRowCount - countLeafRows(rows))
- : null;
- const hasActiveFiltering = !isLoading && hasActiveTableFiltering(table);
- const showFilterSummary =
- hasActiveFiltering &&
- (mode === 'server' ||
- (typeof hiddenLeafRowCount === 'number' && hiddenLeafRowCount > 0));
-
- const handleClearFilters = useCallback(() => {
- updateTableQuery(prev => ({ ...prev, filters: [], search: '' }));
- }, [updateTableQuery]);
-
// Sticky group anchor needs to recompute on scroll only. rAF-throttled so
// the binary search runs at most once per frame regardless of how fast the
// scroll events fire (mousewheel can dispatch dozens per frame on macOS).
@@ -507,37 +482,7 @@ export function DataViewList({
aria-hidden='true'
/>
- {showFilterSummary ? (
-
- {mode === 'server' && hiddenLeafRowCount === null ? (
-
- Some items might be hidden by filters
-
- ) : (
-
-
- {hiddenLeafRowCount}
-
-
- items hidden by filters
-
-
- )}
- }
- onClick={handleClearFilters}
- >
- Clear Filters
-
-
- ) : null}
+
);
}
diff --git a/packages/raystack/components/data-view/data-view.module.css b/packages/raystack/components/data-view/data-view.module.css
index f053083d7..b61bbe31f 100644
--- a/packages/raystack/components/data-view/data-view.module.css
+++ b/packages/raystack/components/data-view/data-view.module.css
@@ -75,6 +75,17 @@
box-sizing: border-box;
}
+/* Empty-state variant of the footer: the same row hugs its content and gains a
+ border so it reads as a compact panel instead of a full-width bar. */
+.filterSummaryFooterEmpty {
+ width: fit-content;
+ gap: var(--rs-space-3);
+ margin: var(--rs-space-6) auto var(--rs-space-9);
+ padding: var(--rs-space-3) var(--rs-space-4);
+ border: 0.5px solid var(--rs-color-border-base-primary);
+ border-radius: var(--rs-radius-2);
+}
+
.filterSummaryCount {
color: var(--rs-color-foreground-base-primary);
font-family: var(--rs-font-body);
diff --git a/packages/raystack/components/data-view/data-view.tsx b/packages/raystack/components/data-view/data-view.tsx
index 74ea565c2..01f9aa075 100644
--- a/packages/raystack/components/data-view/data-view.tsx
+++ b/packages/raystack/components/data-view/data-view.tsx
@@ -11,6 +11,7 @@ import {
} from '@tanstack/react-table';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { DataViewClearFilters } from './components/clear-filters';
import { DataViewCustom } from './components/custom';
import { DisplayAccess } from './components/display-access';
import { DisplayControls } from './components/display-controls';
@@ -308,6 +309,7 @@ export const DataView = Object.assign(DataViewRoot, {
DisplayAccess: DisplayAccess,
EmptyState: DataViewEmptyState,
ZeroState: DataViewZeroState,
+ ClearFilters: DataViewClearFilters,
Toolbar: Toolbar,
Search: DataViewSearch,
Filters: Filters,
diff --git a/packages/raystack/components/data-view/index.ts b/packages/raystack/components/data-view/index.ts
index ca3466e6d..08bc40dc7 100644
--- a/packages/raystack/components/data-view/index.ts
+++ b/packages/raystack/components/data-view/index.ts
@@ -1,5 +1,6 @@
export { EmptyFilterValue } from '~/types/filters';
+export type { DataViewClearFiltersProps } from './components/clear-filters';
export type { DataViewCustomProps } from './components/custom';
export type { DataViewDisplayAccessProps } from './components/display-access';
export type { DataViewEmptyStateProps } from './components/empty-state';