Mantine Picker

Logo

@gfazioli/mantine-picker

A Mantine component that allows you to create a picker effect with a list of elements.

Installation

yarn add @gfazioli/mantine-picker

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

import '@gfazioli/mantine-picker/styles.css';

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

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

Usage

The Picker component allows you to create a picker effect with any list of items. It's inspired by the iOS picker and allows you to create a similar effect with any list of items.

  • You can drag up and down to select the item you want
  • You may also use the mouse wheel to scroll through the items
  • Of course, you can also click on the item to select it
  • Finally, you can use the keyboard to navigate through the items
RomeMilanNaplesBerlinMadridBarcelona
Value: Rome
Rotate y
Wheel sensitivity
Momentum
Deceleration rate
Item height
Perspective
Max rotation
Cylinder radius
Visible items
Min item opacity
Min item scale
Max blur amount
Mask height
Mask intensity
import { useState } from 'react';
import { Picker, type PickerProps } from '@gfazioli/mantine-picker';
import { Code, Stack } from '@mantine/core';

function Demo(props: PickerProps) {
  const [value, setValue] = useState('Rome');

  return (
    <Stack align="center" justify="space-between" h={300}>
      <Picker
        value={value}
        data={[
          'Rome',
          'Milan',
          'Naples',
          'Berlin',
          'Madrid',
          'Barcelona',
          'Paris',
          'London',
          'New York',
          'Los Angeles',
        ]}
        onChange={setValue}
      />
      <Code>Value: {value}</Code>
    </Stack>
  );
}

onChange

You can use the onChange prop to get the selected item. It is called whenever the selected value changes, allowing you to handle the selection logic in your application. Of course, you can also use the value prop to control the selected item.

OctoberNovemberDecemberJanuaryFebruaryMarchAprilMayJuneJulyAugustSeptemberOctober
Value: April
import { useState } from 'react';
import { Picker } from '@gfazioli/mantine-picker';
import { Code, Select, Stack } from '@mantine/core';

function Demo() {
  const [value, setValue] = useState<string | null>(
    new Date().toLocaleString('en-US', { month: 'long' })
  );
  const data = [
    'January',
    'February',
    'March',
    'April',
    'May',
    'June',
    'July',
    'August',
    'September',
    'October',
    'November',
    'December',
  ];

  return (
    <Stack align="center" justify="space-between" h={400}>
      <Picker onChange={setValue} value={value} data={data} />
      <Code>Value: {value}</Code>
      <Select data={data} label="Select value" placeholder="Select value" onChange={setValue} />
    </Stack>
  );
}

renderItem

You can use the renderItem prop to customize the rendering of each item. This prop receives the item as arguments and should return the JSX to be rendered.

gray

violet

pink

brown

cyan

magenta

lime

olive

maroon

red

green

blue

yellow

orange

purple

black

white

gray

violet

gray
violet
pink
brown
cyan
magenta
lime
olive
maroon
red
green
blue
yellow
orange
purple
black
white
gray
violet
Value:
import { useState } from 'react';
import { Picker } from '@gfazioli/mantine-picker';
import { Badge, Code, ColorSwatch, Group, Stack, Text } from '@mantine/core';

function Demo(props: PickerProps) {
  const [value, setValue] = useState<string | number>('');

  const data = [
    'red',
    'green',
    'blue',
    'yellow',
    'orange',
    'purple',
    'black',
    'white',
    'gray',
    'violet',
    'pink',
    'brown',
    'cyan',
    'magenta',
    'lime',
    'olive',
    'maroon',
  ];

  function renderItem(item: string | number) {
    return (
      <Group>
        <ColorSwatch size={16} color={item as string} />
        <Text>{item}</Text>
      </Group>
    );
  }

  return (
    <Stack align="center" justify="space-between" h={400}>
      <Group>
        <Picker w={200} onChange={setValue} data={data} renderItem={renderItem} />
        <Picker
          w={200}
          onChange={setValue}
          data={data}
          renderItem={(item) => <Badge color={item as string}>{item}</Badge>}
        />
      </Group>
      <Code>Value: {value}</Code>
    </Stack>
  );
}

