Virtual joysticks for the modern web
// zero deps · typescript · touch & mouse
Get Started →
npm i nipplejs
// zero deps · typescript · touch & mouse
import nipplejs from 'nipplejs';
const manager = nipplejs.create({
zone: document.getElementById('zone'),
mode: 'static',
position: { left: '50%', top: '50%' },
// color accepts a string or { front, back } object.
// Values use CSS 'background', so gradients work.
color: {
front: 'linear-gradient(135deg, #34d399, #10b981)',
back: 'rgba(16, 185, 129, 0.15)',
},
});
manager.on('move', (evt) => {
const { vector } = evt.data;
// vector.x: -1 (left) to 1 (right)
// vector.y: -1 (down) to 1 (up)
player.x += vector.x * speed;
player.y -= vector.y * speed;
});
manager.on('end', () => {
// Stop moving
});Collect orbs to grow your glowing snake.
import nipplejs from 'nipplejs';
const manager = nipplejs.create({
zone: document.getElementById('zone'),
mode: 'static',
position: { left: '50%', bottom: '15%' },
lockX: true, // Horizontal movement only
color: {
front: 'linear-gradient(135deg, #38bdf8, #0ea5e9)',
back: 'rgba(56, 189, 248, 0.12)',
},
});
manager.on('move', (evt) => {
const { vector } = evt.data;
// Only vector.x changes — lockX freezes Y
ship.x += vector.x * speed;
});Dodge falling asteroids with horizontal-only movement.
import nipplejs from 'nipplejs';
// Left stick — movement
const moveStick = nipplejs.create({
zone: document.getElementById('left-zone'),
mode: 'static',
position: { left: '20%', bottom: '20%' },
// Layer an icon over a gradient background
color: {
front: 'url("move.svg") center/75% no-repeat, ' +
'linear-gradient(135deg, #818cf8, #38bdf8)',
back: 'rgba(99, 102, 241, 0.12)',
},
});
// Right stick — aim & shoot
const aimStick = nipplejs.create({
zone: document.getElementById('right-zone'),
mode: 'static',
position: { left: '80%', bottom: '20%' },
color: {
front: 'url("shoot.svg") center/75% no-repeat, ' +
'linear-gradient(135deg, #e879f9, #ec4899)',
back: 'rgba(236, 72, 153, 0.12)',
},
});
moveStick.on('move', (evt) => {
player.x += evt.data.vector.x * speed;
player.y -= evt.data.vector.y * speed;
});
aimStick.on('move', (evt) => {
aimAngle = evt.data.angle.radian;
fireProjectile(aimAngle);
});Move and shoot with two joysticks. Best on mobile.
import nipplejs from 'nipplejs';
const manager = nipplejs.create({
zone: document.getElementById('zone'),
mode: 'static',
position: { left: '50%', bottom: '15%' },
follow: true, // Base follows your thumb
restOpacity: 0.8, // Keep scope visible at rest
// color uses CSS 'background' — images, gradients, anything
color: {
front: 'url("scope.svg") center/cover',
back: 'radial-gradient(circle, ' +
'rgba(56,189,248,0.06) 0%, ' +
'rgba(56,189,248,0.1) 68%, ' +
'rgba(56,189,248,0.3) 68%)',
},
});
manager.on('move', (evt) => {
const { vector, baseDelta } = evt.data;
// vector: fine aim within joystick radius
crosshair.x = vector.x * aimRange;
crosshair.y = -vector.y * aimRange;
// baseDelta: camera pan when dragging beyond radius
camera.x += baseDelta.x * panSpeed;
camera.y -= baseDelta.y * panSpeed;
});Aim within the joystick, pan the sky by pushing beyond — two controls, one thumb.
import nipplejs from 'nipplejs';
const manager = nipplejs.create({
zone: document.getElementById('zone'),
mode: 'static',
position: { left: '50%', bottom: '15%' },
restJoystick: false, // Stays where you leave it
color: {
front: 'linear-gradient(135deg, #a78bfa, #e879f9)',
back: 'rgba(167, 139, 250, 0.12)',
},
});
let velocity = { x: 0, y: 0 };
manager.on('move', (evt) => {
const { vector } = evt.data;
// Set velocity — persists after release
velocity.x = vector.x * speed;
velocity.y = -vector.y * speed;
});
// No 'end' handler needed —
// restJoystick: false keeps the last input
// so velocity naturally persists
function gameLoop() {
ship.x += velocity.x;
ship.y += velocity.y;
requestAnimationFrame(gameLoop);
}Set your heading and drift through space.