Dice UI
Components

Compare Slider

An interactive before/after comparison slider for comparing two elements side by side.

API
import {
  CompareSlider,
  CompareSliderAfter,
  CompareSliderBefore,
  CompareSliderHandle,
} from "@/components/ui/compare-slider";
 
export function CompareSliderDemo() {
  return (
    <CompareSlider
      defaultValue={50}
      className="h-[400px] overflow-hidden rounded-lg border"
    >
      <CompareSliderBefore>
        {/* biome-ignore lint/performance/noImgElement: Demo image for comparison slider */}
        <img
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&h=1080&fit=crop&auto=format&fm=webp&q=80"
          alt="Before"
          className="size-full object-cover"
        />
      </CompareSliderBefore>
      <CompareSliderAfter>
        {/* biome-ignore lint/performance/noImgElement: Demo image for comparison slider */}
        <img
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&h=1080&fit=crop&auto=format&fm=webp&q=80&sat=-100"
          alt="After"
          className="size-full object-cover grayscale"
        />
      </CompareSliderAfter>
      <CompareSliderHandle />
    </CompareSlider>
  );
}

Installation

CLI

npx shadcn@latest add "https://diceui.com/r/compare-slider"

Manual

Install the following dependencies:

npm install @radix-ui/react-slot lucide-react

Copy the refs composition utilities into your lib/compose-refs.ts file.

/**
 * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx
 */
 
import * as React from "react";
 
type PossibleRef<T> = React.Ref<T> | undefined;
 
/**
 * Set a given ref to a given value
 * This utility takes care of different types of refs: callback refs and RefObject(s)
 */
function setRef<T>(ref: PossibleRef<T>, value: T) {
  if (typeof ref === "function") {
    return ref(value);
  }
 
  if (ref !== null && ref !== undefined) {
    ref.current = value;
  }
}
 
/**
 * A utility to compose multiple refs together
 * Accepts callback refs and RefObject(s)
 */
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
  return (node) => {
    let hasCleanup = false;
    const cleanups = refs.map((ref) => {
      const cleanup = setRef(ref, node);
      if (!hasCleanup && typeof cleanup === "function") {
        hasCleanup = true;
      }
      return cleanup;
    });
 
    // React <19 will log an error to the console if a callback ref returns a
    // value. We don't use ref cleanups internally so this will only happen if a
    // user's ref callback returns a value, which we only expect if they are
    // using the cleanup functionality added in React 19.
    if (hasCleanup) {
      return () => {
        for (let i = 0; i < cleanups.length; i++) {
          const cleanup = cleanups[i];
          if (typeof cleanup === "function") {
            cleanup();
          } else {
            setRef(refs[i], null);
          }
        }
      };
    }
  };
}
 
/**
 * A custom hook that composes multiple refs
 * Accepts callback refs and RefObject(s)
 */
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
  // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values
  return React.useCallback(composeRefs(...refs), refs);
}
 
export { composeRefs, useComposedRefs };

Copy and paste the following code into your project.

"use client";
 
import { Slot } from "@radix-ui/react-slot";
import {
  ChevronDownIcon,
  ChevronLeftIcon,
  ChevronRightIcon,
  ChevronUpIcon,
} from "lucide-react";
import * as React from "react";
import { useComposedRefs } from "@/lib/compose-refs";
import { cn } from "@/lib/utils";
 
const ROOT_NAME = "CompareSlider";
const BEFORE_NAME = "CompareSliderBefore";
const AFTER_NAME = "CompareSliderAfter";
const LABEL_NAME = "CompareSliderLabel";
const HANDLE_NAME = "CompareSliderHandle";
 
const PAGE_KEYS = ["PageUp", "PageDown"];
const ARROW_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"];
 
type Interaction = "hover" | "drag";
type Orientation = "horizontal" | "vertical";
 
interface DivProps extends React.ComponentProps<"div"> {
  asChild?: boolean;
}
 
type RootImplElement = React.ComponentRef<typeof CompareSliderRootImpl>;
 
const useIsomorphicLayoutEffect =
  typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
 
function useAsRef<T>(props: T) {
  const ref = React.useRef<T>(props);
 
  useIsomorphicLayoutEffect(() => {
    ref.current = props;
  });
 
  return ref;
}
 
function useLazyRef<T>(fn: () => T) {
  const ref = React.useRef<T | null>(null);
 
  if (ref.current === null) {
    ref.current = fn();
  }
 
  return ref as React.RefObject<T>;
}
 
