Control an LED Strip from Your Phone with ESP8266

Open your phone's browser. Type in an IP address. A color picker appears. Tap purple — and across the room, eight LEDs shift to purple. Tap the "rainbow" button — they start cycling through every color.

No app to install. No cloud account. No subscription. The ESP8266 itself is the server, the controller, and the interface — all running on a board that costs a few dollars and fits on your thumb.

This is where things start to feel like a real product. You're combining addressable LEDs (each pixel independently controllable), a web server running on a microcontroller, and a responsive UI built with HTML and JavaScript — served directly from the chip. The same architecture powers commercial smart bulbs and LED controllers, just with fancier packaging.

What You'll Need

If you haven't set up the Arduino IDE for ESP8266 yet, see our Wi-Fi Weather Station article for the one-time board installation steps.

About the LED strip: WS2812B strips come in different densities (30, 60, or 144 LEDs per meter). For this project, any density works. You can cut the strip to any length — just cut on the marked copper pads between LEDs. We'll use 8 LEDs, but the code works with any number.


Know Your Component

WS2812B Addressable LEDs

A regular LED strip has all LEDs wired in parallel — they all show the same color. A WS2812B strip is fundamentally different: each LED contains a tiny controller chip that receives data, sets its own color, and passes the remaining data to the next LED in the chain.

NeoPixel Anatomy

This means you can set LED #0 to red, LED #1 to green, LED #2 to blue — all independently, all from a single data wire. The protocol sends 24 bits per LED (8 bits each for red, green, and blue), giving you 16.7 million possible colors per pixel.

The strip has three connections: 5V (power), GND (ground), and Data In (the signal from the microcontroller). Some strips also have a Data Out pad for chaining multiple strips together.

Power matters: Each LED can draw up to 60 mA at full white brightness. For 8 LEDs, that's 480 mA — manageable from USB power. But a 30-LED strip at full brightness could draw 1.8A, requiring an external 5V power supply. We'll keep it safe with 8 LEDs.


Installing the Library

In the Arduino IDE, go to Sketch → Include Library → Manage Libraries and install:

"Adafruit NeoPixel" by Adafruit — the most widely used library for WS2812B LEDs.

The ESP8266 web server library (ESP8266WebServer) comes built-in with the board package.


Wiring It Up

Three wires from the LED strip to the ESP8266. No breadboard needed.

Wiring Diagram

Red wire: Strip 5V → ESP8266 VIN (this passes through the USB 5V)

Green wire: Strip Data In → ESP8266 D4 (GPIO2)

Black wire: Strip GND → ESP8266 GND

Check the arrow: WS2812B strips have a direction. Data flows in one direction, usually marked with arrows printed on the strip. Connect to the Data In end (the side without an arrow pointing away from it).

Why D4? GPIO2 (labeled D4 on NodeMCU) works reliably for NeoPixel data. Some pins on the ESP8266 have special boot functions that can cause issues — D4 is a safe, tested choice.


The Code

Replace YOUR_WIFI_SSID and YOUR_WIFI_PASSWORD with your actual network credentials.

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <Adafruit_NeoPixel.h>

#define LED_PIN D4
#define NUM_LEDS 8

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
ESP8266WebServer server(80);

uint8_t currentR = 0, currentG = 0, currentB = 150;
String currentMode = "solid";

void setup() {
  Serial.begin(115200);

  strip.begin();
  strip.setBrightness(80);
  setAllColor(currentR, currentG, currentB);
  strip.show();

  WiFi.begin(ssid, password);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.print("Connected! Open http://");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot);
  server.on("/color", handleColor);
  server.on("/mode", handleMode);
  server.on("/brightness", handleBrightness);
  server.begin();
}

void loop() {
  server.handleClient();

  if (currentMode == "rainbow") {
    rainbowCycle();
  } else if (currentMode == "breathe") {
    breatheEffect();
  }
}