3D Effect

The Picker features a built-in 3D cylinder rotation effect inspired by the iOS picker. Configure it with:

  • enable3D — toggle the 3D effect (default: true)
  • perspective — depth perspective in pixels (default: 300)
  • maxRotation — maximum rotation angle in degrees (default: 60)
  • cylinderRadius — curvature factor (default: 4)
  • rotateY — rotate the entire picker along the Y-axis (default: 0)
JulyAugustSeptemberOctoberNovemberDecemberJanuaryFebruaryMarchAprilMayJuneJuly
JulyAugustSeptemberOctoberNovemberDecemberJanuaryFebruaryMarchAprilMayJuneJuly
JulyAugustSeptemberOctoberNovemberDecemberJanuaryFebruaryMarchAprilMayJuneJuly
import { Picker } from '@gfazioli/mantine-picker';
import { Group } from '@mantine/core';

function Demo() {
  return (
    <Group justify="center" gap={40}>
      <Picker data={months} w={120} rotateY={-15} perspective={200} cylinderRadius={3} />
      <Picker data={months} w={120} />
      <Picker data={months} w={120} rotateY={15} perspective={200} cylinderRadius={3} />
    </Group>
  );
}

Visual Effects

Non-selected items can be styled with gradual blur, opacity, and scale effects:

  • maxBlurAmount — maximum blur in pixels for non-selected items (default: 0)
  • minItemOpacity — minimum opacity at the edges (default: 0.3)
  • minItemScale — minimum scale at the edges (default: 0.85)

Blur

SolidPreactLitQwikReactAngularVueSvelteSolid

Opacity

SolidPreactLitQwikReactAngularVueSvelteSolid

Scale

SolidPreactLitQwikReactAngularVueSvelteSolid

Combined

SolidPreactLitQwikReactAngularVueSvelteSolid
import { Picker } from '@gfazioli/mantine-picker';
import { Group } from '@mantine/core';

function Demo() {
  return (
    <Group justify="center" gap={40}>
      {/* Blur effect on non-selected items */}
      <Picker data={data} w={100} maxBlurAmount={4} />

      {/* Opacity gradient */}
      <Picker data={data} w={100} minItemOpacity={0.1} />

      {/* Scale gradient */}
      <Picker data={data} w={100} minItemScale={0.5} />

      {/* All combined */}
      <Picker data={data} w={100} maxBlurAmount={3} minItemOpacity={0.2} minItemScale={0.6} />
    </Group>
  );
}

Mask

Enable a gradient mask at the top and bottom edges with withMask. Control the gradient intensity with maskHeight (percentage of picker height) and maskIntensity (gradient opacity).

Default (no mask)

Item 11Item 12Item 13Item 14Item 15Item 16Item 17Item 18Item 19Item 20Item 1Item 2Item 3Item 4Item 5Item 6Item 7Item 8Item 9Item 10Item 11

With mask

Item 11Item 12Item 13Item 14Item 15Item 16Item 17Item 18Item 19Item 20Item 1Item 2Item 3Item 4Item 5Item 6Item 7Item 8Item 9Item 10Item 11

Intense mask

Item 11Item 12Item 13Item 14Item 15Item 16Item 17Item 18Item 19Item 20Item 1Item 2Item 3Item 4Item 5Item 6Item 7Item 8Item 9Item 10Item 11
import { Picker } from '@gfazioli/mantine-picker';
import { Group } from '@mantine/core';

function Demo() {
  return (
    <Group justify="center" gap={40}>
      {/* No mask (default) */}
      <Picker data={data} w={120} visibleItems={5} />

      {/* With gradient mask */}
      <Picker data={data} w={120} visibleItems={5} withMask maskHeight={45} maskIntensity={40} />

      {/* Intense mask */}
      <Picker data={data} w={120} visibleItems={5} withMask maskHeight={60} maskIntensity={70} />
    </Group>
  );
}

Left and Right Sections

You can use the leftSection and rightSection props to add custom content to the left and right sides of the picker. This is useful for adding icons, buttons, or any other content you want.