function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max);
}
 
interface StoreState {
  value: number;
  isDragging: boolean;
}
 
interface Store {
  subscribe: (callback: () => void) => () => void;
  getState: () => StoreState;
  setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;
  notify: () => void;
}
 
const StoreContext = React.createContext<Store | null>(null);
 
function useStoreContext(consumerName: string) {
  const context = React.useContext(StoreContext);
  if (!context) {
    throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
  }
  return context;
}
 
function useStore<T>(selector: (state: StoreState) => T): T {
  const store = useStoreContext("useStore");
 
  const getSnapshot = React.useCallback(
    () => selector(store.getState()),
    [store, selector],
  );
 
  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
}
 
interface CompareSliderContextValue {
  interaction: Interaction;
  orientation: Orientation;
}
 
const CompareSliderContext =
  React.createContext<CompareSliderContextValue | null>(null);
 
function useCompareSliderContext(consumerName: string) {
  const context = React.useContext(CompareSliderContext);
  if (!context) {
    throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
  }
  return context;
}
 
interface CompareSliderRootProps extends DivProps {
  value?: number;
  defaultValue?: number;
  onValueChange?: (value: number) => void;
  step?: number;
  interaction?: Interaction;
  orientation?: Orientation;
}
 
function CompareSliderRoot(props: CompareSliderRootProps) {
  const { value, defaultValue = 50, onValueChange, ...rootProps } = props;
 
  const stateRef = useLazyRef<StoreState>(() => ({
    value: clamp(value ?? defaultValue, 0, 100),
    isDragging: false,
  }));
  const listenersRef = useLazyRef(() => new Set<() => void>());
  const onValueChangeRef = useAsRef(onValueChange);
 
  const store = React.useMemo<Store>(() => {
    return {
      subscribe: (cb) => {
        listenersRef.current.add(cb);
        return () => listenersRef.current.delete(cb);
      },
      getState: () => stateRef.current,
      setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => {
        if (Object.is(stateRef.current[key], value)) return;
        stateRef.current[key] = value;
 
        if (key === "value") {
          onValueChangeRef.current?.(value as number);
        }
 
        store.notify();
      },
      notify: () => {
        for (const cb of listenersRef.current) {
          cb();
        }
      },
    };
  }, [listenersRef, stateRef, onValueChangeRef]);
 
  useIsomorphicLayoutEffect(() => {
    if (value !== undefined) {
      store.setState("value", clamp(value, 0, 100));
    }
  }, [value, store]);
 
  return (
    <StoreContext.Provider value={store}>
      <CompareSliderRootImpl {...rootProps} />
    </StoreContext.Provider>
  );
}
 
