Or you can buy the following kits:
Disclosure: Some of the links provided in this section are Amazon affiliate links. We may receive a commission for any purchases made through these links at no additional cost to you.
Additionally, some of these links are for products from our own brand,
DIYables .
Follow these instructions step by step:
Connect the Arduino Uno R4/DIYables STEM V4 IoT board to your computer using a USB cable.
Launch the Arduino IDE on your computer.
Select the appropriate Arduino Uno R4 board (e.g., Arduino Uno R4 WiFi) and COM port.
Navigate to the Libraries icon on the left bar of the Arduino IDE.
Search "DIYables WebApps", then find the DIYables WebApps library by DIYables
Click Install button to install the library.


#include <DIYablesWebApps.h>
const char WIFI_SSID[] = "YOUR_WIFI_SSID";
const char WIFI_PASSWORD[] = "YOUR_WIFI_PASSWORD";
static UnoR4ServerFactory serverFactory;
DIYablesWebAppServer webAppsServer(serverFactory, 80, 81);
DIYablesHomePage homePage;
DIYablesWebJoystickPage webJoystickPage(false, 5);
int currentJoystickX = 0;
int currentJoystickY = 0;
void setup() {
Serial.begin(9600);
delay(1000);
Serial.println("DIYables WebApp - Web Joystick Example");
webAppsServer.addApp(&homePage);
webAppsServer.addApp(&webJoystickPage);
webAppsServer.setNotFoundPage(DIYablesNotFoundPage());
if (!webAppsServer.begin(WIFI_SSID, WIFI_PASSWORD)) {
while (1) {
Serial.println("Failed to start WebApp server!");
delay(1000);
}
}
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
currentJoystickX = x;
currentJoystickY = y;
Serial.println("Joystick - X: " + String(x) + ", Y: " + String(y));
}
});
webJoystickPage.onJoystickValueToWeb([]() {
webJoystickPage.sendToWebJoystick(currentJoystickX, currentJoystickY);
Serial.println("Web client requested values - Sent to Web: X=" + String(currentJoystickX) + ", Y=" + String(currentJoystickY));
});
}
void loop() {
webAppsServer.loop();
delay(10);
}
const char WIFI_SSID[] = "YOUR_WIFI_NETWORK";
const char WIFI_PASSWORD[] = "YOUR_WIFI_PASSWORD";
DIYables WebApp - Web Joystick Example
INFO: Added app /
INFO: Added app /web-joystick
DIYables WebApp Library
Platform: Arduino Uno R4 WiFi
Network connected!
IP address: 192.168.0.2
HTTP server started on port 80
Configuring WebSocket server callbacks...
WebSocket server started on port 81
WebSocket URL: ws://192.168.0.2:81
WebSocket server started on port 81
==========================================
DIYables WebApp Ready!
==========================================
📱 Web Interface: http://192.168.0.2
🔗 WebSocket: ws://192.168.0.2:81
📋 Available Applications:
🏠 Home Page: http://192.168.0.2/
🕹️ Web Joystick: http://192.168.0.2/web-joystick
==========================================
If you do not see anything, reboot Arduino board.
Take note of the IP address displayed, and enter this address into the address bar of a web browser on your smartphone or PC.
Example: http://192.168.0.2
You will see the home page like below image:
Or you can also access the page directly by IP address followed by /web-joystick. For example: http://192.168.0.2/web-joystick
Try controlling the virtual joystick by clicking and dragging on the joystick area and observe the X/Y coordinate values (-100 to +100) in the Serial Monitor.
The joystick can be configured with different parameters:
DIYablesWebJoystickPage webJoystickPage;
DIYablesWebJoystickPage webJoystickPage(false, 5);
The joystick interface provides:
Joystick Pad: Circular control area for touch/mouse input
Position Indicator: Shows current joystick position
Coordinate Display: Real-time X/Y values (-100 to +100)
Center Point: Visual reference for neutral position
Click and Drag: Click on joystick and drag to move
Release: Joystick returns to center (if autoReturn=true)
Click Position: Direct click to set joystick position
Touch and Drag: Touch joystick and drag finger to move
Multi-touch: Single finger for precise control
Release: Automatic return to center (if enabled)
The joystick provides coordinates in a standard Cartesian system:
X-axis: -100 (full left) to +100 (full right)
Y-axis: -100 (full down) to +100 (full up)
Center: X=0, Y=0 (neutral position)
Corners: Diagonal positions provide combined X/Y values
void setup() {
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
currentJoystickX = x;
currentJoystickY = y;
Serial.println("Joystick - X: " + String(x) + ", Y: " + String(y));
});
}
const int MOTOR_LEFT_PIN1 = 2;
const int MOTOR_LEFT_PIN2 = 3;
const int MOTOR_RIGHT_PIN1 = 4;
const int MOTOR_RIGHT_PIN2 = 5;
const int MOTOR_LEFT_PWM = 9;
const int MOTOR_RIGHT_PWM = 10;
void setup() {
pinMode(MOTOR_LEFT_PIN1, OUTPUT);
pinMode(MOTOR_LEFT_PIN2, OUTPUT);
pinMode(MOTOR_RIGHT_PIN1, OUTPUT);
pinMode(MOTOR_RIGHT_PIN2, OUTPUT);
pinMode(MOTOR_LEFT_PWM, OUTPUT);
pinMode(MOTOR_RIGHT_PWM, OUTPUT);
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
controlRobot(x, y);
});
}
void controlRobot(int x, int y) {
int leftSpeed = y + x;
int rightSpeed = y - x;
leftSpeed = constrain(leftSpeed, -100, 100);
rightSpeed = constrain(rightSpeed, -100, 100);
if (leftSpeed > 0) {
digitalWrite(MOTOR_LEFT_PIN1, HIGH);
digitalWrite(MOTOR_LEFT_PIN2, LOW);
analogWrite(MOTOR_LEFT_PWM, map(leftSpeed, 0, 100, 0, 255));
} else if (leftSpeed < 0) {
digitalWrite(MOTOR_LEFT_PIN1, LOW);
digitalWrite(MOTOR_LEFT_PIN2, HIGH);
analogWrite(MOTOR_LEFT_PWM, map(-leftSpeed, 0, 100, 0, 255));
} else {
digitalWrite(MOTOR_LEFT_PIN1, LOW);
digitalWrite(MOTOR_LEFT_PIN2, LOW);
analogWrite(MOTOR_LEFT_PWM, 0);
}
if (rightSpeed > 0) {
digitalWrite(MOTOR_RIGHT_PIN1, HIGH);
digitalWrite(MOTOR_RIGHT_PIN2, LOW);
analogWrite(MOTOR_RIGHT_PWM, map(rightSpeed, 0, 100, 0, 255));
} else if (rightSpeed < 0) {
digitalWrite(MOTOR_RIGHT_PIN1, LOW);
digitalWrite(MOTOR_RIGHT_PIN2, HIGH);
analogWrite(MOTOR_RIGHT_PWM, map(-rightSpeed, 0, 100, 0, 255));
} else {
digitalWrite(MOTOR_RIGHT_PIN1, LOW);
digitalWrite(MOTOR_RIGHT_PIN2, LOW);
analogWrite(MOTOR_RIGHT_PWM, 0);
}
}
#include <Servo.h>
Servo panServo;
Servo tiltServo;
void setup() {
panServo.attach(9);
tiltServo.attach(10);
panServo.write(90);
tiltServo.write(90);
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
controlServos(x, y);
});
}
void controlServos(int x, int y) {
int panAngle = map(x, -100, 100, 0, 180);
int tiltAngle = map(y, -100, 100, 180, 0);
panServo.write(panAngle);
tiltServo.write(tiltAngle);
Serial.println("Pan: " + String(panAngle) + "°, Tilt: " + String(tiltAngle) + "°");
}
const int LED_UP = 2;
const int LED_DOWN = 3;
const int LED_LEFT = 4;
const int LED_RIGHT = 5;
const int LED_CENTER = 13;
void setup() {
pinMode(LED_UP, OUTPUT);
pinMode(LED_DOWN, OUTPUT);
pinMode(LED_LEFT, OUTPUT);
pinMode(LED_RIGHT, OUTPUT);
pinMode(LED_CENTER, OUTPUT);
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
updateLEDIndicators(x, y);
});
}
void updateLEDIndicators(int x, int y) {
digitalWrite(LED_UP, LOW);
digitalWrite(LED_DOWN, LOW);
digitalWrite(LED_LEFT, LOW);
digitalWrite(LED_RIGHT, LOW);
digitalWrite(LED_CENTER, LOW);
if (abs(x) < 10 && abs(y) < 10) {
digitalWrite(LED_CENTER, HIGH);
return;
}
if (y > 20) digitalWrite(LED_UP, HIGH);
if (y < -20) digitalWrite(LED_DOWN, HIGH);
if (x > 20) digitalWrite(LED_RIGHT, HIGH);
if (x < -20) digitalWrite(LED_LEFT, HIGH);
}
void setup() {
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
handleJoystickInput(x, y);
});
webJoystickPage.setAutoReturn(false);
webJoystickPage.setSensitivity(10.0);
}
void handleJoystickInput(int x, int y) {
static bool precisionMode = false;
if (abs(x) > 95 && abs(y) > 95) {
precisionMode = !precisionMode;
if (precisionMode) {
webJoystickPage.setSensitivity(1.0);
Serial.println("Precision mode ON");
} else {
webJoystickPage.setSensitivity(10.0);
Serial.println("Precision mode OFF");
}
}
}
void processJoystickWithDeadZone(int x, int y) {
const int DEAD_ZONE = 15;
int filteredX = (abs(x) < DEAD_ZONE) ? 0 : x;
int filteredY = (abs(y) < DEAD_ZONE) ? 0 : y;
if (filteredX != 0) {
filteredX = map(abs(filteredX), DEAD_ZONE, 100, 0, 100);
filteredX = (x < 0) ? -filteredX : filteredX;
}
if (filteredY != 0) {
filteredY = map(abs(filteredY), DEAD_ZONE, 100, 0, 100);
filteredY = (y < 0) ? -filteredY : filteredY;
}
controlDevice(filteredX, filteredY);
}
class SpeedController {
private:
int targetX = 0, targetY = 0;
int currentX = 0, currentY = 0;
unsigned long lastUpdate = 0;
const int RAMP_RATE = 5;
public:
void setTarget(int x, int y) {
targetX = x;
targetY = y;
}
void update() {
if (millis() - lastUpdate > 20) {
if (currentX < targetX) {
currentX = min(currentX + RAMP_RATE, targetX);
} else if (currentX > targetX) {
currentX = max(currentX - RAMP_RATE, targetX);
}
if (currentY < targetY) {
currentY = min(currentY + RAMP_RATE, targetY);
} else if (currentY > targetY) {
currentY = max(currentY - RAMP_RATE, targetY);
}
applyControlValues(currentX, currentY);
lastUpdate = millis();
}
}
void applyControlValues(int x, int y) {
Serial.println("Ramped - X: " + String(x) + ", Y: " + String(y));
}
};
SpeedController speedController;
void setup() {
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
speedController.setTarget(x, y);
});
}
void loop() {
server.loop();
speedController.update();
}
void setupRobotCar() {
pinMode(2, OUTPUT);
pinMode(3, OUTPUT);
pinMode(4, OUTPUT);
pinMode(5, OUTPUT);
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
int leftMotor = y + (x / 2);
int rightMotor = y - (x / 2);
leftMotor = constrain(leftMotor, -100, 100);
rightMotor = constrain(rightMotor, -100, 100);
setMotorSpeed(9, 2, 3, leftMotor);
setMotorSpeed(10, 4, 5, rightMotor);
});
}
void setMotorSpeed(int pwmPin, int dir1Pin, int dir2Pin, int speed) {
if (speed > 0) {
digitalWrite(dir1Pin, HIGH);
digitalWrite(dir2Pin, LOW);
analogWrite(pwmPin, map(speed, 0, 100, 0, 255));
} else if (speed < 0) {
digitalWrite(dir1Pin, LOW);
digitalWrite(dir2Pin, HIGH);
analogWrite(pwmPin, map(-speed, 0, 100, 0, 255));
} else {
digitalWrite(dir1Pin, LOW);
digitalWrite(dir2Pin, LOW);
analogWrite(pwmPin, 0);
}
}
#include <Servo.h>
Servo panServo, tiltServo;
int panOffset = 90, tiltOffset = 90;
void setupCameraGimbal() {
panServo.attach(9);
tiltServo.attach(10);
panServo.write(panOffset);
tiltServo.write(tiltOffset);
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
int panPos = panOffset + map(x, -100, 100, -45, 45);
int tiltPos = tiltOffset + map(y, -100, 100, -30, 30);
panPos = constrain(panPos, 0, 180);
tiltPos = constrain(tiltPos, 0, 180);
panServo.write(panPos);
tiltServo.write(tiltPos);
});
}
const int RED_PIN = 9;
const int GREEN_PIN = 10;
const int BLUE_PIN = 11;
void setupRGBControl() {
pinMode(RED_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
pinMode(BLUE_PIN, OUTPUT);
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
int red = map(abs(x), 0, 100, 0, 255);
int green = map(abs(y), 0, 100, 0, 255);
int blue = map(abs(x + y), 0, 141, 0, 255);
if (x > 0 && y > 0) {
blue = 0;
} else if (x < 0 && y > 0) {
red = 0;
} else if (x < 0 && y < 0) {
green = 0;
} else if (x > 0 && y < 0) {
green = blue = 0;
}
analogWrite(RED_PIN, red);
analogWrite(GREEN_PIN, green);
analogWrite(BLUE_PIN, blue);
});
}
1. Joystick not responding
Check WebSocket connection status in browser console
Verify network connectivity
Refresh browser page
Check Serial Monitor for errors
2. Jerky or inconsistent movement
Increase sensitivity value to reduce update frequency
Implement dead zone filtering
Add speed ramping for smooth transitions
Check for network latency issues
3. Auto-return not working
Verify autoReturn setting: webJoystickPage.setAutoReturn(true)
Check browser compatibility (some touch devices vary)
Test with different input methods (mouse vs touch)
4. Values not reaching full range
Check joystick calibration in web interface
Verify coordinate calculations in callback
Test with different browser/device combinations
Add comprehensive debugging:
void debugJoystickState(int x, int y) {
Serial.println("=== Joystick Debug ===");
Serial.println("X: " + String(x) + " (" + String(map(x, -100, 100, 0, 100)) + "%)");
Serial.println("Y: " + String(y) + " (" + String(map(y, -100, 100, 0, 100)) + "%)");
Serial.println("Distance from center: " + String(sqrt(x*x + y*y)));
Serial.println("Angle: " + String(atan2(y, x) * 180 / PI) + "°");
Serial.println("=====================");
}
Smart curtain control (open/close/position)
Camera pan/tilt control
Light brightness and color control
Fan speed and direction control
Coordinate system teaching tool
Motor control demonstrations
Servo positioning experiments
Gaming controller interfaces
LED matrix pattern control
Music visualization control
Drawing robot control
Interactive art installations
Use joystick for direction and sliders for speed/intensity:
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
int maxSpeed = getSliderValue();
int scaledX = map(x, -100, 100, -maxSpeed, maxSpeed);
int scaledY = map(y, -100, 100, -maxSpeed, maxSpeed);
controlRobot(scaledX, scaledY);
});
Use joystick positions to trigger digital pin states:
webJoystickPage.onJoystickValueFromWeb([](int x, int y) {
webDigitalPinsPage.setPinState(2, x > 50);
webDigitalPinsPage.setPinState(3, x < -50);
webDigitalPinsPage.setPinState(4, y > 50);
webDigitalPinsPage.setPinState(5, y < -50);
});
After mastering the WebJoystick example, try:
WebSlider - For additional analog control
WebDigitalPins - For discrete on/off control
WebMonitor - For debugging joystick values
MultipleWebApps - Combining joystick with other controls
For additional help: