.doc_for_ai/TASMOTA_WEBUI_CODING_GUIDE.md
Source Verification: This guide is based on actual Tasmota source code analysis (v15.0.1.4). All code examples are extracted from verified source files in the Tasmota repository.
Tasmota's WebUI is a lightweight, embedded web interface designed for ESP8266/ESP32 microcontrollers with severe memory constraints. The interface is generated dynamically by C++ code and follows a minimalist design philosophy optimized for embedded systems.
The WebUI is implemented across these key source files:
tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino - Main web server implementation
tasmota/html_uncompressed/ - Uncompressed HTML, CSS, and JavaScript templates
HTTP_HEADER1_ES6.h - Main HTML template with JavaScript functionsHTTP_HEAD_STYLE1.h - Basic CSS styles (forms, inputs, layout)HTTP_HEAD_STYLE2.h - Button and utility CSS stylesHTTP_HEAD_STYLE3.h - Page structure and container stylesHTTP_SCRIPT_*.h - Various JavaScript modulestasmota/include/tasmota_globals.h - Default color constant definitions (light theme)tasmota/my_user_config.h - Default color theme configuration (dark theme by default)USE_UNISHOX_COMPRESSION is defined, the build system uses compressed versions from html_compressed/ directoryHTTP_HEADER1_ES6.h and HTTP_HEAD_STYLE3.h)<!DOCTYPE html>
<html lang="%s" class="">
<head>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="icon" href="data:image/x-icon;base64,AAABAAEAEBACAAEAAQCwAAAAFgAAACgAAAAQAAAAIAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AP5/b+H6X2/h8k9v4eZnb+Hud2/h7ndv4e53b+FmZm/hMkxv4ZgZb+HOc2/h5+dv4fPPb+H5n2/h/D9v4f5/b+EAAO4EAADuBAAA7gQAAO4EAADuBAAA7gQAAO4EAADuBAAA7gQAAO4EAADuBAAA7gQAAO4EAADuBAAA7gQAAO4E">
<title>%s %s</title>
<script>
// Core JavaScript functions (see below)
</script>
<style>
/* CSS styles (see below) */
</style>
</head>
<body>
<div style='background:var(--c_bg);text-align:left;display:inline-block;color:var(--c_txt);min-width:340px;position:relative;'>
<div style='text-align:center;color:var(--c_ttl);'><noscript>%s
</noscript>
<h3>%s</h3> <!-- Module name -->
<h2>%s</h2> <!-- Device name -->
<!-- Page content -->
</div>
</body>
</html>
min-width:340px, position:relative)<noscript> fallbackid='l1')WSContentEnd() in xdrv_01_9_webserver.ino)The HTML is generated dynamically by the C++ backend using these key functions from xdrv_01_9_webserver.ino:
// Start HTML content
void WSContentStart_P(const char* title) {
WiFiClient httpClient = Webserver->client();
httpClient.print(F("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n"));
}
// Send CSS styles
void WSContentSendStyle() {
WSContentSend_P(PSTR("<style>"));
WSContentSend_P(HTTP_HEAD_STYLE1);
WSContentSend_P(HTTP_HEAD_STYLE2);
// ... more styles
WSContentSend_P(PSTR("</style>"));
}
// End HTML content
void WSContentEnd() {
WSContentSend_P(PSTR("</div></body></html>"));
}
The template uses sprintf_P style formatting with placeholders:
%s for language, device name, version{t}, {s}, {m}, {e} for table generationTasmota uses CSS custom properties for consistent theming. The default dark theme colors are defined in my_user_config.h:
:root {
/* Dark theme (default) - Verified from my_user_config.h */
--c_bg: #252525; /* COLOR_BACKGROUND - Global background color */
--c_frm: #4f4f4f; /* COLOR_FORM - Form/fieldset background */
--c_ttl: #eaeaea; /* COLOR_TITLE_TEXT - Title text color */
--c_txt: #eaeaea; /* COLOR_TEXT - Regular text color */
--c_txtwrn: #ff5661; /* COLOR_TEXT_WARNING - Warning text color */
--c_txtscc: #008000; /* COLOR_TEXT_SUCCESS - Success text color */
--c_btn: #1fa3ec; /* COLOR_BUTTON - Primary button color */
--c_btnoff: #08405e; /* COLOR_BUTTON_OFF - Disabled button color */
--c_btntxt: #faffff; /* COLOR_BUTTON_TEXT - Button text color */
--c_btnhvr: #0e70a4; /* COLOR_BUTTON_HOVER - Button hover color */
--c_btnrst: #d43535; /* COLOR_BUTTON_RESET - Reset/danger button color */
--c_btnrsthvr: #931f1f; /* COLOR_BUTTON_RESET_HOVER - Reset button hover */
--c_btnsv: #47c266; /* COLOR_BUTTON_SAVE - Save button color */
--c_btnsvhvr: #5aaf6f; /* COLOR_BUTTON_SAVE_HOVER - Save button hover */
--c_in: #dddddd; /* COLOR_INPUT - Input background */
--c_intxt: #000000; /* COLOR_INPUT_TEXT - Input text color */
--c_csl: #1f1f1f; /* COLOR_CONSOLE - Console background */
--c_csltxt: #65c115; /* COLOR_CONSOLE_TEXT - Console text color */
--c_tab: #999999; /* COLOR_TIMER_TAB_BACKGROUND - Tab color */
--c_tabtxt: #faffff; /* COLOR_TIMER_TAB_TEXT - Tab text color */
}
/* Light theme fallbacks (from tasmota_globals.h) */
:root {
--c_bg_light: #fff; /* COLOR_BACKGROUND - White */
--c_frm_light: #f2f2f2; /* COLOR_FORM - Greyish */
--c_txt_light: #000; /* COLOR_TEXT - Black */
--c_in_light: #fff; /* COLOR_INPUT - White */
--c_intxt_light: #000; /* COLOR_INPUT_TEXT - Black */
}
Note: The CSS variable names (--c_*) are generated by the Tasmota backend from the color constants. The actual mapping is:
--c_bg ← COLOR_BACKGROUND--c_frm ← COLOR_FORM--c_txt ← COLOR_TEXT/* Basic element styling - Verified from HTTP_HEAD_STYLE1.h */
div, fieldset, input, select {
padding: 5px;
font-size: 1em;
}
fieldset {
background: var(--c_frm); /* COLOR_FORM - Also update HTTP_TIMER_STYLE */
}
p {
margin: 0.5em 0;
}
body {
text-align: center;
font-family: verdana, sans-serif;
background: var(--c_bg); /* COLOR_BACKGROUND */
}
td {
padding: 0px;
}
/* Input styling - Verified from HTTP_HEAD_STYLE1.h */
input {
width: 100%;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
background: var(--c_in); /* COLOR_INPUT */
color: var(--c_intxt); /* COLOR_INPUT_TEXT */
}
input[type=checkbox], input[type=radio] {
width: 1em;
margin-right: 6px;
vertical-align: -1px;
}
input[type=range] {
width: 99%;
}
select {
width: 100%;
background: var(--c_in); /* COLOR_INPUT */
color: var(--c_intxt); /* COLOR_INPUT_TEXT */
}
textarea {
resize: vertical;
width: 98%;
height: 318px;
padding: 5px;
overflow: auto;
background: var(--c_bg); /* COLOR_BACKGROUND */
color: var(--c_csltxt); /* COLOR_CONSOLE_TEXT */
}
/* Button styling - Verified from HTTP_HEAD_STYLE2.h */
button {
border: 0;
border-radius: 0.3rem;
background: var(--c_btn); /* COLOR_BUTTON */
color: var(--c_btntxt); /* COLOR_BUTTON_TEXT */
line-height: 2.4rem;
font-size: 1.2rem;
width: 100%;
-webkit-transition-duration: 0.4s;
transition-duration: 0.4s;
cursor: pointer;
}
button:hover {
background: var(--c_btnhvr); /* COLOR_BUTTON_HOVER */
}
/* Special button classes */
.bred {
background: var(--c_btnrst); /* COLOR_BUTTON_RESET */
}
.bred:hover {
background: var(--c_btnrsthvr); /* COLOR_BUTTON_RESET_HOVER */
}
.bgrn {
background: var(--c_btnsv); /* COLOR_BUTTON_SAVE */
}
.bgrn:hover {
background: var(--c_btnsvhvr); /* COLOR_BUTTON_SAVE_HOVER */
}
/* Link styling */
a {
color: var(--c_btn); /* COLOR_BUTTON */
text-decoration: none;
}
/* Utility classes */
.p {
float: left;
text-align: left;
}
.q {
float: right;
text-align: right;
}
.r {
border-radius: 0.3em;
padding: 2px;
margin: 4px 2px;
}
.hf {
display: none;
}
// Element selection shortcuts (ES6 arrow functions for code space savings)
var eb = s => document.getElementById(s); // Alias to save code space
var qs = s => document.querySelector(s); // Alias to save code space
// Password visibility toggle
var sp = i => eb(i).type = (eb(i).type === 'text' ? 'password' : 'text');
// Window load event handler (supports multiple handlers)
var wl = f => window.addEventListener('load', f);
// Auto-assign names to form elements
function jd() {
var t = 0, i = document.querySelectorAll('input,button,textarea,select');
while (i.length >= t) {
if (i[t]) {
i[t]['name'] = (i[t].hasAttribute('id') && (!i[t].hasAttribute('name')))
? i[t]['id'] : i[t]['name'];
}
t++;
}
}
// Show/hide elements with class 'hf' (hidden field)
function sf(s) {
var t = 0, i = document.querySelectorAll('.hf');
while (i.length >= t) {
if (i[t]) {
i[t].style.display = s ? 'block' : 'none';
}
t++;
}
}
// Execute auto-assign names on window load
wl(jd);
var x = null, lt, to, tp, pc = '';
// Load data with AJAX
function la(p) {
a = p || '';
clearTimeout(ft);
clearTimeout(lt);
if (x != null) { x.abort(); }
x = new XMLHttpRequest();
x.onreadystatechange = () => {
if (x.readyState == 4 && x.status == 200) {
var s = x.responseText
.replace(/{t}/g, "<table style='width:100%'>")
.replace(/{s}/g, "<tr><th>")
.replace(/{m}/g, "</th><td style='width:20px;white-space:nowrap'>")
.replace(/{e}/g, "</td></tr>");
eb('l1').innerHTML = s;
clearTimeout(ft);
clearTimeout(lt);
lt = setTimeout(la, 400); // Auto-refresh every 400ms
}
};
x.open('GET', '.?m=1' + a, true);
x.send();
ft = setTimeout(la, 2e4); // Fallback timeout 20 seconds
}
// Control function for sliders and buttons
function lc(v, i, p) {
if (eb('s')) {
if (v == 'h' || v == 'd') {
var sl = eb('sl4').value;
eb('s').style.background = 'linear-gradient(to right,rgb(' + sl + '%,' + sl + '%,' + sl + '%),hsl(' + eb('sl2').value + ',100%,50%))';
}
}
la('&' + v + i + '=' + p);
}
// Submit form with UI feedback
function su(t) {
eb('f3').style.display = 'none';
eb('f2').style.display = 'block';
t.form.submit();
}
// File upload with validation
function upl(t) {
var sl = t.form['u2'].files[0].slice(0, 1);
var rd = new FileReader();
rd.onload = () => {
var bb = new Uint8Array(rd.result);
if (bb.length == 1 && bb[0] == 0xE9) {
fct(t); // Factory reset check
} else {
t.form.submit();
}
};
rd.readAsArrayBuffer(sl);
return false;
}
// Factory reset confirmation
function fct(t) {
var x = new XMLHttpRequest();
x.open('GET', '/u4?u4=fct&api=', true);
x.onreadystatechange = () => {
if (x.readyState == 4 && x.status == 200) {
var s = x.responseText;
if (s == 'false') setTimeout(() => { fct(t); }, 6000);
if (s == 'true') setTimeout(() => { su(t); }, 1000);
} else if (x.readyState == 4 && x.status == 0) {
setTimeout(() => { fct(t); }, 2000);
}
};
x.send();
}
Based on the Tasmota configuration menu, here's the standard layout pattern:
<div style='background:var(--c_bg);text-align:left;display:inline-block;color:var(--c_txt);min-width:340px;position:relative;'>
<!-- Header -->
<div style='text-align:center;color:var(--c_ttl);'>
<noscript>To use Tasmota, please enable JavaScript
</noscript>
<h3>ESP32-DevKit</h3>
<h2>Tasmota</h2>
</div>
<!-- Page Title -->
<div style='padding:0px 5px;text-align:center;'>
<h3><hr>Configuration<hr></h3>
</div>
<!-- Menu Items -->
<p></p>
<form id="but7" style="display:block;" action='md' method='get'>
<button>Module</button>
</form>
<p></p>
<form id="but8" style="display:block;" action='wi' method='get'>
<button>WiFi</button>
</form>
<!-- More menu items... -->
<!-- Footer -->
<div style='text-align:right;font-size:11px;'>
<hr>
<a href='https://github.com/arendst/Tasmota' target='_blank' style='color:#aaa;'>
Tasmota 15.0.1.4 (tasmota) by Theo Arends
</a>
</div>
</div>
For configuration pages with forms:
<fieldset>
<legend><b> Other parameters </b></legend>
<form method='get' action='co'>
<!-- Template Section -->
<fieldset>
<legend><b> Template </b></legend>
<p>
<input id='t1' placeholder="Template" value='{"NAME":"ESP32-DevKit","GPIO":[1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1376,0,1,224,1,0,1,1,0,0,0,0,0,1,1,0,1,1,0,0,1],"FLAG":0,"BASE":1}'>
</p>
<p>
<label>
<input id='t2' type='checkbox' checked disabled>
<b>Activate</b>
</label>
</p>
</fieldset>
<!-- Password Field with Toggle -->
<label>
<b>Web Admin Password</b>
<input type='checkbox' onclick='sp("wp")'>
</label>
<input id='wp' type='password' placeholder="Web Admin Password" value="****">
<!-- Checkboxes -->
<label>
<input id='b3' type='checkbox' checked>
<b>HTTP API enable</b>
</label>
<!-- Text Inputs -->
<label><b>Device Name</b> (Tasmota)</label>
<input id='dn' placeholder="" value="Tasmota">
<!-- Radio Buttons -->
<fieldset>
<legend><b> Emulation </b></legend>
<p>
<label>
<input id='r0' name='b2' type='radio' value='0'>
<b>None</b>
</label>
<label>
<input id='r2' name='b2' type='radio' value='2' checked>
<b>Hue Bridge</b> multi device
</label>
</p>
</fieldset>
<!-- Submit Button -->
<button name='save' type='submit' class='button bgrn'>Save</button>
</form>
</fieldset>
The main control page features a prominent status display and interactive controls. Based on the screenshot analysis:
<!-- Dynamic Content Area -->
<div style='padding:0;' id='l1' name='l1'></div>
<!-- Control Buttons -->
<table style='width:100%'>
<tr>
<td style='width:100%'>
<button id='o1' onclick='la("&o=1");'>Toggle 1</button>
</td>
</tr>
</table>
<!-- Color Controls -->
<table style='width:100%'>
<!-- Hue Slider -->
<tr>
<td colspan='2' style='width:100%'>
<div id='b' class='r' style='background-image:linear-gradient(to right,#800,#f00 5%,#ff0 20%,#0f0 35%,#0ff 50%,#00f 65%,#f0f 80%,#f00 95%,#800);'>
<input id='sl2' type='range' min='0' max='359' value='95' onchange='lc("h",0,value)'>
</div>
</td>
</tr>
<!-- Saturation Slider -->
<tr>
<td colspan='2' style='width:100%'>
<div id='s' class='r' style='background-image:linear-gradient(to right,#CCCCCC,#6AFF00);'>
<input id='sl3' type='range' min='0' max='100' value='94' onchange='lc("n",0,value)'>
</div>
</td>
</tr>
<!-- Brightness Control -->
<tr>
<td style='width:15%'>
<button id='o2' onclick='la("&o=2");'>T2</button>
</td>
<td colspan='1' style='width:85%'>
<div id='c' class='r' style='background-image:linear-gradient(to right,#000,#fff);'>
<input id='sl4' type='range' min='0' max='100' value='80' onchange='lc("d",0,value)'>
</div>
</td>
</tr>
</table>
<!-- Button State Script -->
<script>
eb('o1').style.background = 'var(--c_btnoff)';
</script>
.r {
border-radius: 0.3em;
padding: 2px;
margin: 4px 2px;
}
textarea {
resize: vertical;
width: 98%;
height: 318px;
padding: 5px;
overflow: auto;
background: var(--c_bg);
color: var(--c_csltxt);
}
.hf {
display: none;
}
.p {
float: left;
text-align: left;
}
.q {
float: right;
text-align: right;
}
a {
color: var(--c_btn);
text-decoration: none;
}
td {
padding: 0px;
}
Tasmota WebUI is designed for microcontrollers with severe memory constraints (ESP8266: 80KB RAM, ESP32: 520KB RAM). Key optimization techniques:
eb=s=>document.getElementById(s) instead of function eb(s) { return document.getElementById(s); }x, lt, to, tp, pc){t}, {s}, {m}, {e} tokens instead of full HTML tagsCHUNKED_BUFFER_SIZE = 500 to limit RAM usage during transfersx.abort() prevents memory leaks from pending requestseb() and qs() aliases save repeated document. callsonchange and onclick attributeswhile (i.length >= t) instead of for loops with variable declarationsThe Tasmota WebUI communicates with the ESP8266/ESP32 backend through a lightweight HTTP API. The backend is implemented in xdrv_01_9_webserver.ino and handles all WebUI requests.
The webserver processes requests through these main functions:
// From xdrv_01_9_webserver.ino
void HandleRoot() {
// Serve main page with device status
WSContentStart_P(PSTR(D_DEVICE_NAME));
WSContentSendStyle();
WSContentSend_P(HTTP_HEADER1, SettingsText(SET_LANGUAGE), SettingsText(SET_DEVICENAME), my_version);
// ... more content generation
WSContentEnd();
}
void HandleAjaxStatusRefresh() {
// Handle AJAX status updates
char svalue[32];
snprintf_P(svalue, sizeof(svalue), PSTR("{t}"), mqtt_data);
WSContentSend_P(PSTR("%s"), svalue);
}
The frontend JavaScript uses AJAX to communicate with the backend:
// Load data with AJAX (from HTTP_HEADER1_ES6.h)
function la(p) {
a = p || '';
clearTimeout(ft);
clearTimeout(lt);
if (x != null) { x.abort(); }
x = new XMLHttpRequest();
x.onreadystatechange = () => {
if (x.readyState == 4 && x.status == 200) {
var s = x.responseText
.replace(/{t}/g, "<table style='width:100%'>")
.replace(/{s}/g, "<tr><th>")
.replace(/{m}/g, "</th><td style='width:20px;white-space:nowrap'>")
.replace(/{e}/g, "</td></tr>");
eb('l1').innerHTML = s;
clearTimeout(ft);
clearTimeout(lt);
lt = setTimeout(la, 400); // Auto-refresh every 400ms
}
};
x.open('GET', '.?m=1' + a, true);
x.send();
ft = setTimeout(la, 2e4); // Fallback timeout 20 seconds
}
Commands are sent to the backend via URL parameters:
// Control function for sliders and buttons (from HTTP_HEADER1_ES6.h)
function lc(v, i, p) {
if (eb('s')) {
if (v == 'h' || v == 'd') {
var sl = eb('sl4').value;
eb('s').style.background = 'linear-gradient(to right,rgb(' + sl + '%,' + sl + '%,' + sl + '%),hsl(' + eb('sl2').value + ',100%,50%))';
}
}
la('&' + v + i + '=' + p);
}
The backend returns data in a compact format that the frontend parses:
// Example backend response generation
void WebSendState() {
char svalue[32];
snprintf_P(svalue, sizeof(svalue), PSTR("{s}Power{m}%d{e}"), power);
WSContentSend_P(PSTR("%s"), svalue);
}
The response uses special tokens that JavaScript replaces with HTML:
{t} → <table style='width:100%'>{s} → <tr><th>{m} → </th><td style='width:20px;white-space:nowrap'>{e} → </td></tr>Forms are submitted to specific endpoints that process configuration changes:
<form method='get' action='co'>
<!-- Configuration inputs -->
<button name='save' type='submit' class='button bgrn'>Save</button>
</form>
The backend handles these form submissions in HandleConfiguration() and related functions.
For real-time status updates, the WebUI uses:
<button id='o1' onclick='la("&o=1");'>Toggle 1</button>
<fieldset>
<legend><b> Section Title </b></legend>
<!-- Configuration options -->
</fieldset>
<label><b>Setting Name</b> (default)</label>
<input id='setting' placeholder="Enter value" value="current_value">
<label>
<input id='option' type='checkbox' checked>
<b>Option Description</b>
</label>
<fieldset>
<legend><b> Emulation </b></legend>
<p>
<label>
<input name='emulation' type='radio' value='0'>
<b>None</b>
</label>
<label>
<input name='emulation' type='radio' value='2' checked>
<b>Hue Bridge</b> multi device
</label>
</p>
</fieldset>
<label>
<b>Web Admin Password</b>
<input type='checkbox' onclick='sp("wp")' style='width:auto;margin-left:10px;'>
</label>
<input id='wp' type='password' placeholder="Web Admin Password" value="****">
<fieldset>
<legend><b> Template </b></legend>
<p>
<input id='t1' placeholder="Template" value='{"NAME":"ESP32-DevKit","GPIO":[1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1376,0,1,224,1,0,1,1,0,0,0,0,0,1,1,0,1,1,0,0,1],"FLAG":0,"BASE":1}'>
</p>
<p>
<label>
<input id='t2' type='checkbox' checked disabled>
<b>Activate</b>
</label>
</p>
</fieldset>
<table style='width:100%'>
<tr>
<td colspan='2' style='width:100%'>
<div id='b' class='r' style='background-image:linear-gradient(to right,#800,#f00 5%,#ff0 20%,#0f0 35%,#0ff 50%,#00f 65%,#f0f 80%,#f00 95%,#800);'>
<input id='sl2' type='range' min='0' max='359' value='95' onchange='lc("h",0,value)'>
</div>
</td>
</tr>
<tr>
<td colspan='2' style='width:100%'>
<div id='s' class='r' style='background-image:linear-gradient(to right,#CCCCCC,#6AFF00);'>
<input id='sl3' type='range' min='0' max='100' value='94' onchange='lc("n",0,value)'>
</div>
</td>
</tr>
<tr>
<td style='width:15%'>
<button id='o2' onclick='la("&o=2");'>T2</button>
</td>
<td colspan='1' style='width:85%'>
<div id='c' class='r' style='background-image:linear-gradient(to right,#000,#fff);'>
<input id='sl4' type='range' min='0' max='100' value='80' onchange='lc("d",0,value)'>
</div>
</td>
</tr>
</table>
This guide has been verified against Tasmota source code version 15.0.1.4. All code examples are extracted directly from the following source files:
tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino (lines 1-300)
tasmota/html_uncompressed/HTTP_HEADER1_ES6.h
eb, qs, sp, wl, jd, sf)la, lc)tasmota/html_uncompressed/HTTP_HEAD_STYLE1.h
tasmota/html_uncompressed/HTTP_HEAD_STYLE2.h
.p, .q, .r, .hf)tasmota/my_user_config.h (lines 200-245)
tasmota/include/tasmota_globals.h (lines 439-500)
--c_*) match the Tasmota backend generationxdrv_01_9_webserver.inoThis guide provides the foundation for creating Tasmota-compatible WebUI interfaces that are efficient, user-friendly, and consistent with the existing design system. The information is based on actual Tasmota source code analysis, not screenshots or external documentation.