Why hard-coded WiFi credentials are a bad idea, and how to fix that — with WiFiManager, or fully hand-written.
The ESP8266 / ESP32 is basically a programmable switch of GPIO pins, with WiFi and Bluetooth / BLE. WiFi is almost always used with the ESP: most programmable boards without WiFi are much more energy efficient than the ESP. So we can say that we always use the ESP for WiFi.
So how do we best program the connection?
Hard-coding the login information in the sketch? That's a bad idea. Add a separate file "credentials.h" with SSID and password to the sketch? Neither can developing a professional application that way. A customer will not recompile a sketch, he wants his IOT gadget to just work after setup.
On startup, the ESP should try to connect to WiFi, using the login credentials from the EEPROM. If that fails, launch an access point and a web server to store the login credentials, then reboot the ESP.
WiFiManager.h does, with only 2 lines in your sketch. You'll find plenty of sketches on this site (TM1637, SSD1306, SH1106, ...) to see how that works in practice.
If you don't like to depend on a library, you can manually add the code in your sketch. Here is a simple example (ESP32) connecting to WiFi, and if it fails, starting a WiFi access point and web server to store the data — including a clean configuration page listing found networks and their signal strength.
/*------------------------------------------------------------
ESP32 WiFi Setup Portal
------------------------------------------------------------
Description
----------
If the ESP32 cannot connect to a previously stored WiFi network,
it creates a temporary access point allowing the user to select
and configure WiFi credentials via a web interface - 192.168.4.1
Features
--------
• Scans nearby WiFi networks
• Displays signal strength
• Indicates open / secured networks
• Stores credentials in ESP32 NVS (Preferences)
• Automatically reconnects after reboot
Author : johanok@gmail.com - https://espgo.be
Board : ESP32
IDE : Arduino IDE
------------------------------------------------------------
*/
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <vector>
WebServer server(80);
Preferences flash; // Store data in non-volatile storage (NVS)
#define this_Ssid "ESP32-Conn" // Name of the temporary setup hotspot
String ssid, pasw;
struct WifiNet { // Structure used to store scanned networks
String ssid;
int rssi;
bool open; // true = no password required
};
std::vector<WifiNet> availableSSIDs; // List of discovered networks (sorted by signal strength)
void setup() {
Serial.begin(115200);
connect_to_WiFi();
Serial.println("Connected to " + WiFi.SSID());
}
void loop() {} // empty in this design
void connect_to_WiFi() {
WiFi.mode(WIFI_MODE_STA);
flash.begin("login_data", true); // Read previously stored credentials from flash
ssid = flash.getString("ssid", "");
pasw = flash.getString("pasw", "");
flash.end();
if (ssid.length() == 0) {
Serial.println("No saved network found.");
startPortal();
return;
}
Serial.print("Connecting");
#if CONFIG_IDF_TARGET_ESP32S3
WiFi.setSleep(false); // Ensure stable WiFi on ESP32-S3
#endif
WiFi.begin(ssid.c_str(), pasw.c_str());
for (uint8_t i = 0; i < 50; ++i) { // Try connecting for ~8 seconds
if (WiFi.isConnected()) {
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
return;
}
delay(160);
Serial.print(".");
}
Serial.println("\nConnection failed.");
startPortal(); // Connection failed → start WiFi configuration portal
}
void startPortal() {
Serial.println("Connect to hotspot: " + String(this_Ssid));
int numNetworks = WiFi.scanNetworks();
availableSSIDs.clear();
for (int i = 0; i < numNetworks; i++) {
WifiNet net;
net.ssid = WiFi.SSID(i);
net.rssi = WiFi.RSSI(i);
net.open = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN);
availableSSIDs.push_back(net);
}
std::sort(availableSSIDs.begin(), availableSSIDs.end(),
[](const WifiNet &a, const WifiNet &b) {
return a.rssi > b.rssi;
});
WiFi.mode(WIFI_MODE_AP);
WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info) { // Start the AP as soon as the driver indicates that AP mode is active
if (event == WIFI_EVENT_AP_START) {
Serial.println("AP mode ready, starting hotspot...");
WiFi.softAP(this_Ssid, "", 1);
}
});
server.on("/", serveWiFiSetupPage);
server.on("/setting", processWiFiSettingsSubmission);
server.begin();
for (;;) server.handleClient(); // Stay here until credentials are entered and ESP restarts
}
int rssiPercent(int rssi) { // Convert RSSI (dBm) into signal percentage (0-100)
if (rssi >= -50) return 100;
if (rssi <= -100) return 0;
return 2 * (rssi + 100);
}
void serveWiFiSetupPage() {
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WiFi Instellen</title>
<style>
:root{
--bg:#f0f4f8;
--card:#ffffff;
--border:#c8d4e0;
--accent:#0066cc;
--accent2:#004fa3;
--text:#1a2332;
--muted:#5a6a7a;
--success:#16a34a;
--radius:14px;
}
*{box-sizing:border-box;margin:0;padding:0}
body{
background:var(--bg);
color:var(--text);
font-family:'Segoe UI',system-ui,sans-serif;
min-height:100vh;
display:flex;
align-items:center;
justify-content:center;
padding:16px
}
.card{
background:var(--card);
border:1px solid var(--border);
border-radius:22px;
width:100%;
max-width:460px;
padding:32px;
box-shadow:0 4px 24px rgba(0,60,120,0.10)
}
.logo{
display:flex;
align-items:center;
justify-content:center;
gap:20px;
margin-bottom:8px}
.logo svg {
flex-shrink:0;
height: 46px;
vertical-align: middle;
}
.logo-text {
font-size:20px;
font-weight:900;
letter-spacing:.10em;
color:var(--accent);
line-height: 1; /* prevents extra vertical space */
}
h1{font-size:26px;font-weight:800;margin-bottom:6px}
.sub{
font-size:16px;
color:var(--muted);
margin-bottom:26px
}
.section-label{
font-size:13px;
font-weight:700;
letter-spacing:.07em;
text-transform:uppercase;
color:var(--muted);
margin-bottom:12px
}
.net-list{
display:flex;
flex-direction:column;
gap:8px;
margin-bottom:26px;
max-height:300px;
overflow-y:auto;
padding-right:2px
}
.net-btn{
display:flex;
align-items:center;
gap:14px;
width:100%;
background:#f7fafd;
border:2px solid var(--border);
border-radius:var(--radius);
padding:14px 18px;
cursor:pointer;
transition:all .15s;
text-align:left;
color:var(--text)
}
.net-btn:hover{
border-color:var(--accent);
background:#e8f0fb
}
.net-btn.selected{
border-color:var(--accent);
background:#ddeeff
}
.net-name{
font-size:17px;
font-weight:600;
flex:1;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap
}
.net-lock{font-size:18px;opacity:.8}
.signal{
display:flex;
align-items:flex-end;
gap:3px;
height:20px;
flex-shrink:0
}
.signal span{
display:inline-block;
width:5px;
background:#c8d4e0;
border-radius:2px
}
.signal span.on{background:var(--accent)}
.s1{height:4px}
.s2{height:7px}
.s3{height:11px}
.s4{height:15px}
.s5{height:20px}
.divider{
height:1px;
background:var(--border);
margin-bottom:24px
}
label{
display:block;
font-size:14px;
font-weight:700;
letter-spacing:.05em;
text-transform:uppercase;
color:var(--muted);
margin-bottom:8px
}
input[type=text],input[type=password]{
width:100%;
background:#f7fafd;
border:2px solid var(--border);
border-radius:var(--radius);
padding:14px 16px;
color:var(--text);
font-size:17px;
outline:none;
margin-bottom:16px
}
.btn{
width:100%;
background:linear-gradient(135deg,var(--accent2),var(--accent));
border:none;
border-radius:var(--radius);
padding:17px;
color:#fff;
font-size:18px;
font-weight:700;
cursor:pointer
}
</style>
</head>
<body>
<div class="card">
<div class="logo">
<svg width="46" height="46" preserveAspectRatio="xMidYMid" viewBox="0 -32 256 256">
<path fill-rule="evenodd" fill="#004d26" d="M198.751 31.302C181.263 12.072 156.041 0 128 0S74.737 12.072 57.249 31.302zM57.25 159.91c17.488
19.23 42.71 31.302 70.751 31.302s53.263-12.072 70.751-31.302zM42.464 39.431C19.012 39.431 0 58.443 0 81.896v27.42c0 23.452
19.012 42.464 42.464 42.464h171.072c23.452 0 42.464-19.012 42.464-42.464v-27.42c0-23.453-19.012-42.465-42.464-42.465zm209.168
41.98c0-20.773-16.839-37.612-37.611-37.612h-42.465c-20.772 0-37.611 16.839-37.611 37.611v28.512c0 20.773-16.839 37.612-37.611
37.612H214.02c20.772 0 37.611-16.84 37.611-37.612zM104.1 83.957v40.16h12.132v-40.16zm-1.214-12.678a7.219 7.219 0 1 0 14.438
0 7.219 7.219 0 0 0-14.438 0M21.96 65.517l15.651 58.6h10.07l11.77-40.523 11.768 40.524h10.434l15.409-58.601h-11.77l-8.977
38.582-11.162-38.582H54.355l-11.163 37.611L34.7 65.517zm131.276
0v58.6h12.86v-22.93h29.12v-11.89h-29.12v-11.89h31.06v-11.89zm52.656 18.441v40.16h12.133v-40.16zm-1.092-12.8a7.219
7.219 0 1 0 14.438 0 7.219 7.219 0 0 0-14.438 0"/>
</svg>
<span class="logo-text">WiFi Setup</span>
<svg width="46" height="46" data-name="Layer 1" viewBox="0 0 24 24">
<defs>
<style>
.cls-1 {
fill: none;
stroke: #004d26;
stroke-miterlimit: 10;
stroke-width: 1.91px;
}
.cls-arc {
fill: none;
stroke: #0066cc;
stroke-miterlimit: 10;
stroke-width: 1.91px;
}
</style>
</defs>
<rect width="21" height="7.64" x="1.5" y="14.86" class="cls-1" rx="1.91"/>
<path d="M6.27 18.68ZM10.09 18.68ZM19.64 18.68H12M4.36 3.41v11.45" class="cls-1"/>
<circle cx="4.36" cy="2.45" r=".95" class="cls-1"/>
<circle cx="9.14" cy="11.05" r=".95" style="fill:#004d26"/>
<!-- inner arc - blue -->
<path d="M8.18 7.23A4.76 4.76 0 0 1 13 12" class="cls-arc"/>
<!-- outer arc - blue -->
<path d="M8.18 3.41A8.59 8.59 0 0 1 16.77 12" class="cls-arc"/>
</svg>
</div>
<h1>Configure Connection</h1>
<p class="sub">Select a network or enter the credentials manually.</p>
<div class="section-label">Available networks</div>
<div class="net-list">
)rawliteral";
// -------- Add dynamic networks --------
for (const WifiNet &net : availableSSIDs) {
int pct = rssiPercent(net.rssi);
String s1 = pct >= 10 ? "on" : "";
String s2 = pct >= 30 ? "on" : "";
String s3 = pct >= 50 ? "on" : "";
String s4 = pct >= 70 ? "on" : "";
String s5 = pct >= 90 ? "on" : "";
String lockIcon = net.open ? "🔓" : "🔒";
html += "<button type='button' class='net-btn' data-ssid='";
html += net.ssid;
html += "' onclick=\"selectNet('";
html += net.ssid;
html += "',";
html += net.open ? "true" : "false";
html += ")\">";
html += "<span class='signal'>";
html += "<span class='s1 " + s1 + "'></span>";
html += "<span class='s2 " + s2 + "'></span>";
html += "<span class='s3 " + s3 + "'></span>";
html += "<span class='s4 " + s4 + "'></span>";
html += "<span class='s5 " + s5 + "'></span>";
html += "</span>";
html += "<span class='net-name'>" + net.ssid + "</span>";
html += "<span class='net-lock'>" + lockIcon + "</span>";
html += "</button>";
}
// -------- HTML continued --------
html += R"rawliteral(
</div>
<div class="divider"></div>
<form method="get" action="setting">
<label for="ssid-inp">Network Name (SSID)</label>
<input type="text" id="ssid-inp" name="ssid" placeholder="Network Name" required>
<label for="pass-inp">Password</label>
<input type="password" id="pass-inp" name="pass" placeholder="Password" required>
<button class="btn" type="submit">Save & Connect</button>
</form>
</div>
<script>
function selectNet(name,isOpen){
var si=document.getElementById('ssid-inp');
var pi=document.getElementById('pass-inp');
si.value=name;
document.querySelectorAll('.net-btn').forEach(function(b){
b.classList.toggle('selected', b.dataset.ssid === name);
});
if(isOpen){
pi.value='';
pi.disabled=true;
pi.placeholder='Open network - no password needed';
pi.removeAttribute('required');
}
else{
pi.disabled=false;
pi.placeholder='Password';
pi.setAttribute('required','required');
}
si.focus();
}
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html; charset=utf-8", html);
}
void processWiFiSettingsSubmission() { // Read submitted form data
ssid = server.arg("ssid");
pasw = server.arg("pass");
Serial.println();
Serial.println("Received WiFi credentials:");
Serial.println(ssid);
Serial.println(pasw);
flash.begin("login_data", false); // Store credentials in flash memory
flash.putString("ssid", ssid);
flash.putString("pasw", pasw);
flash.end();
String reply = F("<!DOCTYPE html><html><head>"
"<meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>Saved</title>"
"</head><body style='font-family:Arial;text-align:center;padding:40px'>"
"<h2>Settings saved</h2>"
"<p>The ESP32 will now restart and attempt to connect to:</p><b>");
reply += ssid;
reply += F("</b></body></html>");
server.send(200, "text/html; charset=utf-8", reply);
delay(1000);
ESP.restart();
}
Curious about actual projects that use WiFiManager? Check out the full overview, including TM1637, SSD1306, SH1106, ST7735, ST7789, e-paper, the Lilygo T-Display(S3) and the Waveshare ESP32-S3.