Mantine Lens Select

Logo

@gfazioli/mantine-lens-select

A fisheye/lens magnification select component for React applications built with Mantine. Displays items with a macOS Dock-like magnification effect on hover, supporting horizontal and vertical orientations.

Installation

yarn add @gfazioli/mantine-lens-select

After installation import package styles at the root of your application:

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

You can import styles within a layer @layer mantine-lens-select by importing @gfazioli/mantine-lens-select/styles.layer.css file.

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

Usage

The LensSelect component displays a list of items with a fisheye/lens magnification effect on hover — similar to the macOS Dock. Items near the cursor scale up, creating an interactive selection experience.

When no view is provided in data items, the component renders default pills (rounded vertical bars). You can customize their appearance with pillWidth, pillHeight, pillRadius, pillColor, hoverColor, and activeColor props.

Count
Variant
Selection mode
Orientation
Magnification
Lens range
Item size
Gap
Pill height
Pill width
Pill radius
Pill color
Hover color
Active color
Transition duration
import { LensSelect } from '@gfazioli/mantine-lens-select';

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

Count mode

For simple numeric selection, use the count prop instead of data. This generates pills automatically with numeric values — no array needed.

Combine with min and max for a custom range, or use step for fixed increments (similar to Mantine's Slider API):

// Simple: 15 pills with values 1..15
<LensSelect count={15} />

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

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

Use precision to control decimal places for generated values.

Simple count (1..15): 1

Range 0–100 (20 pills): 50

Step 10 (0, 10, 20, ..., 100): 0

import { useState } from 'react';
import { LensSelect } from '@gfazioli/mantine-lens-select';
import { Stack, Text } from '@mantine/core';

function Demo() {
  const [simple, setSimple] = useState<string | number>(1);
  const [range, setRange] = useState<string | number>(50);
  const [stepped, setStepped] = useState<string | number>(0);

  return (
    <Stack gap={48}>
      <Stack gap={32} align="center">
        <Text size="sm" fw={500}>Simple count (1..15): {String(simple)}</Text>
        <LensSelect count={15} value={simple} onChange={setSimple} withIndicator />
      </Stack>

      <Stack gap={32} align="center">
        <Text size="sm" fw={500}>Range 0–100 (20 pills): {String(range)}</Text>
        <LensSelect count={20} min={0} max={100} value={range} onChange={setRange} withIndicator />
      </Stack>

      <Stack gap={32} align="center">
        <Text size="sm" fw={500}>Step 10 (0, 10, 20, ..., 100): {String(stepped)}</Text>
        <LensSelect min={0} max={100} step={10} value={stepped} onChange={setStepped} withIndicator />
      </Stack>
    </Stack>
  );
}

Controlled

The LensSelect component supports both controlled and uncontrolled modes via value/defaultValue and onChange. Use the slider below to drive the selection externally:

Selected: 6

import { useState } from 'react';
import { LensSelect } from '@gfazioli/mantine-lens-select';
import { Slider, Stack, Text } from '@mantine/core';

const data = Array.from({ length: 12 }, (_, i) => ({ value: i + 1 }));

function Demo() {
  const [value, setValue] = useState<string | number>(6);

  return (
    <Stack align="center" gap="xl" pt={80}>
      <LensSelect data={data} value={value} onChange={setValue} withIndicator />
      <Text size="sm">Selected: {String(value)}</Text>
      <Slider
        w={300}
        min={1}
        max={12}
        step={1}
        value={Number(value)}
        onChange={setValue}
        label={(v) => `Item ${v}`}
      />
    </Stack>
  );
}

Variants

Both LensSelect and LensSelect.Indicator support default and outline variants. The outline variant renders pills and indicator with borders instead of filled backgrounds. You can mix variants independently:

Default variant

Outline variant

Outline variant with outline indicator

Default variant with outline indicator

import { LensSelect } from '@gfazioli/mantine-lens-select';
import { Stack, Text } from '@mantine/core';

const data = Array.from({ length: 10 }, (_, i) => ({ value: i + 1 }));

function Demo() {
  return (
    <Stack align="center" gap={60}>
      <Stack align="center" gap="xl">
        <Text size="sm" fw={500}>Default variant</Text>
        <LensSelect data={data} />
      </Stack>

      <Stack align="center" gap="xl">
        <Text size="sm" fw={500}>Outline variant</Text>
        <LensSelect data={data} variant="outline" />
      </Stack>

      <Stack align="center" gap="xl">
        <Text size="sm" fw={500}>Outline variant with outline indicator</Text>
        <LensSelect data={data} variant="outline" withIndicator={false}>
          <LensSelect.Indicator variant="outline" />
        </LensSelect>
      </Stack>

      <Stack align="center" gap="xl">
        <Text size="sm" fw={500}>Default variant with outline indicator</Text>
        <LensSelect data={data} withIndicator={false}>
          <LensSelect.Indicator variant="outline" />
        </LensSelect>
      </Stack>
    </Stack>
  );
}

LensSelect.Indicator

The indicator is a compound component that shows a dot below the selected item. It can be used automatically via withIndicator prop or explicitly as a child with its own props (color, size, offset, variant):

Variant
Color
Size
Offset
import { LensSelect } from '@gfazioli/mantine-lens-select';

const data = Array.from({ length: 12 }, (_, i) => ({ value: i + 1 }));

function Demo() {
  return (
    <LensSelect data={data} withIndicator={false}>
      <LensSelect.Indicator />
    </LensSelect>
  );
}

You can also pass indicator props from the parent via indicatorProps:

<LensSelect
  data={data}
  indicatorProps={{
    color: 'red',
    size: 10,
    offset: 20,
    variant: 'outline',
  }}
/>

Use Cases

macOS Dock

Use renderItem with custom icons and a Paper container to recreate the macOS Dock experience:

📁
🧭
📧
📷
🎵
📅
📝
⚙️
🖥️
🛒

Active: finder

import { useState } from 'react';
import { LensSelect } from '@gfazioli/mantine-lens-select';
import { Box, Paper, Stack, Text } from '@mantine/core';

const apps = [
  { value: 'finder', view: '📁' },
  { value: 'safari', view: '🧭' },
  { value: 'mail', view: '📧' },
  { value: 'photos', view: '📷' },
  { value: 'music', view: '🎵' },
  { value: 'calendar', view: '📅' },
  { value: 'notes', view: '📝' },
  { value: 'settings', view: '⚙️' },
  { value: 'terminal', view: '🖥️' },
  { value: 'appstore', view: '🛒' },
];

function Demo() {
  const [active, setActive] = useState<string | number>('finder');

  return (
    <Stack align="center" gap="md">
      <Paper
        px="lg"
        pt="md"
        pb="sm"
        radius="lg"
        withBorder
        style={{
          backgroundColor: 'light-dark(color-mix(in srgb, var(--mantine-color-gray-3) 60%, transparent), color-mix(in srgb, var(--mantine-color-gray-8) 20%, transparent))',
          borderColor: 'light-dark(var(--mantine-color-gray-4), var(--mantine-color-gray-7))',
          backdropFilter: 'blur(20px)',
        }}
      >
        <LensSelect
          data={apps}
          value={active}
          onChange={setActive}
          itemSize={48}
          gap={4}
          magnification={2.5}
          lensRange={3}
          expandOnHover
          transitionDuration={0}
          easing="linear"
          renderItem={(item, { active }) => (
            <Box
              style={{
                width: '100%',
                height: '100%',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                borderRadius: 10,
                background: active ? 'var(--mantine-color-blue-6)' : 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-gray-8))',
                fontSize: 24,
                lineHeight: 1,
                cursor: 'pointer',
              }}
            >
              {item.view}
            </Box>
          )}
          indicatorProps={{ size: 4, offset: 8 }}
        />
      </Paper>
      <Text size="xs" c="dimmed">Active: {String(active)}</Text>
    </Stack>
  );
}