function CompareSliderRootImpl(
  props: Omit<
    CompareSliderRootProps,
    "value" | "defaultValue" | "onValueChange"
  >,
) {
  const {
    step = 1,
    interaction = "drag",
    orientation = "horizontal",
    className,
    children,
    ref,
    onPointerMove: onPointerMoveProp,
    onPointerUp: onPointerUpProp,
    onPointerDown: onPointerDownProp,
    onKeyDown: onKeyDownProp,
    asChild,
    ...rootProps
  } = props;
 
  const store = useStoreContext(ROOT_NAME);
  const value = useStore((state) => state.value);
 
  const containerRef = React.useRef<RootImplElement>(null);
  const composedRef = useComposedRefs(ref, containerRef);
  const isDraggingRef = React.useRef(false);
 
  const propsRef = useAsRef({
    onPointerMove: onPointerMoveProp,
    onPointerUp: onPointerUpProp,
    onPointerDown: onPointerDownProp,
    onKeyDown: onKeyDownProp,
    interaction,
    orientation,
    step,
  });
 
  const onPointerMove = React.useCallback(
    (event: React.PointerEvent<RootImplElement>) => {
      if (!isDraggingRef.current && propsRef.current.interaction === "drag") {
        return;
      }
      if (!containerRef.current) return;
 
      propsRef.current.onPointerMove?.(event);
      if (event.defaultPrevented) return;
 
      const containerRect = containerRef.current.getBoundingClientRect();
      const isVertical = propsRef.current.orientation === "vertical";
      const position = isVertical
        ? event.clientY - containerRect.top
        : event.clientX - containerRect.left;
      const size = isVertical ? containerRect.height : containerRect.width;
      const percentage = clamp((position / size) * 100, 0, 100);
 
      store.setState("value", percentage);
    },
    [propsRef, store],
  );
 
  const onPointerDown = React.useCallback(
    (event: React.PointerEvent<RootImplElement>) => {
      if (propsRef.current.interaction !== "drag") return;
 
      propsRef.current.onPointerDown?.(event);
      if (event.defaultPrevented) return;
 
      event.currentTarget.setPointerCapture(event.pointerId);
      isDraggingRef.current = true;
      store.setState("isDragging", true);
    },
    [store, propsRef],
  );
 
  const onPointerUp = React.useCallback(
    (event: React.PointerEvent<RootImplElement>) => {
      if (propsRef.current.interaction !== "drag") return;
 
      propsRef.current.onPointerUp?.(event);
      if (event.defaultPrevented) return;
 
      event.currentTarget.releasePointerCapture(event.pointerId);
      isDraggingRef.current = false;
      store.setState("isDragging", false);
    },
    [store, propsRef],
  );
 
  const onKeyDown = React.useCallback(
    (event: React.KeyboardEvent<RootImplElement>) => {
      propsRef.current.onKeyDown?.(event);
      if (event.defaultPrevented) return;
 
      const currentValue = store.getState().value;
      const isVertical = propsRef.current.orientation === "vertical";
 
      if (event.key === "Home") {
        event.preventDefault();
        store.setState("value", 0);
      } else if (event.key === "End") {
        event.preventDefault();
        store.setState("value", 100);
      } else if (PAGE_KEYS.concat(ARROW_KEYS).includes(event.key)) {
        event.preventDefault();
 
        const isPageKey = PAGE_KEYS.includes(event.key);
        const isSkipKey =
          isPageKey || (event.shiftKey && ARROW_KEYS.includes(event.key));
        const multiplier = isSkipKey ? 10 : 1;
 
        let direction = 0;
        if (isVertical) {
          const isDecreaseKey = ["ArrowUp", "PageUp"].includes(event.key);
          direction = isDecreaseKey ? -1 : 1;
        } else {
          const isDecreaseKey = ["ArrowLeft", "PageUp"].includes(event.key);
          direction = isDecreaseKey ? -1 : 1;
        }
 
        const stepInDirection = propsRef.current.step * multiplier * direction;
        const newValue = clamp(currentValue + stepInDirection, 0, 100);
        store.setState("value", newValue);
      }
    },
    [store, propsRef],
  );
 
  const contextValue = React.useMemo<CompareSliderContextValue>(
    () => ({
      interaction,
      orientation,
    }),
    [interaction, orientation],
  );
 
  const RootPrimitive = asChild ? Slot : "div";
 
  return (
    <CompareSliderContext.Provider value={contextValue}>
      <RootPrimitive
        role="slider"
        aria-orientation={orientation}
        aria-valuemax={100}
        aria-valuemin={0}
        aria-valuenow={value}
        data-slot="compare-slider"
        data-orientation={orientation}
        {...rootProps}
        ref={composedRef}
        tabIndex={0}
        className={cn(
          "relative isolate select-none overflow-hidden outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
          orientation === "horizontal" ? "w-full" : "h-full",
          className,
        )}
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={onPointerUp}
        onPointerCancel={onPointerUp}
        onKeyDown={onKeyDown}
      >
        {children}
      </RootPrimitive>
    </CompareSliderContext.Provider>
  );
}
 
interface CompareSliderBeforeProps extends DivProps {
  label?: string;
}
 
function CompareSliderBefore(props: CompareSliderBeforeProps) {
  const { className, children, style, label, asChild, ref, ...beforeProps } =
    props;
 
  const value = useStore((state) => state.value);
  const { orientation } = useCompareSliderContext(BEFORE_NAME);
 
  const labelId = React.useId();
 
  const isVertical = orientation === "vertical";
  const clipPath = isVertical
    ? `inset(${value}% 0 0 0)`
    : `inset(0 0 0 ${value}%)`;
 
  const BeforePrimitive = asChild ? Slot : "div";
 
  return (
    <BeforePrimitive
      role="img"
      aria-labelledby={label ? labelId : undefined}
      aria-hidden={label ? undefined : "true"}
      data-slot="compare-slider-before"
      data-orientation={orientation}
      {...beforeProps}
      ref={ref}
      className={cn("absolute inset-0 h-full w-full object-cover", className)}
      style={{
        clipPath,
        ...style,
      }}
    >
      {children}
      {label && (
        <CompareSliderLabel id={labelId} side="before">
          {label}
        </CompareSliderLabel>
      )}
    </BeforePrimitive>
  );
}
 