void handleRoot() {
  String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>LED Controller</title>
  <style>
    body { font-family: -apple-system, sans-serif; background: #1a1a2e; color: #fff;
           display: flex; flex-direction: column; align-items: center; padding: 20px; }
    h1 { font-size: 1.4em; margin-bottom: 20px; }
    .card { background: #16213e; border-radius: 12px; padding: 20px;
            width: 90%; max-width: 340px; margin-bottom: 16px; }
    .card h2 { font-size: 1em; margin: 0 0 12px 0; color: #a8b2d1; }
    input[type=color] { width: 100%; height: 60px; border: none; border-radius: 8px;
                        cursor: pointer; background: none; }
    .btn { display: inline-block; padding: 10px 18px; margin: 4px; border-radius: 8px;
           border: 2px solid #a8b2d1; background: transparent; color: #fff;
           font-size: 0.9em; cursor: pointer; }
    .btn.active { background: #e94560; border-color: #e94560; }
    .slider { width: 100%; margin-top: 8px; }
    .ip { font-size: 0.7em; color: #555; margin-top: 16px; }
  </style>
</head>
<body>
  <h1>LED Strip Controller</h1>

  <div class="card">
    <h2>Color</h2>
    <input type="color" id="picker" value="#0000FF"
           onchange="fetch('/color?r='+parseInt(this.value.substr(1,2),16)+'&g='+parseInt(this.value.substr(3,2),16)+'&b='+parseInt(this.value.substr(5,2),16))">
  </div>

  <div class="card">
    <h2>Mode</h2>
    <button class="btn active" onclick="setMode('solid',this)">Solid</button>
    <button class="btn" onclick="setMode('rainbow',this)">Rainbow</button>
    <button class="btn" onclick="setMode('breathe',this)">Breathe</button>
    <button class="btn" onclick="setMode('off',this)">Off</button>
  </div>

  <div class="card">
    <h2>Brightness</h2>
    <input type="range" class="slider" min="5" max="255" value="80"
           oninput="fetch('/brightness?val='+this.value)">
  </div>

  <div class="ip">Connected to ESP8266</div>

  <script>
    function setMode(mode, btn) {
      fetch('/mode?m=' + mode);
      document.querySelectorAll('.btn').forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
    }
  </script>
</body>
</html>
)rawliteral";

  server.send(200, "text/html", html);
}

void handleColor() {
  currentR = server.arg("r").toInt();
  currentG = server.arg("g").toInt();
  currentB = server.arg("b").toInt();
  currentMode = "solid";
  setAllColor(currentR, currentG, currentB);
  strip.show();
  server.send(200, "text/plain", "OK");
}

void handleMode() {
  currentMode = server.arg("m");
  if (currentMode == "solid") {
    setAllColor(currentR, currentG, currentB);
    strip.show();
  } else if (currentMode == "off") {
    setAllColor(0, 0, 0);
    strip.show();
  }
  server.send(200, "text/plain", "OK");
}

void handleBrightness() {
  int val = server.arg("val").toInt();
  strip.setBrightness(val);
  strip.show();
  server.send(200, "text/plain", "OK");
}

void setAllColor(uint8_t r, uint8_t g, uint8_t b) {
  for (int i = 0; i < NUM_LEDS; i++) {
    strip.setPixelColor(i, strip.Color(r, g, b));
  }
}

void rainbowCycle() {
  static uint16_t hue = 0;
  for (int i = 0; i < NUM_LEDS; i++) {
    uint16_t pixelHue = hue + (i * 65536L / NUM_LEDS);
    strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)));
  }
  strip.show();
  hue += 256;
  delay(20);
}

void breatheEffect() {
  static int brightness = 0;
  static int direction = 5;
  brightness += direction;
  if (brightness >= 255 || brightness <= 0) direction = -direction;
  for (int i = 0; i < NUM_LEDS; i++) {
    strip.setPixelColor(i, strip.Color(
      currentR * brightness / 255,
      currentG * brightness / 255,
      currentB * brightness / 255));
  }
  strip.show();
  delay(30);
}

How to upload

  1. Go to Tools → Board → NodeMCU 1.0 (ESP-12E Module)
  2. Select the correct Port
  3. Click Upload

After uploading, open the Serial Monitor (115200 baud). It will print the IP address — something like 192.168.1.42. Type that address into any browser on the same Wi-Fi network.