Weekday Selector

Use data with custom view to build a day picker. Weekend days (Sat, Sun) are highlighted in red — both the label text and the active indicator color change dynamically based on the selected value:

Selected: Mon

Mon

Tue

Wed

Thu

Fri

Sat

Sun

import { useState } from 'react';
import { LensSelect, type LensSelectItem } from '@gfazioli/mantine-lens-select';
import { Stack, Text } from '@mantine/core';

const DAYS: LensSelectItem[] = [
  { value: 'Mon', view: <Text size="xs" fw={600}>Mon</Text> },
  { value: 'Tue', view: <Text size="xs" fw={600}>Tue</Text> },
  { value: 'Wed', view: <Text size="xs" fw={600}>Wed</Text> },
  { value: 'Thu', view: <Text size="xs" fw={600}>Thu</Text> },
  { value: 'Fri', view: <Text size="xs" fw={600}>Fri</Text> },
  { value: 'Sat', view: <Text size="xs" fw={600} c="red">Sat</Text> },
  { value: 'Sun', view: <Text size="xs" fw={600} c="red">Sun</Text> },
];

function Demo() {
  const [day, setDay] = useState<string | number>('Mon');
  const isWeekend = day === 'Sat' || day === 'Sun';

  return (
    <Stack align="center" gap={32}>
      <Text size="sm" fw={500} c={isWeekend ? 'red' : undefined}>
        Selected: {String(day)}
      </Text>
      <LensSelect
        data={DAYS}
        value={day}
        onChange={setDay}
        itemSize={36}
        magnification={1.8}
        lensRange={2}
        activeColor={isWeekend ? 'red' : 'blue'}
        indicatorProps={{ color: isWeekend ? 'red' : 'blue' }}
      />
    </Stack>
  );
}

