DEV Community

Cover image for Mantine List View Table - From Table to Finder
Giovambattista Fazioli
Giovambattista Fazioli

Posted on

Mantine List View Table - From Table to Finder

Row selection, keyboard navigation, context menus, column visibility, dual resize modes, and 6 exported hooks — one release, zero compromises.

Introduction

Picture the macOS Finder: you click a file, Shift+click to select a range, right-click for a context menu, double-click a column divider to auto-fit, and hide columns you don't need. Now picture doing all of that in a React table component — with full Mantine integration and zero external dependencies. That's what @gfazioli/mantine-list-view-table 2.0.0 delivers. This isn't an incremental update; it's a rewrite that transforms a sortable table into a complete Finder-style list view, with every piece of logic extracted into reusable, publicly exported hooks.

What's New

✨ Row Selection

Select rows exactly like macOS Finder. Single click selects one row, Cmd/Ctrl+Click toggles, Shift+Click selects a range. Both single and multiple modes are supported, with controlled and uncontrolled state.

<ListViewTable
  columns={columns}
  data={data}
  rowKey="id"
  selectionMode="multiple"
  onSelectionChange={(keys, records) => setSelected(keys)}
  selectedRowColor="blue"
/>
Enter fullscreen mode Exit fullscreen mode

Selected rows get a visible highlight that desaturates when the component loses focus — matching Finder's behavior pixel for pixel.

⌨️ Keyboard Navigation

When selectionMode is set, keyboard navigation activates automatically:

Key Action
Arrow Up/Down Move focus between rows
Enter Activate row (onRowActivate)
Space Toggle selection
Home / End Jump to first / last row
Cmd/Ctrl+A Select all (multiple mode)
<ListViewTable
  selectionMode="multiple"
  enableKeyboardNavigation
  onRowActivate={(record) => openFile(record)}
/>
Enter fullscreen mode Exit fullscreen mode

🖱️ Context Menu

Right-click any row to show a context menu powered by Mantine's Menu component. The clicked row is automatically selected before the menu appears. You get Mantine's full accessibility, keyboard support, and dark mode out of the box.

<ListViewTable
  selectionMode="single"
  renderContextMenu={({ record }) => (
    <>
      <Menu.Item leftSection={<IconCopy size={14} />}>Copy</Menu.Item>
      <Menu.Item leftSection={<IconDownload size={14} />}>Download</Menu.Item>
      <Menu.Divider />
      <Menu.Item color="red" leftSection={<IconTrash size={14} />}>Delete</Menu.Item>
    </>
  )}
/>
Enter fullscreen mode Exit fullscreen mode

👁️ Column Visibility

Hide and show columns programmatically or let users toggle them by right-clicking the table header. Supports both controlled and uncontrolled modes.

<ListViewTable
  hiddenColumns={['size', 'modified']}
  onHiddenColumnsChange={setHiddenColumns}
  enableColumnVisibilityToggle
/>
Enter fullscreen mode Exit fullscreen mode

↔️ Dual Resize Modes

The new resizeMode prop gives you two column resize behaviors:

  • standard (default) — width is traded between the dragged column and its right neighbor. Total table width stays fixed. Great for fixed-width layouts.
  • finder — only the dragged column changes width. The table grows freely, just like Finder. Pair with Table.ScrollContainer for horizontal scrolling.
<ListViewTable enableColumnResizing resizeMode="finder" />
Enter fullscreen mode Exit fullscreen mode

🎯 Double-Click Auto-Fit

Double-click any resize handle to auto-fit the column to its content — measured accurately using off-screen DOM clones. In standard mode, the adjacent column compensates to keep the total width constant.

🪝 6 Exported Hooks

Every piece of internal logic is now a standalone, reusable hook:

Hook Purpose
useSorting Sort state with controlled/uncontrolled modes
useColumnReorder Drag-and-drop column reordering
useColumnResize Column resize with standard/finder modes
useRowSelection Row selection (single, multiple, range)
useKeyboardNavigation Arrow/Enter/Space keyboard navigation
useColumnVisibility Column show/hide management

Import them directly for advanced compositions:

import { useRowSelection, useKeyboardNavigation } from '@gfazioli/mantine-list-view-table';
Enter fullscreen mode Exit fullscreen mode

📋 New Props Summary

Prop Type Default Description
selectionMode `'single' \ 'multiple'`
selectedRows React.Key[] Controlled selected row keys
defaultSelectedRows React.Key[] Default selected keys (uncontrolled)
onSelectionChange (keys, records) => void Selection change callback
selectedRowColor MantineColor Selected row background color
enableKeyboardNavigation boolean true when selectionMode is set Enable keyboard nav
onRowActivate (record, index) => void Enter key callback
renderContextMenu (info) => ReactNode Context menu content (use Menu.Item)
onRowContextMenu (record, index, event) => void Right-click callback
hiddenColumns string[] Controlled hidden column keys
defaultHiddenColumns string[] Default hidden columns (uncontrolled)
onHiddenColumnsChange (keys) => void Hidden columns change callback
enableColumnVisibilityToggle boolean false Right-click header to toggle columns
resizeMode `'standard' \ 'finder'` 'standard'
tableProps Partial<TableProps> Props passed to inner <Table>
stickyHeader boolean false Sticky table header
stickyHeaderOffset `number \ string` 0
tabularNums boolean false Tabular number alignment

