Mantine Rings Progress

Undolog

@gfazioli/mantine-rings-progress

A Mantine component that replicates the progress rings of Apple Watch.

Installation

yarn add @gfazioli/mantine-rings-progress

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

import '@gfazioli/mantine-rings-progress/styles.css';

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

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

Usage

Size
Gap
Thickness
Root color alpha
Animation duration
Animation steps
import { RingsProgress } from '@gfazioli/mantine-rings-progress';

function Demo() {
  const rings = [
    { value: 20, color: 'cyan' },
    { value: 50, color: 'red' },
    { value: 80, color: '#f90' },
  ];

  return (
    <RingsProgress rings={rings} size={180} label="" />
  );
}

Mantine RingProgress

We're using a custom version of the RingProgress component from the mantine library to render the rings.

The label prop can be a string or a component. Try to change the label on the above configurator. You may use also an emoji 😊 or a custom component.

Label

import { RingsProgress } from '@gfazioli/mantine-rings-progress';

function Demo() {
  const rings = [{ value: 20, color: 'green' }];

  return (
    <RingsProgress
      size={100}
      rings={rings}
      label={
        <ActionIcon color="teal" variant="light" radius="xl" size="xl">
          <IconCheck style={{ width: rem(22), height: rem(22) }} />
        </ActionIcon>
      }
    />
  );
}

The label prop can be a string or a component.

import { RingsProgress } from '@gfazioli/mantine-rings-progress';

function Demo() {
  const rings = [
    { value: 20, color: 'green' },
    { value: 80, color: 'blue' },
  ];

  return (
    <RingsProgress
      size={140}
      rings={rings}
      label={
        <ActionIcon color="yellow" variant="filled" radius="xl" size="xl">
          <IconCheck style={{ width: rem(22), height: rem(22) }} />
        </ActionIcon>
      }
    />
  );
}

Tooltips

Despite the RingProgress component from the mantine library has a tooltip prop, we discourage its use. The tooltip will be rendered on the top of the rings and it will be hard to read the label.

import { RingsProgress } from '@gfazioli/mantine-rings-progress';

function Demo() {
  const rings = [
    { value: 20, color: 'green', tooltip: 'Fitness – 40 Gb' },
    { value: 80, color: 'blue', tooltip: 'Running – 50 minutes' },
  ];

  return (
    <RingsProgress
      size={140}
      rings={rings}
      label={
        <ActionIcon color="yellow" variant="filled" radius="xl" size="xl">
          <IconCheck style={{ width: rem(22), height: rem(22) }} />
        </ActionIcon>
      }
    />
  );
}

Use cases

Countdown

Below, another example of the RingProgress component from the mantine library. This time, we're using it to render a countdown.

import { useEffect, useState } from 'react';
import { RingsProgress } from '@gfazioli/mantine-rings-progress';
import {
  ActionIcon,
  Box,
  Button,
  Center,
  Group,
  NumberInput,
  Stack,
  Switch,
  Text,
} from '@mantine/core';

function Demo() {
  const [inputMinutes, setInputMinutes] = useState(10);
  const [timeLeft, setTimeLeft] = useState(inputMinutes * 60 * 100);
  const [isRunning, setIsRunning] = useState(false);
  const [displayHundredthsSeconds, setDisplayHundredthsSeconds] = useState(false);

  useEffect(() => {
    if (!isRunning) return;

    const timer = setInterval(() => {
      setTimeLeft((prevTime) => (prevTime > 0 ? prevTime - 1 : 0));
    }, 10);

    return () => clearInterval(timer);
  }, [isRunning]);

  const handleStartPause = () => {
    setIsRunning((prev) => !prev);
  };

  const handleReset = () => {
    setIsRunning(false);
    setTimeLeft(inputMinutes * 60 * 100);
  };

  const handleInputChange = (value: string | number) => {
    const minutes = typeof value === 'number' ? value : parseInt(value, 10);
    setInputMinutes(isNaN(minutes) ? 0 : minutes);
    setTimeLeft((isNaN(minutes) ? 0 : minutes) * 60 * 100);
  };

  const totalSeconds = Math.floor(timeLeft / 100);
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  const hundredthsSeconds = timeLeft % 100;

  const minutesPercent = (minutes / inputMinutes) * 100;
  const secondsPercent = (seconds / 60) * 100;
  const hundredthsSecondsPercent = (hundredthsSeconds / 100) * 100;

  const rings = [
    { value: minutesPercent, color: 'green' },
    { value: secondsPercent, color: 'blue' },
    ...(displayHundredthsSeconds ? [{ value: hundredthsSecondsPercent, color: 'red' }] : []),
  ];

  return (
    <Stack h={250} align="stretch">
      <Group align="center" justify="center">
        <NumberInput
          size="xs"
          disabled={isRunning}
          value={inputMinutes}
          onChange={handleInputChange}
          placeholder="Minutes"
          min={0}
        />
        <Button size="xs" onClick={handleStartPause} ml={10}>
          {isRunning ? 'Pause' : 'Start'}
        </Button>
        <Button size="xs" onClick={handleReset} ml={10}>
          Reset
        </Button>
        <Switch
          checked={displayHundredthsSeconds}
          onChange={(event) => setDisplayHundredthsSeconds(event.currentTarget.checked)}
          label="Hundredths of Seconds"
        />
      </Group>

      <Center>
        <Box h={200} w={200}>
          <RingsProgress
            size={200}
            thickness={12}
            rings={rings}
            label={
              <ActionIcon color="yellow" variant="outline" radius="xl" size={64}>
                <Text size="md">
                  {minutes < 10 ? `0${minutes}` : minutes}:{seconds < 10 ? `0${seconds}` : seconds}
                </Text>
              </ActionIcon>
            }
          />
        </Box>
      </Center>
    </Stack>
  );
}