👉
56789100123456
👈
56789100123456
56789100123456
import { Picker } from '@gfazioli/mantine-picker';
import { Group, Stack } from '@mantine/core';

function Demo(props: PickerProps) {
  const data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  return (
    <Stack>
      <Group justify="space-between">
        <Picker w={200} data={data} leftSection="👉" />
        <Picker w={200} data={data} rightSection="👈" />
        <Picker w={200} data={data} leftSection="↑" rightSection="↓" />
      </Group>
    </Stack>
  );
}

As you can see, by using leftSection and rightSection props you can add any content you want to the left and right sides of the picker. In these cases the sections are fixed and do not scroll with the items. They will also inside the picker.

Alternatively, you can use a simple <Group/> component to add any left (center) and right section you want.

👉

56789100123456

👈

56789100123456

:

56789100123456
import { Picker } from '@gfazioli/mantine-picker';
import { Group, Stack, Text } from '@mantine/core';

function Demo(props: PickerProps) {
  const data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  return (
    <Stack>
      <Group justify="space-between">
        <Group>
          <Text>👉</Text>
          <Picker w={200} data={data} />
          <Text>👈</Text>
        </Group>
        <Group>
          <Picker w={100} data={data} />
          <Text>:</Text>
          <Picker w={100} data={data} />
        </Group>
      </Group>
    </Stack>
  );
}

Styles the Picker

You can change the default styles of the items by using the same props you use in the <Text/> component.

PhiladelphiaSan AntonioSan DiegoDallasSan JoseNew YorkLos AngelesChicagoHoustonPhoenixPhiladelphia
PhiladelphiaSan AntonioSan DiegoDallasSan JoseNew YorkLos AngelesChicagoHoustonPhoenixPhiladelphia
PhiladelphiaSan AntonioSan DiegoDallasSan JoseNew YorkLos AngelesChicagoHoustonPhoenixPhiladelphia
import { Picker } from '@gfazioli/mantine-picker';
import { Group, Stack } from '@mantine/core';

function Demo() {
  const data = [
    'New York',
    'Los Angeles',
    'Chicago',
    'Houston',
    'Phoenix',
    'Philadelphia',
    'San Antonio',
    'San Diego',
    'Dallas',
    'San Jose',
  ];

  return (
    <Stack>
      <Group justify="space-between">
        <Picker w={200} data={data} tt="uppercase" />
        <Picker w={200} data={data} size="xl" />
        <Picker
          w={200}
          data={data}
          variant="gradient"
          gradient={{ from: 'blue', to: 'red', deg: 90 }}
          style={{
            textShadow: '0 4px 2px rgba(0, 0, 0, 0.4)',
          }}
        />
      </Group>
    </Stack>
  );
}

readOnly

In addition to the disabled prop, you can use the readOnly prop to make the picker read-only. This will prevent the user from changing the selected item, but will still allow you to change it programmatically. It can be useful when you want to display the selected item without allowing the user to change it.

56789012345
012345
import { useState } from 'react';
import { Picker } from '@gfazioli/mantine-picker';
import { Button, Group, Stack } from '@mantine/core';
import { useInterval } from '@mantine/hooks';

function Demo() {
  const length = 10;
  const data = Array.from({ length }, (_, i) => i);

  const [seconds, setSeconds] = useState(0);
  const interval = useInterval(() => {
    setSeconds((s) => (s === length - 1 ? 0 : s + 1));
  }, 1000);

  const pickerProps: PickerProps = {
    w: 60,
    withDividers: false,
    withHighlight: false,
    withMask: false,
    itemHeight: 24,
    readOnly: true,
    data,
  };

  return (
    <Stack align="center" justify="space-between" h={200}>
      <Group>
        <Picker
          {...pickerProps}
          maxBlurAmount={1.5}
          minItemOpacity={0.5}
          visibleItems={3}
          value={seconds}
        />
        <Picker
          {...pickerProps}
          loop={false}
          c="red"
          minItemScale={0.1}
          visibleItems={1}
          animationDuration={700}
          value={seconds}
        />
      </Group>

      <Button onClick={interval.toggle}>{interval.active ? 'Stop' : 'Start'}</Button>
    </Stack>
  );
}

Accessibility

