Joystick

Joystick captures two-axis input as a spring-return stick or persistent XY pad.

Basics

A stick that springs back to centre on release. Drag anywhere on the pad — the gesture keeps tracking even when the finger leaves it.

Loading demo…

import { useState } from 'react'; import { Joystick } from '@plocks/ui'; export function Demo() { const [value, setValue] = useState({ x: 0, y: 0 }); return ( <Joystick accessibilityLabel="Joystick" value={value} onChange={setValue} showCrosshair valueLabel /> ); }

Variants

Compare the joystick presets, including the unstyled base for custom skins.

Loading demo…

import { Column, Flex, Joystick, Text } from '@plocks/ui'; const variants = ['default', 'filled', 'outline', 'minimal', 'unstyled'] as const; export function Demo() { return ( <Flex direction="row" gap="lg" wrap="wrap"> {variants.map(variant => ( <Column key={variant} gap="xs" align="center"> <Text size="sm">{variant}</Text> <Joystick variant={variant} accessibilityLabel={`${variant} joystick`} /> </Column> ))} </Flex> ); }

XY pad

shape="square" clamps each axis independently so the corners are reachable, and the handle holds its position on release — the shape a filter or effect pad wants.

Loading demo…

import { useState } from 'react'; import { Flex, Joystick, Text } from '@plocks/ui'; export function Demo() { const [value, setValue] = useState({ x: -0.4, y: 0.6 }); // Map the pad onto a pair of parameters the way an effect unit would. const cutoff = Math.round(((value.x + 1) / 2) * 18000 + 200); const resonance = ((value.y + 1) / 2).toFixed(2); return ( <Flex direction="column" gap="md"> <Joystick shape="square" size="lg" value={value} onChange={setValue} showCrosshair label="Filter" /> <Text size="sm" c="muted">Cutoff {cutoff} Hz · Resonance {resonance}</Text> </Flex> ); }

Dead zone and steps

deadZone ignores small deflections around centre and rescales the rest, so full travel still reports 1. step snaps each axis onto a grid.

Loading demo…

import { useState } from 'react'; import { Flex, Joystick } from '@plocks/ui'; export function Demo() { const [free, setFree] = useState({ x: 0, y: 0 }); const [stepped, setStepped] = useState({ x: 0, y: 0 }); return ( <Flex gap="xl" wrap="wrap"> <Joystick label="Dead zone 0.25" deadZone={0.25} value={free} onChange={setFree} valueLabel /> <Joystick label="Step 0.25" shape="square" step={0.25} returnToCenter={false} value={stepped} onChange={setStepped} showCrosshair valueLabel /> </Flex> ); }

Axis lock

lockAxis restricts travel to one direction. A single-axis pad also leaves the perpendicular direction to the page, so vertical scrolling still works over a horizontal control.

Loading demo…

import { useState } from 'react'; import { Flex, Joystick, Text } from '@plocks/ui'; export function Demo() { const [pan, setPan] = useState({ x: 0, y: 0 }); const position = pan.x === 0 ? 'Center' : `${pan.x < 0 ? 'L' : 'R'} ${Math.round(Math.abs(pan.x) * 100)}`; return ( <Flex direction="column" gap="md" align="flex-start"> <Joystick label="Pan" lockAxis="x" size="sm" value={pan} onChange={setPan} /> <Text size="sm" c="muted">{position}</Text> </Flex> ); }
Scan to open this page on your phoneplocks.dev/ui/Joystick