🎨 New Styles API

New selectors: selectedRow, focusedRow

New CSS variables:

Variable Description
--list-view-selected-row-color Background color of selected rows
--list-view-sticky-blur Backdrop blur for sticky column overlay

💥 Breaking Changes

[!CAUTION]
This release contains breaking changes. Review the migration guide below before upgrading.

1. Props interface no longer extends TableProps

Table-level props (variant, layout, etc.) must now be passed through tableProps:

// Before
<ListViewTable variant="vertical" />

// After
<ListViewTable tableProps={{ variant: 'vertical' }} />
Enter fullscreen mode Exit fullscreen mode

2. enableColumnReordering and enableColumnResizing default to false

Previously both defaulted to true. Now you must opt in:

<ListViewTable enableColumnReordering enableColumnResizing />
Enter fullscreen mode Exit fullscreen mode

3. Component ref type changed

// Before
const ref = useRef<HTMLTableElement>(null);

// After
const ref = useRef<HTMLDivElement>(null);
Enter fullscreen mode Exit fullscreen mode

4. Context menu requires Menu.Item elements

The renderContextMenu prop must now return Mantine Menu.Item / Menu.Divider components instead of arbitrary JSX:

// Before
renderContextMenu={() => (
  <Stack gap={0}>
    <UnstyledButton>Copy</UnstyledButton>
  </Stack>
)}

// After
renderContextMenu={() => (
  <>
    <Menu.Item leftSection={<IconCopy size={14} />}>Copy</Menu.Item>
  </>
)}
Enter fullscreen mode Exit fullscreen mode

5. visibleMediaQuery removed from column config

Use the new hiddenColumns prop instead for programmatic column visibility.

6. contextMenu Styles API selector removed

The context menu is now powered by Mantine's Menu, which has its own styling system.

🐛 Bug Fixes

  • Double-click no longer triggers drag resizeevent.detail >= 2 guard prevents mousedown from starting a drag during double-click
  • Auto-fit preserves table width — adjacent column compensates the exact delta in standard mode
  • Controlled sort respects external data order — no internal re-sorting when sortStatus + onSort are both provided
  • Keyboard navigation bounds check — Enter/Space no longer fire with invalid indices after data filtering
  • Range selection clamped — Shift+click clamps to [0, data.length - 1] when data changes between clicks

🔧 Improvements

  • Resize handle UX overhaul — 14px hit area (20px touch), animated indicator line with spring curve
  • Finder-style header focus — inset bottom border instead of background highlight
  • Accessible focus indicator:focus-visible outline on root element for keyboard users
  • Sticky columns without !important — cleaner CSS specificity
  • Vertical variant — now supports dot-notation keys (getNestedValue) and renderCell

Migration Guide

Step 1: Install the update

yarn add @gfazioli/mantine-list-view-table@^2.0.0
Enter fullscreen mode Exit fullscreen mode

Step 2: Wrap Table props in tableProps

If you passed Mantine Table props directly, move them:

// Before
<ListViewTable variant="vertical" layout="fixed" />

// After
<ListViewTable tableProps={{ variant: 'vertical', layout: 'fixed' }} />
Enter fullscreen mode Exit fullscreen mode

Step 3: Opt in to reordering/resizing

<ListViewTable enableColumnReordering enableColumnResizing />
Enter fullscreen mode Exit fullscreen mode

Step 4: Update ref type

const ref = useRef<HTMLDivElement>(null); // was HTMLTableElement
Enter fullscreen mode Exit fullscreen mode

Step 5: Update context menu

Replace custom JSX with Menu.Item from @mantine/core:

import { Menu } from '@mantine/core';

renderContextMenu={({ record }) => (
  <>
    <Menu.Item leftSection={<IconCopy size={14} />}>Copy</Menu.Item>
    <Menu.Divider />
    <Menu.Item color="red" leftSection={<IconTrash size={14} />}>Delete</Menu.Item>
  </>
)}
Enter fullscreen mode Exit fullscreen mode

Step 6: Replace visibleMediaQuery

// Before (on column)
{ key: 'size', visibleMediaQuery: '(min-width: 768px)' }

// After (on component)
<ListViewTable hiddenColumns={isMobile ? ['size'] : []} />
Enter fullscreen mode Exit fullscreen mode

Compatibility

  • Mantine: 8.x
  • React: 18.x / 19.x
  • @tabler/icons-react: ^3.34.0
  • License: MIT

Links

Top comments (0)