A lightweight captive portal library for ESP8266 (Arduino/PlatformIO).
Lets users connect to a temporary WiFi access point, open a web portal, and interact with your device (e.g., submit WiFi credentials or other data).
- Captive portal: Redirects all DNS and HTTP traffic to the ESP for easy setup.
- Static file serving: Serves files (e.g.,
index.html, CSS, JS) from LittleFS. - Custom API endpoints: Easily add your own HTTP handlers (e.g.,
/api/wifi). - Captive portal detection: Handles Android, Windows, and Apple captive portal checks.
- Simple API: Minimal setup, easy to extend.
Looking for a ready-to-use example?
Check out the ESP8266-CaptivePortal-ExampleProject.
- ESP8266-based board (e.g., NodeMCU, Wemos D1 Mini)
- PlatformIO or Arduino IDE
Add this library to your platformio.ini:
lib_deps =
lennart080/CaptivePortal
# Optional: Only add the following if PlatformIO cannot find ESPAsyncWebServer automatically
me-no-dev/ESPAsyncWebServer
Note:
Theme-no-dev/ESPAsyncWebServerdependency is optional and should only be added if PlatformIO fails to resolve it automatically and you encounter an error about a missing library.
Or install via the PlatformIO Library Manager.
- Download this repository as a ZIP file.
- In Arduino IDE, go to Sketch > Include Library > Add .ZIP Library... and select the downloaded ZIP.
- Install the required dependencies:
To serve web files (like index.html), upload them to the ESP8266's LittleFS filesystem.
-
PlatformIO:
LittleFS upload is natively supported. Add to yourplatformio.ini:board_build.filesystem = littlefsPlace your web files in the
/datafolder. Then run:pio run --target uploadfs -
Arduino IDE:
Use the LittleFS Data Upload tool.
#include "CaptivePortal.h"
CaptivePortal portal;You can initialize the portal as an open AP (no password):
if (!portal.initializeOpen("My-Example-AP", "index.html")) {
Serial.println("Init failed: " + portal.getLastErrorString());
}Or as a password-protected AP:
if (!portal.initialize("My-Example-AP", "12345678", "index.html")) {
Serial.println("Init failed: " + portal.getLastErrorString());
}- The password must be 8–63 characters for WPA2 AP mode.
- The third argument is the default file to serve (e.g.,
"index.html"from LittleFS).
Start the portal:
if (!portal.startAP()) {
Serial.println("Start failed: " + portal.getLastErrorString());
}Call processDNS() in your loop to handle DNS redirection:
void loop() {
portal.processDNS();
delay(100);
}You can add your own HTTP handlers using the underlying AsyncWebServer:
portal.getServer().on("/api", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("text", true)) {
String text = request->getParam("text", true)->value();
// Save credentials, connect, etc.
request->send(200, "text/plain", "Text received.");
portal.stopAP();
} else {
request->send(400, "text/plain", "Missing Text.");
}
});Here’s a simple example of how you could send data from your frontend to the custom API endpoint using JavaScript:
function sendText() {
const text = document.getElementById('textInput').value;
fetch('/api', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'text=' + encodeURIComponent(text)
})
.then(response => response.text())
.then(data => {
alert('Server response: ' + data);
})
.catch(error => {
alert('Error: ' + error);
});
}And in your index.html:
<input id="textInput" type="text" placeholder="Enter text">
<button onclick="sendText()">Send</button>
<script src="script.js"></script>- This example sends the value from an input field to your
/apiendpoint using a POST request. - Adjust the parameter name and logic as needed for your use case.
- Place your
index.html,style.css, andscript.jsin the device's LittleFS filesystem. - The portal will serve these files automatically at
/.
- LittleFS mount failed: Ensure you have uploaded files to LittleFS and the filesystem is formatted.
- File not found: Make sure your default file (e.g.,
index.html) exists in LittleFS. - Invalid SSID/Password: SSID must be 1–32 chars; password must be 8–63 chars or empty for open AP.
- AP won't start: Check for conflicting WiFi modes or hardware issues.
| Method | Description |
|---|---|
bool initializeOpen(ssid, defaultFile) |
Open AP, no password. |
bool initialize(ssid, password, defaultFile) |
WPA2 AP, with password. |
bool startAP() |
Start the AP and web server. |
bool stopAP() |
Stop the AP and web server. |
void processDNS() |
Handle DNS requests (call in loop). |
AsyncWebServer& getServer() |
Access the underlying AsyncWebServer. |
CaptivePortalError getLastError() const |
Get the last error code. |
String getLastErrorString() const |
Get a string describing the last error. |
This project is licensed under the Apache 2.0 License.
You may use, share and modify it, as long as you give credit to:
Lennart Gutjahr (2025) — Original author
gutjahrlennart@gmail.com — Email
License details: Apache License 2.0
Based on ESPAsyncWebServer and LittleFS.