Joystick

Joystick is a two-axis positional input. In its default circle shape it behaves like a physical stick — the handle rides the rim at full deflection and springs back to centre when released. As a square it becomes an XY pad: each axis clamps on its own so the corners are reachable, and the handle stays where it is left.
Both axes are normalized to −1…1. y is up-positive by default, matching how a gamepad axis reads; pass invertY={false} to follow screen space instead.
deadZone zeroes small deflections and rescales what is left, so the value still spans the full range past the threshold rather than jumping to the dead-zone size. step snaps each axis, lockAxis restricts travel to one direction, and showCrosshair adds accent rules that track the handle — the usual XY-pad readout.
The gesture runs on the shared useDragGesture hook, which means a drag that leaves the pad keeps tracking the finger instead of handing the touch back to the page. Arrow keys nudge by keyboardStep on web, Home and Escape recentre, and VoiceOver/TalkBack get increment and decrement actions.

Basic

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

 

'@platform-blocks/react-ui-library'

;

 

export

 

function

 

Demo

(

)

 

{

  

const

 

[

value

,

setValue

]

 

=

 

useState

(

{

x

:

 

0

,

y

:

 

0

 

}

)

;

 

  

return

 

(

    

<Joystick

      

value

=

{

value

}

      

onChange

=

{

setValue

}

      showCrosshair

      valueLabel

    

/>

  

)

;

}

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

 

'@platform-blocks/react-ui-library'

;

 

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

=

"dimmed"

>

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

 

'@platform-blocks/react-ui-library'

;

 

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

 

'@platform-blocks/react-ui-library'

;

 

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

=

"dimmed"

>

{

position

}

</Text

>

    

</Flex

>

  

)

;

}

</>

react-ui-library

100+ accessible, themeable components that work seamlessly across iOS, Android, and Web.

A Platform Blocks product.