interface CompareSliderAfterProps extends DivProps {
  label?: string;
}
 
function CompareSliderAfter(props: CompareSliderAfterProps) {
  const { className, children, style, label, asChild, ref, ...afterProps } =
    props;
 
  const value = useStore((state) => state.value);
  const { orientation } = useCompareSliderContext(AFTER_NAME);
 
  const labelId = React.useId();
 
  const isVertical = orientation === "vertical";
  const clipPath = isVertical
    ? `inset(0 0 ${100 - value}% 0)`
    : `inset(0 ${100 - value}% 0 0)`;
 
  const AfterPrimitive = asChild ? Slot : "div";
 
  return (
    <AfterPrimitive
      role="img"
      aria-labelledby={label ? labelId : undefined}
      aria-hidden={label ? undefined : "true"}
      data-slot="compare-slider-after"
      data-orientation={orientation}
      {...afterProps}
      ref={ref}
      className={cn("absolute inset-0 h-full w-full object-cover", className)}
      style={{
        clipPath,
        ...style,
      }}
    >
      {children}
      {label && (
        <CompareSliderLabel id={labelId} side="after">
          {label}
        </CompareSliderLabel>
      )}
    </AfterPrimitive>
  );
}
 
function CompareSliderHandle(props: DivProps) {
  const { className, children, style, asChild, ref, ...handleProps } = props;
 
  const value = useStore((state) => state.value);
  const { interaction, orientation } = useCompareSliderContext(HANDLE_NAME);
 
  const isVertical = orientation === "vertical";
 
  const HandlePrimitive = asChild ? Slot : "div";
 
  return (
    <HandlePrimitive
      role="presentation"
      aria-hidden="true"
      data-slot="compare-slider-handle"
      data-orientation={orientation}
      {...handleProps}
      ref={ref}
      className={cn(
        "absolute z-50 flex items-center justify-center",
        isVertical
          ? "-translate-y-1/2 left-0 h-10 w-full"
          : "-translate-x-1/2 top-0 h-full w-10",
        interaction === "drag" && "cursor-grab active:cursor-grabbing",
        className,
      )}
      style={{
        [isVertical ? "top" : "left"]: `${value}%`,
        ...style,
      }}
    >
      {children ?? (
        <>
          <div
            className={cn(
              "absolute bg-background",
              isVertical
                ? "-translate-y-1/2 top-1/2 h-1 w-full"
                : "-translate-x-1/2 left-1/2 h-full w-1",
            )}
          />
          {interaction === "drag" && (
            <div className="z-50 flex aspect-square size-11 shrink-0 items-center justify-center rounded-full bg-background p-2 [&_svg]:size-4 [&_svg]:select-none [&_svg]:stroke-3 [&_svg]:text-muted-foreground">
              {isVertical ? (
                <div className="flex flex-col items-center">
                  <ChevronUpIcon />
                  <ChevronDownIcon />
                </div>
              ) : (
                <div className="flex items-center">
                  <ChevronLeftIcon />
                  <ChevronRightIcon />
                </div>
              )}
            </div>
          )}
        </>
      )}
    </HandlePrimitive>
  );
}
 
interface CompareSliderLabelProps extends DivProps {
  side?: "before" | "after";
}
 
function CompareSliderLabel(props: CompareSliderLabelProps) {
  const { className, children, side, asChild, ref, ...labelProps } = props;
 
  const { orientation } = useCompareSliderContext(LABEL_NAME);
  const isVertical = orientation === "vertical";
 
  const LabelPrimitive = asChild ? Slot : "div";
 
  return (
    <LabelPrimitive
      ref={ref}
      data-slot="compare-slider-label"
      className={cn(
        "absolute z-20 rounded-md border border-border bg-background/80 px-3 py-1.5 font-medium text-sm backdrop-blur-sm",
        isVertical
          ? side === "before"
            ? "top-2 left-2"
            : "bottom-2 left-2"
          : side === "before"
            ? "top-2 left-2"
            : "top-2 right-2",
        className,
      )}
      {...labelProps}
    >
      {children}
    </LabelPrimitive>
  );
}
 
