How to Use an ESP32 Toggle Button to Control LEDs with a Single Press

Learn how to use an ESP32 toggle button to control LED states—because sometimes, your circuit remembers things you didn’t ask it to.

The ESP32 toggle button seems simple—until it starts acting like it remembers your last press a little too well.

The ESP32 toggle button seems simple—until it starts remembering your last move like it’s got a grudge.

One Button, Infinite Bugs

I still remember the first time I tried wiring up an ESP32 toggle button to control an LED. The goal was simple: press once, light turns on; press again, light turns off. But instead of behaving like a friendly switch, it turned into a petty little gremlin—randomly flipping states, ghosting my inputs, and sometimes refusing to respond at all.

Classic toggle logic fail.
Or, more accurately: classic lack-of-debounce fail.

It was one of those “why won’t this basic thing just work” moments that reminds us—embedded systems don’t forgive poor logic. But that frustration also led to something better: a cleaner way to use an ESP32 toggle button that actually works, is open-source friendly, and teaches more than just wiring.

Whether you’re building a desk lamp toggle, a state machine trigger, or just learning how ESP32 handles GPIOs—this guide will walk you through the circuit, code, and logic to make a single button do exactly what you want.

Let’s make your button behave—with FOSS, finesse, and just one press.
Read on for the full walkthrough.

What Exactly Is a Toggle Button? (And Why It’s Not What You Think)

A toggle button isn’t a special kind of hardware—it’s a software trick that lets a regular momentary pushbutton behave like a switch. Push once, it turns something on. Push again, it turns it off. That’s it.

Physically, the button is just a momentary contact switch—it only connects the circuit while you’re pressing it. But the ESP32 “remembers” the last state of whatever it’s controlling, and flips it each time you press the button. That’s what creates the toggle effect.

In short: you turn a dumb button into a smart switch—purely in code.

This is a powerful little trick in embedded systems. It helps you reduce hardware costs (fewer buttons), simplify UI, and open up creative control schemes, all while staying true to the FOSS ethos of doing more with less.

· · ─ ·𖥸· ─ · ·

Debouncing 101: Why Your ESP32 Thinks You Pressed the Button Five Times

Here’s the deal: mechanical buttons aren’t perfect. When you press one, it doesn’t just make a clean, crisp connection—it bounces. That means the contacts rapidly connect and disconnect several times within milliseconds.

To your ESP32, that bounce looks like multiple presses.

This is where debouncing comes in. You basically tell the ESP32:

“Hey, if the button just changed state, ignore any further input for, say, 50 milliseconds.”

There are two common ways to do this:

  • Using delay(): Simple, but it halts your code.
  • Using millis(): Non-blocking, lets your code keep running while waiting out the bounce.

This guide uses the millis() method because it’s cleaner, more efficient, and FOSS-friendly—great for when your project needs to scale or multitask.

· · ─ ·𖥸· ─ · ·

Wiring 101: The Mysterious Pull-Up Resistor, Explained

So you connect one side of your button to GND, the other to a GPIO pin, and it works.
But why does it work?

Here’s the magic: the ESP32 pin is set to INPUT_PULLUP, which activates an internal pull-up resistor. This keeps the pin at a HIGH voltage when the button isn’t pressed. When you press the button, it connects to GND, pulling the voltage LOW. Simple.

Why not leave the pin floating?
Because without the pull-up, the pin could randomly read HIGH or LOW from electrical noise—ghost inputs. Not fun.

This wiring method is reliable, cheap (no external resistors needed), and just works. If you’re building with students or in low-resource labs, this kind of internal pull-up magic is your new best friend.

Components Needed

  • ESP32 development board
  • 1 LED connected to pin 21 (or your preferred GPIO pin)
  • 1 Button connected to pin 15 (or your preferred GPIO pin)
  • Breadboard and jumper wires

Wiring the Components

Here’s how to wire up the ESP32, button, and LED for this tutorial:

  • Button: One side of the button is connected to pin 15 and the other side to GND.
  • LED: The anode (long leg) of the LED is connected to pin 21, and the cathode (short leg) is connected to GND.
  • Optional: Use a 10kΩ pull-up or pull-down resistor to ensure proper button input handling.

· · ─ ·𖥸· ─ · ·

Program to Toggle LED with Button Press

In this example, we will use a button to toggle the state of an LED connected to pin 21. Each press of the button will toggle the LED on and off. We will track the button’s state with a simple counter that changes the LED’s state.

Here’s the MicroPython code for this:

from machine import Pin
from time import sleep

