Geometa
Components

2D MD Slider Input

2D multi-dimensional slider with circular range and canvas performance widget.

The 2D MD Slider Input is a geometry-focused control for exploring values in a normalised 2D space. It lets you pick an ((x, y)) point where both coordinates are in the range ([0, 1]) — left-to-right for (x), bottom-to-top for (y).

The slider uses a canvas-backed square widget for smooth rendering, with a handle that tracks the current ((x, y)) position.

0
100
100
0
X:50
Y:50

Installation (shadcn-style CLI)

These commands assume you will expose Geometa components through a shadcn-style CLI. Adjust the exact names once your CLI is wired up.

# React / Next.js
npx shadcn@latest add geometa-2d-md-slider

# Svelte
npx shadcn-svelte@latest add geometa-2d-md-slider

Usage – React (Next.js)

The example below shows the 2D slider backed by React state, exposing the current ((x, y)) value in ([0, 1] \times [0, 1]).

'use client';

import * as React from 'react';

type Vec2 = { x: number; y: number };

interface TwoDSliderProps {
  radius?: number;
  onChange?(value: Vec2): void;
}

export function TwoDSlider({ radius = 80, onChange }: TwoDSliderProps) {
  const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
  const [angle, setAngle] = React.useState(0);

  const position: Vec2 = React.useMemo(
    () => ({
      x: Math.cos(angle) * radius,
      y: Math.sin(angle) * radius,
    }),
    [angle, radius],
  );

  React.useEffect(() => {
    if (onChange) onChange(position);
  }, [position, onChange]);

  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const dpr = window.devicePixelRatio ?? 1;
    const size = radius * 2 + 32;

    canvas.width = size * dpr;
    canvas.height = size * dpr;
    canvas.style.width = `${size}px`;
    canvas.style.height = `${size}px`;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

    ctx.clearRect(0, 0, size, size);

    // circle
    ctx.strokeStyle = '#888';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.arc(size / 2, size / 2, radius, 0, Math.PI * 2);
    ctx.stroke();

    // handle
    ctx.fillStyle = '#22c55e';
    ctx.beginPath();
    ctx.arc(size / 2 + position.x, size / 2 + position.y, 6, 0, Math.PI * 2);
    ctx.fill();
  }, [position, radius]);

  const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
    const rect = event.currentTarget.getBoundingClientRect();
    const cx = rect.left + rect.width / 2;
    const cy = rect.top + rect.height / 2;
    const dx = event.clientX - cx;
    const dy = event.clientY - cy;
    const nextAngle = Math.atan2(dy, dx);
    setAngle(nextAngle);
  };

  const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
    event.currentTarget.setPointerCapture(event.pointerId);
    handlePointerMove(event);
  };

  const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
    event.currentTarget.releasePointerCapture(event.pointerId);
  };

  return (
    <div className="flex flex-col gap-3">
      <div
        className="relative inline-flex items-center justify-center rounded-full bg-neutral-900/40"
        style={{ width: radius * 2 + 32, height: radius * 2 + 32 }}
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
      >
        <canvas ref={canvasRef} className="pointer-events-none" />
      </div>

      <div className="text-xs text-muted-foreground">
        Angle: <span className="font-mono">{angle.toFixed(2)} rad</span>{' '}
        | x:{' '}
        <span className="font-mono">
          {position.x.toFixed(1)}
        </span>{' '}
        y:{' '}
        <span className="font-mono">
          {position.y.toFixed(1)}
        </span>
      </div>
    </div>
  );
}

On this page