The Picker component provides built-in keyboard navigation and screen reader support:

  • Arrow Up/Down — move one item up/down
  • Home/End — jump to first/last item
  • Page Up/Down — move 5 items at a time
  • role="listbox" and role="option" for semantic structure
  • aria-selected on the active item
  • aria-disabled and aria-readonly when applicable

Use the id, label, description, and keyboardHint props to provide additional context for assistive technologies. Set focusable={false} to exclude the picker from the tab order.

Example: Time Picker

The Picker component can be used as a Time Picker. You can use the renderItem prop to customize the rendering of each item and the onChange prop to handle the selection logic. In this example, we will create a simple time picker that allows you to select the hour and minute.

🕑

02030405060708091011121314151617181920212223000102

:

29303132333435363738394041424344454647484950515253545556575859000102030405060708091011121314151617181920212223242526272829

min

Selected time: 14:8

08091011000102030405060708

:

29303132333435363738394041424344454647484950515253545556575859000102030405060708091011121314151617181920212223242526272829
ampm

Selected time: 02:8 pm

import { useState } from 'react';
import { Picker } from '@gfazioli/mantine-picker';
import { Group, Stack, Text } from '@mantine/core';

function Demo() {
  const initialHour12 = +(new Date().getHours() as number) % 12;

  const [hours24, setHours24] = useState(new Date().getHours().toString());
  const [hours12, setHours12] = useState(
    initialHour12 < 10 ? `0${initialHour12}` : `${initialHour12}`
  );
  const [minutes24, setMinutes24] = useState(new Date().getMinutes());
  const [minutes12, setMinutes12] = useState(new Date().getMinutes());
  const [amPm, setAmPm] = useState(new Date().getHours() >= 12 ? 'pm' : 'am');

  const hours24Data = Array.from({ length: 24 }, (_, i) => (i < 10 ? `0${i}` : `${i}`));
  const hours12Data = Array.from({ length: 12 }, (_, i) => (i < 10 ? `0${i}` : `${i}`));
  const minutesData = Array.from({ length: 60 }, (_, i) => (i < 10 ? `0${i}` : i));

  const pickerProps: Omit<PickerProps, 'data'> = {
    w: 50,
    withDividers: false,
    withHighlight: false,
    loop: true,
    maxRotation: 90,
    itemHeight: 30,
    visibleItems: 5,
    withMask: false,
  };

  return (
    <Group justify="center" grow>
      <Stack align="center" justify="space-between" h={200}>
        <Group gap={0}>
          <Text>🕑</Text>
          <Picker
            {...pickerProps}
            rotateY={-10}
            data={hours24Data}
            value={hours24}
            onChange={setHours24}
          />
          <Text>:</Text>
          <Picker
            {...pickerProps}
            rotateY={10}
            data={minutesData}
            value={minutes24}
            onChange={setMinutes24}
          />
          <Text>min</Text>
        </Group>
        <Text>Selected time: {`${hours24}:${minutes24}`}</Text>
      </Stack>

      <Stack align="center" justify="space-between" h={200}>
        <Group gap={0}>
          <Picker
            {...pickerProps}
            rotateY={-10}
            data={hours12Data}
            value={hours12}
            onChange={setHours12}
          />
          <Text>:</Text>
          <Picker
            {...pickerProps}
            rotateY={10}
            data={minutesData}
            value={minutes12}
            onChange={setMinutes12}
          />
          <Picker
            {...pickerProps}
            rotateY={10}
            data={['am', 'pm']}
            loop={false}
            value={amPm}
            onChange={setAmPm}
          />
        </Group>
        <Text>Selected time: {`${hours12}:${minutes12} ${amPm}`}</Text>
      </Stack>
    </Group>
  );
}

Example: Date Picker

The Picker component can be used as a Date Picker.

242526272829303101020304050607080910111213141516171819202122232425
OctoberNovemberDecemberJanuaryFebruaryMarchAprilMayJuneJulyAugustSeptemberOctober
199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996

Selected date: 09 April 2026

import { useState } from 'react';
import { Picker, PickerProps } from '@gfazioli/mantine-picker';
import { Group, Stack, Text } from '@mantine/core';