# PINS
led1 = Pin(21, Pin.OUT)  # LED connected to pin 21
btn1 = Pin(15, Pin.IN, Pin.PULL_UP)  # Button connected to pin 15 with pull-up resistor

# State tracking variable
button_state = 0
led_state = 0

while True:
    # Read the current button state
    state = btn1.value()
    
    # Check if the button is pressed
    if state == 0:  # Button pressed (LOW due to pull-up)
        # Toggle the LED state
        led_state = not led_state
        if led_state:
            led1.on()  # Turn LED on
        else:
            led1.off()  # Turn LED off
        
        # Wait a bit to debounce the button
        sleep(0.2)
    sleep(0.1)

Explanation of the Code

  1. Pin Setup:
    • led1 = Pin(21, Pin.OUT): This sets pin 21 as an output pin for the LED.
    • btn1 = Pin(15, Pin.IN, Pin.PULL_UP): This sets pin 15 as an input for the button, with an internal pull-up resistor. This means the button will read as LOW when pressed.
  2. State Tracking:
    • button_state tracks the button’s state.
    • led_state tracks whether the LED is ON (1) or OFF (0).
    • The state of the LED toggles each time the button is pressed.
  3. Button Press Logic:
    • If the button is pressed (state == 0), the led_state toggles between 1 (LED ON) and 0 (LED OFF).
    • The LED is turned on with led1.on() and turned off with led1.off().
  4. Debouncing:
    • The sleep(0.2) line helps debounce the button by delaying for a short period. This ensures the LED doesn’t toggle multiple times for a single press.
  5. Main Loop:
    • The loop continuously checks the button’s state, toggles the LED accordingly, and waits briefly before checking again.

Adding More LEDs

If you want to control multiple LEDs using the same button, you can modify the code to toggle between several LEDs as shown in the following example:

from machine import Pin
from time import sleep

# PINS
led1 = Pin(21, Pin.OUT)  # LED 1 connected to pin 21 (RED)
led2 = Pin(19, Pin.OUT)  # LED 2 connected to pin 19 (GREEN)
led3 = Pin(18, Pin.OUT)  # LED 3 connected to pin 18 (BLUE)
btn1 = Pin(15, Pin.IN, Pin.PULL_UP)  # Button connected to pin 15

# State tracking variable
counter = 0

while True:
    # Read the button state
    state = btn1.value()

    if state == 0:  # Button pressed
        counter += 1  # Increment the counter
        
        if counter == 1:
            led1.on()  # Turn on LED 1
            led2.off()  # Turn off LED 2
            led3.off()  # Turn off LED 3
        elif counter == 2:
            led1.off()  # Turn off LED 1
            led2.on()   # Turn on LED 2
            led3.off()  # Turn off LED 3
        elif counter == 3:
            led1.off()  # Turn off LED 1
            led2.off()  # Turn off LED 2
            led3.on()   # Turn on LED 3
        else:
            led1.off()  # Turn off LED 1
            led2.off()  # Turn off LED 2
            led3.off()  # Turn off LED 3
            counter = 0  # Reset the counter

        # Debounce the button
        sleep(0.2)

In this example, each button press toggles between three LEDs connected to pins 21, 19, and 18. After cycling through the LEDs, the counter resets, and the cycle starts again.

· · ─ ·𖥸· ─ · ·

Smart Control, Open-Source Logic

Toggling an LED with an ESP32 toggle button might look like beginner-level stuff—but under the hood, it’s a crash course in edge detection, state logic, and responsive design. You’ve now seen how even a single input can teach you volumes about hardware-software interaction.

The best part? You built this the FOSS way—using open, transparent methods anyone can audit, extend, or teach.

So the next time your ESP32 starts acting like it “remembers your last press like it’s got a grudge”—you’ll know why. And more importantly, you’ll know how to fix it.

Like simple builds with real-world impact?

Then you’ll love what’s coming next.

Subscribe to the newsletter for exclusive how-tos, field-tested tools, and open-source gear walkthroughs—all crafted for hackers, educators, and everyday devs who believe tech should serve people, not profit.

Like what you’re reading?
Help keep DevDigest
free and caffeine-powered
—buy me a coffee on Ko-fi.

Leave a Reply

Your email address will not be published. Required fields are marked *

Comments (

)

  1. Grit

    Thank you for writing this article. I appreciate the subject too.

    1. Sam Galope

      You’re very welcome! I’m glad you enjoyed the article and found the topic interesting. If you’re into more hands-on projects, you might like this guide as well:

      How to Monitor Soil Moisture Levels with an ESP32 and Soil Moisture Sensor using MicroPython

      Thanks for reading, and I appreciate your support! 🚀