Rating / Volume Selector

Use count for a quick numeric selector — no data array needed:

Volume: 5/10

import { useState } from 'react';
import { LensSelect } from '@gfazioli/mantine-lens-select';
import { Stack, Text } from '@mantine/core';

function Demo() {
  const [value, setValue] = useState<string | number>(5);

  return (
    <Stack align="center" gap="md">
      <Text size="sm" fw={500}>Volume: {String(value)}/10</Text>
      <LensSelect
        count={10}
        value={value}
        onChange={setValue}
        itemSize={32}
        gap={3}
        pillWidth={6}
        magnification={1.5}
        lensRange={2}
        activeColor="teal"
        hoverColor="teal"
        indicatorProps={{ color: 'teal', size: 4 }}
      />
    </Stack>
  );
}

Vertical Timeline

Use orientation="vertical" to create a timeline selector inspired by macOS Time Machine:

Dec 2025

Selected date in the timeline

import { useState } from 'react';
import { LensSelect } from '@gfazioli/mantine-lens-select';
import { Group, Stack, Text } from '@mantine/core';

const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const events = [2022, 2023, 2024, 2025].flatMap((year) =>
  months.map((month) => ({ value: `${month} ${year}` }))
);

function Demo() {
  const [date, setDate] = useState<string | number>('Dec 2025');

  return (
    <Group align="flex-start" gap="xl" justify="center">
      <LensSelect
        data={events}
        value={date}
        onChange={setDate}
        orientation="vertical"
        itemSize={12}
        gap={6}
        pillWidth={2}
        magnification={1.5}
        lensRange={2}
        activeColor="orange"
        hoverColor="orange"
        indicatorProps={{ color: 'orange', size: 5, offset: 12 }}
      />
      <Stack gap={4} pt={4}>
        <Text size="lg" fw={700}>{String(date)}</Text>
        <Text size="sm" c="dimmed">Selected date in the timeline</Text>
      </Stack>
    </Group>
  );
}

Styles API

LensSelect supports Styles API, you can add styles to any inner element of the component with classNames prop. Follow Styles API documentation to learn more.

Component Styles API

Hover over selectors to highlight corresponding elements

/*
 * Hover over selectors to apply outline styles
 *
 */