export {
  CompareSliderRoot as Root,
  CompareSliderAfter as After,
  CompareSliderBefore as Before,
  CompareSliderHandle as Handle,
  CompareSliderLabel as Label,
  //
  CompareSliderRoot as CompareSlider,
  CompareSliderAfter,
  CompareSliderBefore,
  CompareSliderHandle,
  CompareSliderLabel,
};

Layout

Import the parts, and compose them together.

import * as CompareSlider from "@/components/ui/compare-slider";

return (
  <CompareSlider.Root>
    <CompareSlider.Before />
    <CompareSlider.After />
    <CompareSlider.Handle />
  </CompareSlider.Root>
)

Examples

Controlled State

A compare slider with external controls for the slider position.

"use client";
 
import * as React from "react";
import {
  CompareSlider,
  CompareSliderAfter,
  CompareSliderBefore,
  CompareSliderHandle,
} from "@/components/ui/compare-slider";
 
export function CompareSliderControlledDemo() {
  const [value, setValue] = React.useState(30);
 
  return (
    <CompareSlider
      value={value}
      onValueChange={setValue}
      className="h-[400px] overflow-hidden rounded-lg border"
    >
      <CompareSliderBefore label="Original">
        {/* biome-ignore lint/performance/noImgElement: Demo image for comparison slider */}
        <img
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&h=1080&fit=crop&auto=format&fm=webp&q=80"
          alt="Original"
          className="size-full object-cover"
        />
      </CompareSliderBefore>
      <CompareSliderAfter label="Enhanced">
        {/* biome-ignore lint/performance/noImgElement: Demo image for comparison slider */}
        <img
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&h=1080&fit=crop&auto=format&fm=webp&q=80&sat=50"
          alt="Enhanced"
          className="size-full object-cover"
        />
      </CompareSliderAfter>
      <CompareSliderHandle />
    </CompareSlider>
  );
}

Vertical Orientation

A compare slider with vertical orientation, perfect for comparing tall images or content.

import {
  CompareSlider,
  CompareSliderAfter,
  CompareSliderBefore,
  CompareSliderHandle,
} from "@/components/ui/compare-slider";
 
export function CompareSliderVerticalDemo() {
  return (
    <CompareSlider
      defaultValue={50}
      orientation="vertical"
      className="h-[400px] w-full overflow-hidden rounded-lg border"
    >
      <CompareSliderBefore>
        {/* biome-ignore lint/performance/noImgElement: Demo image for comparison slider */}
        <img
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&h=1080&fit=crop&auto=format&fm=webp&q=80"
          alt="Before"
          className="size-full object-cover"
        />
      </CompareSliderBefore>
      <CompareSliderAfter>
        {/* biome-ignore lint/performance/noImgElement: Demo image for comparison slider */}
        <img
          src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&h=1080&fit=crop&auto=format&fm=webp&q=80&sat=-100"
          alt="After"
          className="size-full object-cover grayscale"
        />
      </CompareSliderAfter>
      <CompareSliderHandle />
    </CompareSlider>
  );
}

Customization

Compare slider with custom handle, labels, and vertical orientation.

"use client";
 
import {
  CompareSlider,
  CompareSliderAfter,
  CompareSliderBefore,
  CompareSliderHandle,
} from "@/components/ui/compare-slider";
 
export function CompareSliderCustomDemo() {
  return (
    <CompareSlider
      defaultValue={50}
      className="h-[300px] overflow-hidden rounded-lg border"
    >
      <CompareSliderBefore className="flex size-full items-center justify-center bg-muted text-center">
        <div className="font-bold text-2xl">Kickflip</div>
      </CompareSliderBefore>
      <CompareSliderAfter className="flex size-full items-center justify-center bg-primary text-center text-primary-foreground">
        <div className="font-bold text-2xl">Heelflip</div>
      </CompareSliderAfter>
      <CompareSliderHandle>
        <div className="flex size-10 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg">
          <span className="font-bold text-xs">VS</span>
        </div>
      </CompareSliderHandle>
    </CompareSlider>
  );
}

API Reference

Root

The root container for the compare slider component.

Prop

Type

Data AttributeValue
[data-orientation]"horizontal" | "vertical"

Before

The container for the "before" content that appears on the left (or top in vertical mode).

Prop

Type

Data AttributeValue
[data-orientation]"horizontal" | "vertical"