Understanding the code

ESP8266WebServer server(80) — Creates a web server on port 80 (the standard HTTP port). When a browser connects to the ESP8266's IP address, this server handles the request.

server.on("/", handleRoot) — Defines a route. When someone visits the root URL (/), the handleRoot() function runs and sends back the HTML page. This is the same routing concept used in full web frameworks like Express or Flask.

server.on("/color", handleColor) — Another route. When the color picker changes, the browser sends a request like /color?r=255&g=0&b=128. The handler reads the parameters and updates the LEDs.

server.handleClient() — Must be called repeatedly in loop(). It checks for incoming HTTP requests and dispatches them to the matching handler. Without this, the server won't respond.

R"rawliteral(...)rawliteral" — A C++ raw string literal that lets you embed the entire HTML page without escaping quotes or special characters. The HTML includes inline CSS and JavaScript — everything in one string.

strip.setPixelColor(i, strip.Color(r, g, b)) — Sets a specific LED (by index) to an RGB color. Changes don't appear until you call strip.show().

strip.ColorHSV(hue) — The rainbow effect uses HSV (Hue-Saturation-Value) color space, where rotating the hue from 0 to 65535 cycles through all colors. This is much easier than calculating RGB values for smooth color transitions.

strip.setBrightness(val) — Sets the global brightness (0–255). This scales all colors proportionally, so you can dim the strip without changing individual colors.


Troubleshooting

LEDs don't light up at all. Check the data wire direction — connect to the Data In end of the strip (follow the arrows). Also verify D4 is connected, and that 5V and GND are solid.

First LED lights, rest don't. The data signal isn't passing through. This can happen if the first LED is damaged. Try cutting off the first LED and connecting to the second one's Data In pad.

Colors are wrong (red and green swapped). Some strips use GRB order instead of RGB. The code already uses NEO_GRB — if your colors look wrong, try changing it to NEO_RGB in the Adafruit_NeoPixel constructor.

Web page doesn't load. Make sure your phone/computer is on the same Wi-Fi network as the ESP8266. The ESP8266 only supports 2.4 GHz — if your phone is on 5 GHz, it won't see the ESP's server.

LEDs flicker or show random colors. The WS2812B data protocol is timing-sensitive. The ESP8266 occasionally interrupts for Wi-Fi processing, which can cause glitches. Reducing the number of LEDs or adding a 300–500Ω resistor on the data line can help.

"Brownout" — ESP resets when LEDs turn on. The LEDs are drawing too much current for USB power. Reduce brightness with strip.setBrightness(40), or use fewer LEDs, or add an external 5V supply.


Where to Take It Next

Motion-reactive: Add a PIR sensor to change the strip color when someone enters the room, and fade to dim after 5 minutes of no motion.

Music visualizer: Connect a microphone module and map sound amplitude to LED brightness or color, creating a simple audio visualizer.

Notification light: Flash red when you have unread emails (poll a server), green when your build passes, or a custom pattern for each event.

Sunrise alarm: Gradually shift from warm orange to bright white over 30 minutes using the breathe effect with color transitions.

Multiple zones: Use longer strips with different LED ranges assigned to different rooms or areas, all controlled from the same web page.

Home Assistant integration: The web server's URL-based control (/color?r=255&g=0&b=0) makes it easy to integrate with Home Assistant's rest_command for full smart home automation.


The Bigger Picture

Step back and look at what's running here: a microcontroller smaller than a matchbox is serving a responsive web page, parsing HTTP requests, and controlling hardware — all simultaneously. The HTML, CSS, and JavaScript live inside the C++ sketch as a raw string. There's no separate server, no database, no deployment pipeline. It's the entire stack in one file.

The pattern — "ESP serves a page, user interacts, ESP controls hardware" — is the same one behind DIY smart thermostats, garage door controllers, and irrigation systems. The specifics change, but the architecture doesn't. You've got it now.


Part of the Arduino Intermediate series. Previous: Temperature MonitorWi-Fi Weather StationMotion-Activated LightDistance-Controlled ServoMelody Buzzer