ZYVOPMulti-Platform Sync
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZYVOPMulti-Platform Sync

The Developer Publishing Hub. Write once, publish everywhere, and make your work citation-ready with built-in SEO, AEO, and GEO discovery support. Zero reader paywalls.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Developer API & CLI
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

ยฉ 2026 ZyVOP. Developer Publishing Hub.

Zero paywalls ยท Full content ownership
All systems operational
HomeMagnify Your Choices: A Fisheye Select Component for Mantine

Magnify Your Choices: A Fisheye Select Component for Mantine

Giovambattista Fazioli
Giovambattista Fazioli
Senior Full-stack Engineer, Lead DeveloperSupport
September 4, 2026
6 min read
Magnify Your Choices: A Fisheye Select Component for Mantine
#reactjsdevelopment#TypeScript#mantine#nextjs

A select component where items magnify under your cursor -- macOS Dock-style -- built for Mantine 9.

The Problem With Flat Lists

You are building a settings panel where users pick a value from a range -- say, a volume level from 1 to 10, or a date from a multi-year timeline. A traditional <select> dropdown works, but it is buried behind a click. A slider works, but it carries no visual weight. What if the selection itself could respond to the user's intent -- items growing as the cursor approaches, then settling back as it moves away?

That is exactly what @gfazioli/mantine-lens-select does. It brings the macOS Dock's fisheye magnification effect to a Mantine select component, making selection both functional and delightful.

What Is LensSelect?

LensSelect is a React component built on top of Mantine 9 that displays a row (or column) of items with a cosine-based lens magnification effect on hover. Items near the cursor scale up smoothly while distant items remain at their base size, creating an interactive and visually engaging selection experience.

Configurator

It ships as @gfazioli/mantine-lens-select v1.0.0, follows the full Mantine Factory pattern, and supports the Styles API out of the box.

Key Features

Pill Mode -- Zero Configuration

The simplest usage needs no data array at all. Just pass a count and you get styled pills with automatic numeric values:

import { LensSelect } from '@gfazioli/mantine-lens-select';

function Demo() {return <LensSelect count={20} />;}

Enter fullscreen mode Exit fullscreen mode

Twenty rounded vertical bars appear, each responding to your cursor with the fisheye effect. The active pill gets the primary color, hovered pills get a lighter tint, and a dot indicator tracks the selection.

Count Mode with Ranges

For numeric selection, combine count with min/max to generate evenly distributed values, or use step for fixed increments -- an API that will feel familiar if you have used Mantine's Slider:

// 20 pills mapped linearly from 0 to 100<LensSelect count={20} min={0} max={100} />

// Fixed steps: pills at 0, 10, 20, ..., 100<LensSelect min={0} max={100} step={10} />

// Decimal precision: 0.00, 0.25, 0.50, 0.75, 1.00<LensSelect min={0} max={1} step={0.25} precision={2} />

Enter fullscreen mode Exit fullscreen mode

The data prop always takes priority, so you can start with count mode during prototyping and switch to a custom data array later without changing anything else.

Custom Data with Rich Content

Pass an array of { value, view } objects for full control over what each item renders:

const apps = [{ value: 'finder', view: <span>๐Ÿ“</span> },{ value: 'safari', view: <span>๐Ÿงญ</span> },{ value: 'mail', view: <span>๐Ÿ“ง</span> },{ value: 'settings', view: <span>โš™๏ธ</span> },];

<LensSelect data={apps} itemSize={48} magnification={2.5} expandOnHover />

Enter fullscreen mode Exit fullscreen mode

When view is omitted from all items, the component automatically enters pill mode -- no special flag needed.

Orientation: Horizontal and Vertical

Switch between horizontal and vertical layouts with a single prop. Vertical mode is perfect for timeline selectors:

<LensSelectdata={events}orientation="vertical"itemSize={12}gap={6}pillWidth={2}activeColor="orange"/>

Enter fullscreen mode Exit fullscreen mode

Visual Effects Stack

Three independent visual effects can be combined:

  • Scale (withScale) -- the core fisheye magnification, enabled by default

  • Opacity (withOpacity) -- distant items fade, focused items stay opaque

  • Blur (withBlur) -- distant items blur, focused items stay sharp

Each effect has configurable ranges (opacityRange, blurRange) and all respect the same cosine-based falloff curve controlled by magnification and lensRange.

Expand on Hover

Enable expandOnHover to make items push their neighbors apart during magnification -- the signature macOS Dock behavior:

<LensSelectcount={15}itemSize={48}magnification={2.5}expandOnHovertransitionDuration={0}easing="linear"/>

Enter fullscreen mode Exit fullscreen mode

Selection Modes and Navigation

Two selection modes serve different use cases:

  • Click mode (default) -- user clicks to select

  • Hover mode -- selection follows the cursor automatically

On top of that, LensSelect supports:

  • Keyboard navigation -- Arrow keys, Home, End with proper orientation awareness

  • Mouse wheel -- scroll through items with withWheel

  • Loop -- wrap around from last to first (and vice versa) with loop

  • Touch -- swipe navigation on mobile devices

The Indicator: A Compound Component

LensSelect.Indicator is a compound sub-component that renders a tracking dot below (or beside, in vertical mode) the active item. It follows the item in real time using requestAnimationFrame, staying in sync even during magnification animations.

Use it automatically via the withIndicator prop, or mount it explicitly as a child for full control:

<LensSelect data={data} withIndicator={false}><LensSelect.Indicator
    variant="outline"
    color="red"
    size={10}
    offset={20}/></LensSelect>

Enter fullscreen mode Exit fullscreen mode

Both LensSelect and LensSelect.Indicator support default and outline variants independently, so you can mix filled pills with an outline indicator dot (or vice versa).

renderItem for Total Control

When pills and view are not enough, renderItem gives you a callback with the item, its active state, current scale factor, and hover status:

<LensSelectdata={apps}renderItem={(item, { active, scale, hovered }) => (
    <Box
      style={{
        borderRadius: 10,
        background: active ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-gray-1)',
        fontSize: 24,
      }}
    >
      {item.view}
    </Box>)}/>

Enter fullscreen mode Exit fullscreen mode

MacOS Dock

Getting Started

Install the package:

npm install @gfazioli/mantine-lens-select
# or
yarn add @gfazioli/mantine-lens-select

Enter fullscreen mode Exit fullscreen mode

Import styles at the root of your application:

import '@gfazioli/mantine-lens-select/styles.css';

Enter fullscreen mode Exit fullscreen mode

Layer-aware import is also available:

import '@gfazioli/mantine-lens-select/styles.layer.css';

Enter fullscreen mode Exit fullscreen mode

Then use it:

import { LensSelect } from '@gfazioli/mantine-lens-select';

function App() {const [value, setValue] = useState(5);

  return (
    <LensSelect
      count={10}
      value={value}
      onChange={setValue}
    />);}

Enter fullscreen mode Exit fullscreen mode

Props and API at a Glance

Core props:

Prop

Type

Default

Description

data

LensSelectItem[]

--

Array of { value, view? } items

count

number

--

Generate N pills with numeric values (no data needed)

min / max

number

--

Range bounds for generated values

step

number

--

Fixed increment between generated values

precision

number

0

Decimal places for generated values

value / defaultValue

`string \

number`

--

onChange

(value) => void

--

Selection change callback

orientation

`'horizontal' \

'vertical'`

'horizontal'

magnification

number

2

Maximum scale factor (200%)

lensRange

number

3

Number of adjacent items influenced

expandOnHover

boolean

false

Push neighbors apart (Dock behavior)

selectionMode

`'click' \

'hover'`

'click'

withWheel

boolean

false

Enable scroll wheel navigation

loop

boolean

false

Wrap-around navigation

variant

`'default' \

'outline'`

'default'

Visual effects: withScale, withOpacity, withBlur, opacityRange, blurRange

Pill styling: itemSize, gap, pillWidth, pillHeight, pillRadius, pillColor, hoverColor, activeColor

Transitions: transitionDuration, easing (linear, ease-out, ease-in-out, spring, or custom cubic-bezier)

Styles API: 6 selectors (root, track, item, itemContent, itemPill, indicator) and 12 CSS custom properties for full visual customization.

Advanced Usage

macOS Dock Recreation

Combine renderItem, expandOnHover, and a Paper container for a faithful Dock clone:

<Paper px="lg" pt="md" pb="sm" radius="lg" withBorderstyle={{ backdropFilter: 'blur(20px)' }}><LensSelect
    data={apps}
    itemSize={48}
    gap={4}
    magnification={2.5}
    lensRange={3}
    expandOnHover
    transitionDuration={0}
    easing="linear"
    renderItem={(item, { active }) => (
      <Box style={{
        borderRadius: 10,
        background: active
          ? 'var(--mantine-color-blue-6)'
          : 'var(--mantine-color-gray-1)',
        fontSize: 24,
      }}>
        {item.view}
      </Box>
    )}
    indicatorProps={{ size: 4, offset: 8 }}/></Paper>

