Mantine Compare

Logo

@gfazioli/mantine-compare

Mantine Compare is a responsive side‑by‑side comparison container that lets you render two pieces of content (images or custom React nodes) with consistent styling, layout controls, and accessibility.

Installation

yarn add @gfazioli/mantine-compare

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

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

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

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

Usage

The Compare is interactive component that allows you to compare two content sections by dragging a slider. It is particularly useful for visualizing differences between two versions of data.

Before

After

Variant
Radius
Angle
Default position
Min drag bound
Max drag bound
Slider color
Slider width
Auto play speed
import { Compare } from '@gfazioli/mantine-compare';
import { Box, Text } from '@mantine/core';

function Demo() {
  return (
    <Compare
      leftSection={
        <Box
          style={{
            background: 'linear-gradient(45deg, #667eea 0%, #764ba2 100%)',
            width: '100%',
            height: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text size="xl" c="white" fw={700}>Before</Text>
        </Box>
      }
      rightSection={
        <Box
          style={{
            background: 'linear-gradient(45deg, #f093fb 0%, #f5576c 100%)',
            width: '100%',
            height: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text size="xl" c="white" fw={700}>After</Text>
        </Box>
      }
    />
  );
}

With Image Content

You can use the Compare component to compare two images by passing Image components as the leftSection and rightSection props.

Before
After
import { Compare } from '@gfazioli/mantine-compare';
import { Image } from '@mantine/core';

function Demo() {
  return (
    <Compare
      leftSection={
        <Image
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&auto=format&fit=crop"
          alt="Before"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
      rightSection={
        <Image
          src="https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&auto=format&fit=crop"
          alt="After"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
    />
  );
}

With Code Content

You can use the Compare component to compare two code snippets by passing code blocks as the leftSection and rightSection props.

type Item = { id: string; price: number; qty: number };

function total(items: Item[], discountCode?: string): number {

    let sum = 0;
    for (const it of items) {
        sum += it.price * it.qty;
    }
    if (discountCode === "SAVE10") {
        sum = sum * 0.9;
    }
    return Math.round(sum * 100) / 100;
}
type Item = { id: string; price: number; qty: number };
type Discount = { code: string; apply(total: number): number };

const SAVE10: Discount = { code: "SAVE10", apply: (t) => t * 0.9 };
const discounts: Record<string, Discount> = { [SAVE10.code]: SAVE10 };

function computeTotal(items: Item[], discountCode?: string): number {
    if (items.some(i => i.price < 0 || i.qty <= 0)) {
        throw new Error("Invalid item values");
    }
    const base = items.reduce((sum, i) => sum + i.price * i.qty, 0);
    const discounted = discountCode && discounts[discountCode]
        ? discounts[discountCode].apply(base)
        : base;
    return Math.round(discounted * 100) / 100;
}
import { Compare } from '@gfazioli/mantine-compare';
import { CodeHighlight } from '@mantine/code-highlight';
import { exampleCodeBefore } from './exampleCodeBefore';
import { exampleCodeAfter } from './exampleCodeAfter';
import classes from './Compare.module.css';

function Demo() {
  return (
    <Compare
      classNames={classes}
      defaultPosition={80}
      leftSection={<CodeHighlight code={exampleCodeBefore} language="tsx" radius="md" />}
      rightSection={<CodeHighlight code={exampleCodeAfter} language="tsx" radius="md" />}
    />
  );
}

Labels

Use leftLabel and rightLabel props to display overlay text labels on each section. Labels are styled with a semi-transparent dark background and positioned at the top corners.

Before
After
BeforeAfter
import { Compare } from '@gfazioli/mantine-compare';
import { Image } from '@mantine/core';

function Demo() {
  return (
    <Compare
      leftLabel="Before"
      rightLabel="After"
      leftSection={
        <Image
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&auto=format&fit=crop"
          alt="Before"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
      rightSection={
        <Image
          src="https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&auto=format&fit=crop"
          alt="After"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
    />
  );
}

Angle

Use angle to control the divider orientation.

  • angle={0} behaves like the previous vertical direction (left/right compare)
  • angle={90} behaves like the previous horizontal direction (top/bottom compare)
  • Any value between 0 and 360 creates an oblique (diagonal) divider
Before
After
import { Compare } from '@gfazioli/mantine-compare';
import { Image } from '@mantine/core';

function Demo() {
  return (
    <Compare
      angle={90}
      leftSection={
        <Image
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&auto=format&fit=crop"
          alt="Before"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
      rightSection={
        <Image
          src="https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&auto=format&fit=crop"
          alt="After"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
    />
  );
}

Diagonal angle

Set angle to any value between 0 and 360 to get an oblique divider.

Before
After
import { Compare } from '@gfazioli/mantine-compare';
import { Image } from '@mantine/core';

function Demo() {
  return (
    <Compare
      angle={30}
      leftSection={
        <Image
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&auto=format&fit=crop"
          alt="Before"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
      rightSection={
        <Image
          src="https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&auto=format&fit=crop"
          alt="After"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
    />
  );
}

Drag boundaries

Use minDragBound and maxDragBound props to limit the slider movement range. Both props accept percentage values (0-100):

  • minDragBound - controls how far left/top the slider can be dragged (default: 0)
  • maxDragBound - controls how far right/bottom the slider can be dragged (default: 100)

These boundaries work with any angle setting, making it useful for preventing the slider from completely hiding either section.

Left Section

Right Section

import { Compare } from '@gfazioli/mantine-compare';
import { Box, Text } from '@mantine/core';

function Demo() {
  return (
    <Compare
      angle={30}
      minDragBound={20}
      maxDragBound={80}
      leftSection={
        <Box
          style={{
            background: 'linear-gradient(45deg, #667eea 0%, #764ba2 100%)',
            width: '100%',
            height: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text size="xl" c="white" fw={700}>
            Left Section
          </Text>
        </Box>
      }
      rightSection={
        <Box
          style={{
            background: 'linear-gradient(45deg, #f093fb 0%, #f5576c 100%)',
            width: '100%',
            height: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text size="xl" c="white" fw={700}>
            Right Section
          </Text>
        </Box>
      }
    />
  );
}

Variants

The Compare component supports three interaction variants:

Drag variant (default)

The default variant="drag" allows users to click and drag the slider button to compare content.

Hover variant

Set variant="hover" to reveal content by moving the mouse over the component. The slider button is hidden in this variant.

Before
After
import { Compare } from '@gfazioli/mantine-compare';
import { Image } from '@mantine/core';

function Demo() {
  return (
    <Compare
      variant="hover"
      leftSection={
        <Image
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&auto=format&fit=crop"
          alt="Before"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
      rightSection={
        <Image
          src="https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&auto=format&fit=crop"
          alt="After"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
    />
  );
}

Fixed variant

Set variant="fixed" to display a static comparison at a specific position. The slider button is hidden and users cannot interact with the slider. Use defaultPosition to control the split position.

Before

After

import { Compare } from '@gfazioli/mantine-compare';
import { Box, Text } from '@mantine/core';

function Demo() {
  return (
    <Compare
      variant="fixed"
      defaultPosition={35}
      leftSection={
        <Box
          style={{
            background: 'linear-gradient(45deg, #667eea 0%, #764ba2 100%)',
            width: '100%',
            height: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text size="xl" c="white" fw={700}>
            Before
          </Text>
        </Box>
      }
      rightSection={
        <Box
          style={{
            background: 'linear-gradient(45deg, #f093fb 0%, #f5576c 100%)',
            width: '100%',
            height: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text size="xl" c="white" fw={700}>
            After
          </Text>
        </Box>
      }
    />
  );
}

Disabled

Set disabled to prevent all interactions (drag, hover, keyboard). The component appears with reduced opacity.

Left

Right

import { Compare } from '@gfazioli/mantine-compare';
import { Box, Text } from '@mantine/core';

function Demo() {
  return (
    <Compare
      disabled
      defaultPosition={35}
      leftSection={
        <Box
          style={{
            background: 'linear-gradient(45deg, #667eea 0%, #764ba2 100%)',
            width: '100%',
            height: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text size="xl" c="white" fw={700}>Left</Text>
        </Box>
      }
      rightSection={
        <Box
          style={{
            background: 'linear-gradient(45deg, #f093fb 0%, #f5576c 100%)',
            width: '100%',
            height: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text size="xl" c="white" fw={700}>Right</Text>
        </Box>
      }
    />
  );
}

Slider styling

Customize the slider divider appearance with sliderColor and sliderWidth props. Both are exposed as CSS variables for fine-grained control.

Before
After
BeforeAfter
import { Compare } from '@gfazioli/mantine-compare';
import { Image } from '@mantine/core';

function Demo() {
  return (
    <Compare
      sliderColor="cyan"
      sliderWidth={3}
      leftLabel="Before"
      rightLabel="After"
      leftSection={
        <Image
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&auto=format&fit=crop"
          alt="Before"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
      rightSection={
        <Image
          src="https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&auto=format&fit=crop"
          alt="After"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
    />
  );
}

Handle only

Set handleOnly to restrict dragging to the handle button only. When enabled, clicking and dragging on the slider line does not move the position — only the circular handle button responds to drag.

Before
After
import { Compare } from '@gfazioli/mantine-compare';
import { Image } from '@mantine/core';

function Demo() {
  return (
    <Compare
      handleOnly
      leftSection={
        <Image
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&auto=format&fit=crop"
          alt="Before"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
      rightSection={
        <Image
          src="https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&auto=format&fit=crop"
          alt="After"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
    />
  );
}

Auto-play

Set autoPlay to continuously animate the slider back and forth. The animation pauses when the user hovers over the component. Control the speed with autoPlaySpeed (1-100 scale, higher = faster, default 50). Choose an easing curve with autoPlayEasing: linear (default), ease-in, ease-out, ease-in-out, or spring.

Original
Enhanced
OriginalEnhanced
import { Compare } from '@gfazioli/mantine-compare';
import { Image } from '@mantine/core';

function Demo() {
  return (
    <Compare
      autoPlay
      autoPlaySpeed={60}
      leftLabel="Original"
      rightLabel="Enhanced"
      leftSection={
        <Image
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&auto=format&fit=crop"
          alt="Original"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
      rightSection={
        <Image
          src="https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&auto=format&fit=crop"
          alt="Enhanced"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      }
    />
  );
}

Controlled position

Use the position prop with onPositionChange for full control over the slider position. This allows you to sync the slider with external controls like buttons or a Mantine Slider.

BeforeAfter

Position: 50%

import { useState } from 'react';
import { Compare } from '@gfazioli/mantine-compare';
import { Box, Button, Group, Slider, Text } from '@mantine/core';

function Demo() {
  const [position, setPosition] = useState(50);

  return (
    <>
      <Group mb="sm">
        <Button size="xs" onClick={() => setPosition(25)}>25%</Button>
        <Button size="xs" onClick={() => setPosition(50)}>50%</Button>
        <Button size="xs" onClick={() => setPosition(75)}>75%</Button>
      </Group>
      <Slider value={position} onChange={setPosition} min={0} max={100} mb="sm" />
      <Compare
        position={position}
        onPositionChange={setPosition}
        leftLabel="Before"
        rightLabel="After"
        leftSection={
          <Box style={{ background: 'linear-gradient(45deg, #667eea, #764ba2)', width: '100%', height: '100%' }} />
        }
        rightSection={
          <Box style={{ background: 'linear-gradient(45deg, #f093fb, #f5576c)', width: '100%', height: '100%' }} />
        }
      />
      <Text size="xs" c="dimmed" mt="xs" ta="center">Position: {Math.round(position)}%</Text>
    </>
  );
}

Keyboard navigation

The Compare slider supports keyboard navigation. Click on the slider to focus it, then use:

  • Arrow Left/Down: Move slider by keyboardStep (default 1%)
  • Arrow Right/Up: Move slider by keyboardStep (default 1%)
  • Shift + Arrow: Move slider by keyboardShiftStep (default 10%)
  • Home: Move to minimum position
  • End: Move to maximum position

Keyboard navigation respects minDragBound and maxDragBound constraints and is disabled for fixed and disabled states.

Before

After

import { Compare } from '@gfazioli/mantine-compare';
import { Box, Text } from '@mantine/core';

function Demo() {
  return (
    <Compare
      leftSection={
        <Box
          style={{
            background: 'linear-gradient(45deg, #667eea 0%, #764ba2 100%)',
            width: '100%',
            height: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text size="xl" c="white" fw={700}>Before</Text>
        </Box>
      }
      rightSection={
        <Box
          style={{
            background: 'linear-gradient(45deg, #f093fb 0%, #f5576c 100%)',
            width: '100%',
            height: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text size="xl" c="white" fw={700}>After</Text>
        </Box>
      }
    />
  );
}

Styles API

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

Before

After

BeforeAfter

Component Styles API

Hover over selectors to highlight corresponding elements

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