DEV Community

Cover image for Selection Through the Looking Glass: A 3D Stack Component for Mantine
Giovambattista Fazioli
Giovambattista Fazioli

Posted on

Selection Through the Looking Glass: A 3D Stack Component for Mantine

A Mantine component that turns flat item selection into a spatial, 3D card-stack browsing experience — with keyboard, wheel, touch, and click navigation built in.

Introduction

Remember the first time you used macOS Time Machine? That moment when your desktop peeled away into an infinite corridor of snapshots, and navigating through time suddenly felt physical. That spatial metaphor stuck with us. mantine-depth-select brings that same sense of depth to any selection UI in your React application — pricing plans, document versions, onboarding steps, or anything else where browsing through stacked content beats scrolling a flat list. It's a single component with zero extra dependencies, built entirely on Mantine 8's Styles API.

mantine Depth Select

What is DepthSelect?

DepthSelect renders an array of items as a 3D stack of cards. The frontmost card is the active selection; cards behind it recede with progressive scale, vertical offset, opacity, and blur — all configurable per-level. When the user navigates (via keyboard, mouse wheel, trackpad swipe, click, or arrow controls), cards animate smoothly in and out of the stack with CSS transitions.

The component follows Mantine's compound component pattern. Built-in arrow controls appear by default, but you can hide them and wire up your own UI through controlled value/onChange. It supports both string and numeric values, loop mode, and respects prefers-reduced-motion.

✨ Key Features

3D Perspective Stack

Each card in the stack receives progressive CSS transforms based on its depth. Four props control the effect:

<DepthSelect
  data={items}
  scaleStep={0.1}       // scale reduction per level (default)
  translateYStep={30}    // vertical offset in px per level
  opacityStep={0.15}     // opacity reduction per level
  blurStep={1}           // blur in px per level
  w={400}
  h={300}
/>
Enter fullscreen mode Exit fullscreen mode

Cards that exit the stack (when navigating forward) animate toward the viewer with a scale-up and fade-out. Cards entering from behind animate in from the deepest visible position.

Multi-Input Navigation

Every common input method works out of the box:

  • Keyboard: ArrowUp/ArrowDown to step through, Home/End to jump
  • Mouse wheel / Trackpad: Scroll over the component to navigate (page scroll is blocked automatically). Disable with withScrollNavigation={false}
  • Touch: Vertical swipe gestures for mobile
  • Click: Click the second card (depth 1) to advance
  • Controls: Built-in arrow buttons with optional label

Built-in Controls with controlsProps

The default controls sit to the right of the stack. You can reposition them, add a label, set a fixed width, change alignment, or swap the icons — all through a single controlsProps object:

<DepthSelect
  data={items}
  controlsPosition="left"
  controlsProps={{
    w: 100,
    justify: 'end',
    labelFormatter: (item) => `Step ${item.value}`,
    upIcon: <IconChevronUp size={16} />,
    downIcon: <IconChevronDown size={16} />,
  }}
  w={400}
  h={300}
/>
Enter fullscreen mode Exit fullscreen mode

Compound Component Pattern

Need full layout control? Set withControls={false} and use DepthSelect.Controls as a child:

<DepthSelect data={items} withControls={false} w={400} h={300}>
  <DepthSelect.Controls labelFormatter={(item) => String(item.value)} />
</DepthSelect>
Enter fullscreen mode Exit fullscreen mode

Or skip the built-in controls entirely and build your own navigation with value and onChange.

Loop Mode

Enable loop to let navigation wrap from the last item back to the first (and vice versa). The arrow controls never show a disabled state in loop mode:

<DepthSelect data={items} loop w={400} h={200} />
Enter fullscreen mode Exit fullscreen mode

🚀 Getting Started

npm install @gfazioli/mantine-depth-select
Enter fullscreen mode Exit fullscreen mode

Import the styles at the root of your application:

import '@gfazioli/mantine-depth-select/styles.css';
Enter fullscreen mode Exit fullscreen mode

Minimal example:

import { Card, Text } from '@mantine/core';
import { DepthSelect, DepthSelectItem } from '@gfazioli/mantine-depth-select';

const data: DepthSelectItem[] = [
  { value: 'first', view: <Card p="lg" withBorder h="100%"><Text>First item</Text></Card> },
  { value: 'second', view: <Card p="lg" withBorder h="100%"><Text>Second item</Text></Card> },
  { value: 'third', view: <Card p="lg" withBorder h="100%"><Text>Third item</Text></Card> },
];

function Demo() {
  return <DepthSelect data={data} w={400} h={200} />;
}
Enter fullscreen mode Exit fullscreen mode

The w and h props define the area where cards live. Cards should use h="100%" to fill the available height.

Use Cases

Props & API

DepthSelect

  • dataDepthSelectItem[] — Array of { value, view } objects. value can be string or number, view is any ReactNode
  • value / defaultValue / onChange — Standard controlled/uncontrolled pattern
  • w / h — Dimensions of the card area (passed to the inner stack container)
  • visibleCards — Number of cards visible in the stack (default: 4)
  • withControls — Show built-in arrow controls (default: true)
  • controlsPosition"right" (default) or "left"
  • controlsProps — All DepthSelectControlsProps passed to the built-in controls
  • loop — Enable wrap-around navigation (default: false)
  • withScrollNavigation — Enable mouse wheel / trackpad navigation (default: true)
  • transitionDuration — Animation duration in ms (default: 400)
  • scaleStep / translateYStep / opacityStep / blurStep — Per-level depth effect parameters

DepthSelect.Controls

  • labelFormatter(item: DepthSelectItem) => ReactNode — Custom label between arrows
  • upIcon / downIcon — Custom icons for the arrow buttons
  • justify — Vertical alignment: "start", "center" (default), "end"
  • Plus all Mantine BoxProps (w, h, style, etc.)

🎨 Styles API

Selectors: root, stack, card, controls, controlUp, controlDown, controlLabel

Data attributes:

  • data-active — Frontmost (selected) card
  • data-depth="N" — Card depth level (0 = front)
  • data-exited — Card that has animated out
  • data-disabled — Disabled control button
  • data-controls-position"right" or "left" on root

CSS variables: --ds-transition-duration, --ds-scale-step, --ds-translate-y-step, --ds-opacity-step, --ds-blur-step, --ds-visible-cards

⚡ Performance

  • Render window: Only activeIndex - 1 through activeIndex + visibleCards + 1 are in the DOM. A dataset of 100 items renders ~6 nodes
  • Memoized styles: Card CSS is computed once per navigation change via useMemo
  • Targeted will-change: Only visible cards get GPU layer promotion; hidden cards use will-change: auto
  • Non-passive wheel listener: Blocks page scroll without layout thrashing
  • prefers-reduced-motion: Transitions are disabled automatically for users who prefer reduced motion

Live Demo & Documentation

Explore interactive demos — including a configurator with live props, rich card examples, pricing plan selector, version history browser, onboarding wizard, and more:

gfazioli.github.io/mantine-depth-select

Links

Top comments (0)