In this tutorial you’ll build a web server to control the ESP32 or ESP8266 NodeMCU outputs with a pulse using Arduino IDE. The pulse width (“timer”) can be adjusted using a slider on the web page. When you click the ON button, the ESP sets the output state to HIGH for the number of seconds defined in the slider. This can be specially useful to control appliances that need a HIGH signal for a predetermined number of seconds to actuate.

The ESP32/ESP8266 boards will be programmed using Arduino IDE. So make sure you have these boards installed:
The following image shows an overview of how this project works.

To build the web server you need to install the following libraries:
You can install those libraries in the Arduino IDE Library Manager. Go to Sketch > Include Library > Manage Libraries and search for the libraries’ names.
Copy the following code to your Arduino IDE.
/********* Rui Santos & Sara Santos - Random Nerd Tutorials Complete project details at https://RandomNerdTutorials.com/esp32-esp8266-web-server-timer-pulse/ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. *********/ // Import required libraries #ifdef ESP32 #include <WiFi.h> #include <AsyncTCP.h> #else #include <ESP8266WiFi.h> #include <ESPAsyncTCP.h> #endif #include <ESPAsyncWebServer.h> // Replace with your network credentials const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD"; const char* PARAM_INPUT_1 = "state"; const char* PARAM_INPUT_2 = "value"; const int output = 2; String timerSliderValue = "10"; // Create AsyncWebServer object on port 80 AsyncWebServer server(80); const char index_html[] PROGMEM = R"rawliteral( <!DOCTYPE HTML><html> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>ESP Web Server</title> <style> html {font-family: Arial; display: inline-block; text-align: center;} h2 {font-size: 2.4rem;} p {font-size: 2.2rem;} body {max-width: 600px; margin:0px auto; padding-bottom: 25px;} .switch {position: relative; display: inline-block; width: 120px; height: 68px} .switch input {display: none} .slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 34px} .slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 68px} input:checked+.slider {background-color: #2196F3} input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)} .slider2 { -webkit-appearance: none; margin: 14px; width: 300px; height: 20px; background: #ccc; outline: none; -webkit-transition: .2s; transition: opacity .2s;} .slider2::-webkit-slider-thumb {-webkit-appearance: none; appearance: none; width: 30px; height: 30px; background: #2f4468; cursor: pointer;} .slider2::-moz-range-thumb { width: 30px; height: 30px; background: #2f4468; cursor: pointer; } </style> </head> <body> <h2>ESP Web Server</h2> <p><span id="timerValue">%TIMERVALUE%</span> s</p> <p><input type="range" onchange="updateSliderTimer(this)" id="timerSlider" min="1" max="20" value="%TIMERVALUE%" step="1" class="slider2"></p> %BUTTONPLACEHOLDER% <script> function toggleCheckbox(element) { var sliderValue = document.getElementById("timerSlider").value; var xhr = new XMLHttpRequest(); if(element.checked){ xhr.open("GET", "/update?state=1", true); xhr.send(); var count = sliderValue, timer = setInterval(function() { count--; document.getElementById("timerValue").innerHTML = count; if(count == 0){ clearInterval(timer); document.getElementById("timerValue").innerHTML = document.getElementById("timerSlider").value; } }, 1000); sliderValue = sliderValue*1000; setTimeout(function(){ xhr.open("GET", "/update?state=0", true); document.getElementById(element.id).checked = false; xhr.send(); }, sliderValue); } } function updateSliderTimer(element) { var sliderValue = document.getElementById("timerSlider").value; document.getElementById("timerValue").innerHTML = sliderValue; var xhr = new XMLHttpRequest(); xhr.open("GET", "/slider?value="+sliderValue, true); xhr.send(); } </script> </body> </html> )rawliteral"; // Replaces placeholder with button section in your web page String processor(const String& var){ //Serial.println(var); if(var == "BUTTONPLACEHOLDER"){ String buttons = ""; String outputStateValue = outputState(); buttons+= "<p><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"output\" " + outputStateValue + "><span class=\"slider\"></span></label></p>"; return buttons; } else if(var == "TIMERVALUE"){ return timerSliderValue; } return String(); } String outputState(){ if(digitalRead(output)){ return "checked"; } else { return ""; } return ""; } void setup(){ // Serial port for debugging purposes Serial.begin(115200); pinMode(output, OUTPUT); digitalWrite(output, LOW); // Connect to Wi-Fi WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi.."); } // Print ESP Local IP Address Serial.println(WiFi.localIP()); // Route for root / web page server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/html", index_html, processor); }); // Send a GET request to <ESP_IP>/update?state=<inputMessage> server.on("/update", HTTP_GET, [] (AsyncWebServerRequest *request) { String inputMessage; // GET input1 value on <ESP_IP>/update?state=<inputMessage> if (request->hasParam(PARAM_INPUT_1)) { inputMessage = request->getParam(PARAM_INPUT_1)->value(); digitalWrite(output, inputMessage.toInt()); } else { inputMessage = "No message sent"; } Serial.println(inputMessage); request->send(200, "text/plain", "OK"); }); // Send a GET request to <ESP_IP>/slider?value=<inputMessage> server.on("/slider", HTTP_GET, [] (AsyncWebServerRequest *request) { String inputMessage; // GET input1 value on <ESP_IP>/slider?value=<inputMessage> if (request->hasParam(PARAM_INPUT_2)) { inputMessage = request->getParam(PARAM_INPUT_2)->value(); timerSliderValue = inputMessage; } else { inputMessage = "No message sent"; } Serial.println(inputMessage); request->send(200, "text/plain", "OK"); }); // Start server server.begin(); } void loop() { }
You just need to enter your network credentials (SSID and password) and the web server will work straight away. The code is compatible with both the ESP32 and ESP8266 boards and controls the on-board LED GPIO 2 – you can change the code to control any other GPIO.
We’ve already explained in great details how web servers like this work in previous tutorials (DHT Temperature Web Server or Relay Web Server), so we’ll just take a look at the relevant parts for this project.
As said previously, insert your network credentials in the following lines:
const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Above the slider, there’s a number showing the current slider value.
<p><span id="timerValue">%TIMERVALUE%</span> s</p>
By default, the slider value is set to the %TIMERVALUE% placeholder.
The %TIMERVALUE% is a placeholder that will be replaced with the value stored in the timerSliderValue variable which is set to 10 by default. But you can change that in the following line:
String timerSliderValue = "10";
This will also be changed when you move the slider. When the slider is moved, it calls a JavaScript function that updates the slider value.
The following line creates the slider.
<input type="range" onchange="updateSliderTimer(this)" id="timerSlider" min="1" max="20" value="%TIMERVALUE%" step="1" class="slider2">Let’s break this down into smaller sections.
In HTML, a slider is an input type. The <input> tag specifies an input field where the user can enter data. The slider is an input field of type range. There are many other input field types.
<input type="range">The default range of the slider is 0 to 100. You can use the following attributes to customize the slider settings:
<input type="range" onchange="updateSliderTimer(this)" id="timerSlider" min="1" max="20" value="%TIMERVALUE%" step="1" class="slider2">The %TIMERVALUE% is a placeholder that will be replaced with an actual value. In the code, it will be replaced with the value of the timerSliderValue variable that is set to 10 by default. But you can change that in the following line:
String timerSliderValue = "10";
The slider has two more attributes: id and onchange.
When you move the slider, the updateSliderTimer() function is executed.
It gets the current slider value by referring to its id timerSlider:
var sliderValue = document.getElementById("timerSlider").value;
Updates the slider label to the current slider value by referring to its id timerValue:
document.getElementById("timerValue").innerHTML = sliderValue;
Then, it makes a request on the /slider?value=sliderValue URL. Where the sliderValue is equal to the current slider value.
Then, the ESP32/ESP8266 handles what happens when it receives a request on that URL.
When you click the ON/OFF button to control the output, it calls the toogleCheckbox() JavaScript function.
This function gets the current value of the slider label:
var sliderValue = document.getElementById("timerSlider").value;
Makes a request on the /update?state=1 URL so that the ESP knows it needs to set the output to HIGH.
if(element.checked){ xhr.open("GET", "/update?state=1", true); xhr.send();
The following lines decrease the slider label value every second creating the countdown timer.
var count = sliderValue, timer = setInterval(function() { count--; document.getElementById("timerValue").innerHTML = count; if(count == 0){ clearInterval(timer); document.getElementById("timerValue").innerHTML = document.getElementById("timerSlider").value; } }, 1000);
When the timer hits zero, the label value gets back to its original value and a request is made on the /update?state=0 URL, so that the ESP knows it is time to set the output to LOW. The button on the web server gets back to the off state.
setTimeout(function(){ xhr.open("GET", "/update?state=0", true); document.getElementById(element.id).checked = false; xhr.send(); }, sliderValue);
The ESP32/ESP8266 needs to handle what happens when it receives a request on a certain URL.
When you access the root URL /, send the HTML text saved on the index_html variable. All placeholders are replaced with the actual values by the processor() function.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", index_html, processor); });
The following lines handle what happens when you receive a request on the /update?state=1 and /update?state=0 URLs. It sets the output state to HIGH or LOW accordingly.
server.on("/update", HTTP_GET, [] (AsyncWebServerRequest *request) { String inputMessage; // GET input1 value on <ESP_IP>/update?state=<inputMessage> if (request->hasParam(PARAM_INPUT_1)) { inputMessage = request->getParam(PARAM_INPUT_1)->value(); digitalWrite(output, inputMessage.toInt()); } else { inputMessage = "No message sent"; } Serial.println(inputMessage); request->send(200, "text/plain", "OK"); });
The output state is updated in the following line:
digitalWrite(output, inputMessage.toInt());
Every time you drag the slider, the ESP receives a request with the new value. We store the new slider value and print it in the Serial Monitor.
// Send a GET request to <ESP_IP>/slider?value=<inputMessage> server.on("/slider", HTTP_GET, [] (AsyncWebServerRequest *request) { String inputMessage; // GET input1 value on <ESP_IP>/slider?value=<inputMessage> if (request->hasParam(PARAM_INPUT_2)) { inputMessage = request->getParam(PARAM_INPUT_2)->value(); timerSliderValue = inputMessage; } else { inputMessage = "No message sent"; } Serial.println(inputMessage); request->send(200, "text/plain", "OK"); });
Upload the code to your ESP32 or ESP8266 NodeMCU board. Then, open the Serial Monitor and press the on-board RST/EN button to get is IP address.
Open a browser in your local network and type the ESP IP address. The following page should load.

Drag the slider to adjust the pulse width, and then, click the ON/OFF button. The output (in this case GPIO 2 – built-in LED) will stay on for the period of time you’ve set on the slider.

Copyright ©2025. All Rights Reserved Emblab THE RAVE INNOVATION