function DatePicker() {
  const days = Array.from({ length: 31 }, (_, i) => (i < 9 ? `0${i + 1}` : `${i + 1}`));
  const months = [
    'January',
    'February',
    'March',
    'April',
    'May',
    'June',
    'July',
    'August',
    'September',
    'October',
    'November',
    'December',
  ];
  // years start from 1970 to 2030
  const years = Array.from({ length: 61 }, (_, i) => (i + 1970).toString());

  const today =
    (new Date().getDate() as number) < 10 ? `0${new Date().getDate()}` : new Date().getDate();

  const [day, setDay] = useState(today.toString());
  const [month, setMonth] = useState(months[new Date().getMonth()]);
  const [year, setYear] = useState(new Date().getFullYear().toString());

  const pickerProps: Omit<PickerProps, 'data'> = {
    withDividers: false,
    withHighlight: false,
    loop: true,
    maxRotation: 90,
    itemHeight: 30,
    visibleItems: 5,
    withMask: false,
    cylinderRadius: 3,
  };

  return (
    <Stack align="center" justify="space-between" h={200}>
      <Group gap={0}>
        <Picker {...pickerProps} w={60} value={day} data={days} rotateY={-10} onChange={setDay} />
        <Picker {...pickerProps} w={80} value={month} data={months} onChange={setMonth} />
        <Picker {...pickerProps} w={60} value={year} data={years} rotateY={10} onChange={setYear} />
      </Group>
      <Text>Selected date: {`${day} ${month} ${year}`}</Text>
    </Stack>
  );
}

Example: Slot Machine

As funny as it may sound, the Picker component can be used to create a slot machine effect.

🍌🍍🥝🍏🍎🍐🍑🍒🍋🍊🍉🍇🍓🍈🍌
🍌🍍🥝🍏🍎🍐🍑🍒🍋🍊🍉🍇🍓🍈🍌
🍌🍍🥝🍏🍎🍐🍑🍒🍋🍊🍉🍇🍓🍈🍌

🎉 Jackpot! 🎉

import { useState } from 'react';
import { Picker } from '@gfazioli/mantine-picker';
import { Button, Group, Paper, Stack, Text } from '@mantine/core';
import { MantineDemo } from '@mantinex/demo';

function SlotMachine() {
  const slotMachineData = [
    '🍒',
    '🍋',
    '🍊',
    '🍉',
    '🍇',
    '🍓',
    '🍈',
    '🍌',
    '🍍',
    '🥝',
    '🍏',
    '🍎',
    '🍐',
    '🍑',
  ];

  const pickerNumber = 3;
  const [values, setValues] = useState(Array(pickerNumber).fill(slotMachineData[0]));
  const [spinning, setSpinning] = useState(false);

  const pickerSize = 80;
  const groupGap = 4;
  const paperPadding = 4;
  const paperWidth = pickerSize * pickerNumber + paperPadding * 2 + groupGap * pickerNumber;

  const spin = () => {
    setSpinning(true);
    const newValues = Array.from(
      { length: pickerNumber },
      () => slotMachineData[Math.floor(Math.random() * slotMachineData.length)]
    );

    setValues(newValues);
    setSpinning(false);
  };

  return (
    <Stack align="center" justify="space-between" h={400}>
      <Paper radius={16} withBorder w={paperWidth} p={paperPadding}>
        <Group gap={groupGap}>
          {Array.from({ length: pickerNumber }).map((_, index) => (
            <Picker
              key={index}
              w={pickerSize}
              loop
              withMask
              data={slotMachineData}
              value={values[index]}
              readOnly
              itemHeight={50}
              visibleItems={5}
              withHighlight={false}
              maxBlurAmount={8}
              maskHeight={20}
              size="32px"
            />
          ))}
        </Group>
      </Paper>
      <Button onClick={spin} disabled={spinning}>
        {spinning ? 'Spinning...' : 'Spin'}
      </Button>
      <Text>{values.every((val, _, arr) => val === arr[0]) ? '🎉 Jackpot! 🎉' : 'Try Again!'}</Text>
    </Stack>
  );
}