Enter fullscreen mode Exit fullscreen mode

Weekday Picker with Dynamic Colors

Leverage controlled state to change colors based on the selected value:

function WeekdayPicker() {const [day, setDay] = useState('Mon');const isWeekend = day === 'Sat' || day === 'Sun';

  return (
    <LensSelect
      data={DAYS}
      value={day}
      onChange={setDay}
      itemSize={36}
      magnification={1.8}
      lensRange={2}
      activeColor={isWeekend ? 'red' : 'blue'}
      indicatorProps={{ color: isWeekend ? 'red' : 'blue' }}
    />);}

Enter fullscreen mode Exit fullscreen mode

Volume / Rating Selector

A compact numeric selector with count mode -- no data array required:

function VolumeControl() {const [volume, setVolume] = useState(5);

  return (
    <LensSelect
      count={10}
      value={volume}
      onChange={setVolume}
      itemSize={32}
      gap={3}
      pillWidth={6}
      magnification={1.5}
      lensRange={2}
      activeColor="teal"
      hoverColor="teal"
      indicatorProps={{ color: 'teal', size: 4 }}
    />);}

Enter fullscreen mode Exit fullscreen mode

Vertical Timeline

Use vertical orientation for a time-machine-style date navigator:

const events = [2022, 2023, 2024, 2025].flatMap((year) =>months.map((month) => ({ value: `${month} ${year}` })));

<LensSelectdata={events}orientation="vertical"itemSize={12}gap={6}pillWidth={2}magnification={1.5}lensRange={2}activeColor="orange"indicatorProps={{ color: 'orange', size: 5, offset: 12 }}/>

Enter fullscreen mode Exit fullscreen mode

Accessibility

LensSelect implements the WAI-ARIA listbox pattern:

  • role="listbox" on the root, role="option" on each item

  • aria-selected reflects the current selection

  • aria-orientation matches the layout direction

  • Full keyboard navigation: Arrow keys (orientation-aware), Home, End

  • prefers-reduced-motion media query disables all CSS transitions

  • Configurable ariaLabel prop

Under the Hood

A few implementation details worth noting:

  • Cosine-based falloff: the lens effect uses (1 + cos(pi * distance / maxRange)) / 2 for a smooth bell-curve that feels natural rather than linear

  • CSS-native responsive props: itemSize, gap, pillWidth, and pillHeight all support Mantine's StyleProp responsive syntax via InlineStyles + CSS media queries -- no useMatches re-renders

  • GPU-optimized: will-change is set only during active hover and removed on mouse leave

  • RAF-synced indicator: the dot position updates via requestAnimationFrame to stay in sync with CSS transitions

Live Demo

Try the interactive configurator and all use-case demos on the documentation site:

gfazioli.github.io/mantine-lens-select

Links

  • Documentation & Demos

  • npm package

  • GitHub repository

  • All Mantine Extensions

Comments (0)

Login to post a comment.

Giovambattista Fazioli
Giovambattista Fazioli

Senior Full-stack Engineer, Lead Developer

Italian Senior Full-stack Engineer and Lead Developer on the Cloud team at Namecheap. I build developer tools, React/Mantine UI components and native macOS apps โ€” mostly open source. I work across TypeScript, Next.js, Go and SwiftUI, and I've been coding since the Commodore/Assembly days. Creator of WP Bones and 25+ Mantine extensions, and maintainer of the Undolog open-source studio.

Support
Subscribe to Giovambattista Fazioli's Newsletter

More from Giovambattista Fazioli

View profile

FinderGit 0.29.0 โ€” open a commit like a folder

Thereโ€™s a moment, reading a commit list, where you want to know one small thing: which files did that one touch?

3 minSep 4

Netfox 0.18.0 โ€” is it my connection, or the internet?

There is a question that comes up every time something feels slow, and until now Netfox could not...

3 minSep 4

Mantine Book โ€” Grab Any Edge, Turn Any Page

A realistic iBooks-style book for React built on Mantine: stack two-sided pages and turn them by...

5 minSep 4

Netfox โ€” The macOS app that never loses track of your network

I built Netfox because every existing answer to "what's actually on my network?" annoyed me. The...

5 minSep 4

Audio That Sees Itself

A Mantine-native audio player for React with waveform visualisation and a live spectrum analyser,...

5 minSep 4