After

The container for the "after" content that appears on the right (or bottom in vertical mode).

Prop

Type

Data AttributeValue
[data-orientation]"horizontal" | "vertical"

Handle

The draggable handle that controls the comparison position.

Prop

Type

Data AttributeValue
[data-orientation]"horizontal" | "vertical"

Label

Custom labels that can be positioned on either side of the comparison.

Prop

Type

Accessibility

Keyboard Interactions

KeyDescription
TabMoves focus to the slider.
Shift + TabMoves focus away from the slider to the previous focusable element.
ArrowLeftArrowUpMoves the slider position left (or up in vertical mode) by the step amount.
ArrowRightArrowDownMoves the slider position right (or down in vertical mode) by the step amount.
PageUpMoves the slider position left (or up in vertical mode) by ten steps.
PageDownMoves the slider position right (or down in vertical mode) by ten steps.
Shift + ArrowLeftShift + ArrowUpMoves the slider position left (or up in vertical mode) by ten steps.
Shift + ArrowRightShift + ArrowDownMoves the slider position right (or down in vertical mode) by ten steps.
HomeMoves the slider to the minimum position (0%).
EndMoves the slider to the maximum position (100%).

Mouse and Touch Interactions

  • Drag: Click and drag the handle to adjust the comparison position
  • Click: Click anywhere on the slider container to jump to that position
  • Touch: Full touch support for mobile devices

Advanced Usage

Custom Content Types

The compare slider works with any React content, not just images:

<CompareSlider>
  <CompareSliderBefore>
    <div className="flex items-center justify-center bg-blue-500">
      <p>Old Design</p>
    </div>
  </CompareSliderBefore>
  <CompareSliderAfter>
    <div className="flex items-center justify-center bg-green-500">
      <p>New Design</p>
    </div>
  </CompareSliderAfter>
  <CompareSliderHandle />
</CompareSlider>

Vertical Orientation

Use vertical orientation for comparing content that works better in a vertical layout. The slider handle moves vertically, and the "before" content appears on top while "after" content appears on bottom.

<CompareSlider orientation="vertical" className="h-[600px]">
  <CompareSliderBefore>
    {/* Top content */}
  </CompareSliderBefore>
  <CompareSliderAfter>
    {/* Bottom content */}
  </CompareSliderAfter>
  <CompareSliderHandle />
</CompareSlider>

See the Vertical Orientation example for a complete demo.

Custom Labels

Add custom labels to identify each side:

<CompareSlider>
  <CompareSliderBefore label="Original">
    {/* Content */}
  </CompareSliderBefore>
  <CompareSliderAfter label="Enhanced">
    {/* Content */}
  </CompareSliderAfter>
  <CompareSliderHandle />
</CompareSlider>

Or use the CompareSliderLabel component for more control:

<CompareSlider>
  <CompareSliderBefore>
    {/* Content */}
  </CompareSliderBefore>
  <CompareSliderAfter>
    {/* Content */}
  </CompareSliderAfter>
  <CompareSliderHandle />
  <CompareSliderLabel side="before" className="bg-blue-500/90 text-white">
    Original
  </CompareSliderLabel>
  <CompareSliderLabel side="after" className="bg-green-500/90 text-white">
    Enhanced
  </CompareSliderLabel>
</CompareSlider>

Browser Support

Core Features

All core comparison features work in modern browsers:

  • Chrome/Edge: Full support
  • Firefox: Full support
  • Safari: Full support (iOS 13+)

Touch Gestures

Touch interactions require modern touch APIs:

  • iOS Safari: Supported from iOS 13+
  • Chrome Mobile: Full support
  • Firefox Mobile: Full support

Troubleshooting

Content Overflow

Ensure your content is properly contained within the slider:

<CompareSlider className="h-[400px] overflow-hidden">
  <CompareSliderBefore>
    <img className="size-full object-cover" src="..." />
  </CompareSliderBefore>
  {/* ... */}
</CompareSlider>

Performance with Large Images

For large images, consider:

  1. Using optimized image formats (WebP, AVIF)
  2. Lazy loading images
  3. Using appropriate image sizes for the display size
  4. Implementing image preloading for smoother transitions

Mobile Considerations

On mobile devices:

  • Ensure touch targets are adequately sized
  • Test on various screen sizes
  • Consider using vertical orientation for better mobile UX
  • Ensure content is responsive and scales appropriately

Credits