Cheap Yellow Display with ESP Home

A few weeks ago I stumbled across the idea of a “Cheap Yellow Display” (CYD) which is an ESP32 development board with a small resistive touchscreen attached. As the name suggests, it’s cheap (usually $15–$20), and since it’s built around the ESP32, it immediately caught my attention. I’ve used ESP32 boards in several projects, and ESPHome has been treating me well lately, so this felt like a perfect little side experiment.

I didn’t have a specific use case in mind. At this price point, the goal was simply to get familiar with the hardware, learn how to drive the display and touchscreen, and eventually build toward something that integrates with Home Assistant.

Materials

The only thing I purchased was the CYD itself (a fully integrated ESP32 board with a 2.8″ resistive touchscreen). These come from various sellers with different USB connectors; I picked one with USB‑C.

  • 2.8” ESP32 Display Resistive Touch Screen – from Amazon

Preparing the CYD

The screen arrived in a small plastic case along with a stylus, a USB cable, and a 4-pin connector. When I powered it on for the first time, it booted into a preloaded demo application showcasing various UI elements such as buttons, selectors, charts, and lists. It actually reminded me of those Bootstrap-style demo pages you sometimes see in UI kits.

One detail I thought was particularly neat was the built-in frame rate counter. You could watch the FPS dip as more complex elements appeared on screen. That’s not surprising for a small microcontroller, but it was still interesting to see. The touchscreen also isn’t nearly as responsive as a modern smartphone, although that’s to be expected since it uses a resistive touchscreen rather than a capacitive one.

Here’s a quick video showing the demo.

The demo was fun to play with, but I couldn’t really picture that style of interface being practical on such a small display. Besides, I wasn’t interested in keeping the demo, I wanted to get ESPHome running on it.

While looking for a starting point, I came across an excellent post on the Decryption Blog, which pointed me to witnessmenow’s GitHub repository full of ESPHome examples for the CYD. Between those two resources, I had everything I needed to get started.

Installing ESP Home

I started the setup much the same way as my previous ESPHome projects. For the initial flash, I used the ESPHome Web Flasher. The only quirk was needing to hold the BOOT button until flashing began so that the board would enter programming mode.

Once that was complete, I switched over to “ESPHome Device Builder” in Home Assistant and took control of the device. This compiled and flashed the initial firmware, giving me a clean starting point to build from.

While the firmware was compiling, I also created a DHCP reservation on my router so the device would always receive the same IP address. (If you’re unfamiliar with the initial ESPHome setup process, I’ve covered it in more detail in a previous post.)

Here is the base config I started with
esphome:
  name: esp-cyd-screen
  friendly_name: CYD Screen
  min_version: 2026.4.0
  name_add_mac_suffix: false

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_key

# Allow Over-The-Air updates
ota:
  platform: esphome
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.229
    gateway: 192.168.1.1
    subnet: 255.255.255.0
  ap:
    password: !secret ap_password

binary_sensor:
 - platform: status
   name: "Status"

sensor:
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
  - platform: uptime
    type: seconds
    name: Uptime Sensor

text_sensor:
  - platform: wifi_info
    ssid:
      name: ESP Connected SSID
    mac_address:
      name: ESP Mac Wifi Address
  - platform: version
    name: "ESPHome Version"
    hide_timestamp: true

I won’t go into detail on each of these settings since they’re largely the same as the configuration I’ve used in previous ESPHome projects. The Wi-Fi, OTA updates, logging, API access, and diagnostic sensors all serve the same purpose here. (I’ve described these sensors in more detail in a previous post)

Once the device came online, I added it to Home Assistant and confirmed that all of the sensors were reporting correctly. (more details on the process here).

Using the Display

With the basics out of the way, it was finally time to make the screen earn its keep. Up to this point, the device was connected to Home Assistant and happily reporting sensors, but the display itself was still blank.

Showing the test image

The first step was simply getting something to appear on the screen. Following the Decryption Blog post I mentioned earlier, I started by configuring the display’s backlight.

output:
  - platform: ledc
    pin: GPIO21
    id: backlight_pwm

light:
  - platform: monochromatic
    output: backlight_pwm
    name: Display Backlight
    id: backlight
    restore_mode: ALWAYS_ON

This exposes the backlight as a light entity in Home Assistant while also ensuring the screen is actually visible. Having it exposed as an entity opens up some interesting possibilities for automation later on (for example, automatically dimming or turning off the display after a period of inactivity).

Next came the SPI configuration. The display communicates over SPI, so ESPHome needs to know which pins are connected to the controller.

spi:
  - id: tft
    clk_pin: GPIO14
    mosi_pin: GPIO13
    miso_pin: GPIO12

Once SPI was in place, all that remained was defining the display itself. ESPHome includes built-in support for the ILI9341 controller used by the CYD.

display:
  - platform: ili9xxx
    model: ILI9341
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    invert_colors: false
    color_palette: 8BIT
    auto_clear_enabled: true
    show_test_card: true

A few of these settings are worth calling out:

  • invert_colors determines whether the display colors should be inverted (required)
  • color_palette switches the display to an 8-bit color palette. This reduces memory usage compared to the default 16-bit mode, which is a better fit for the ESP32’s limited RAM.
  • auto_clear_enabled clears the display before each redraw. I’m honestly not sure it’s required for this simple example, but it doesn’t hurt to leave enabled.
  • show_test_card tells ESPHome to render a built-in test pattern instead of a custom interface.
Putting everything together, the configuration now looked like this
esphome:
  name: esp-cyd-screen
  friendly_name: CYD Screen
  min_version: 2026.4.0
  name_add_mac_suffix: false

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_key

# Allow Over-The-Air updates
ota:
  platform: esphome
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.229
    gateway: 192.168.1.1
    subnet: 255.255.255.0
  ap:
    password: !secret ap_password

binary_sensor:
 - platform: status
   name: "Status"

sensor:
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
  - platform: uptime
    type: seconds
    name: Uptime Sensor

text_sensor:
  - platform: wifi_info
    ssid:
      name: ESP Connected SSID
    mac_address:
      name: ESP Mac Wifi Address
  - platform: version
    name: "ESPHome Version"
    hide_timestamp: true

output:
  - platform: ledc
    pin: GPIO21
    id: backlight_pwm

light:
  - platform: monochromatic
    output: backlight_pwm
    name: Display Backlight
    id: backlight
    restore_mode: ALWAYS_ON

spi:
  - id: tft
    clk_pin: GPIO14
    mosi_pin: GPIO13
    miso_pin: GPIO12

display:
  - platform: ili9xxx
    model: ILI9341
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    invert_colors: false
    color_palette: 8BIT
    auto_clear_enabled: true
    show_test_card: true

After compiling and flashing the updated configuration, the display sprang to life with ESPHome’s built-in test card. It wasn’t exciting visually, but it confirmed that the ESP32 was communicating correctly with the display, which was exactly what I wanted before moving on.

Hello World Example

The test card proved that the hardware was working, but I wanted to render something of my own. One of the examples from witnessmenow’s repository was a simple “Hello World” application, which made for a perfect next step. Besides displaying some text, it also introduced custom fonts, colors, and ESPHome’s display lambda, which is where the real flexibility comes from.

I started with the configuration I had from the previous example and added on to it. The first thing I needed was a font. The example used a local TrueType font, so I downloaded it and copied it into my Home Assistant configuration. (I later discovered ESPHome can also download fonts directly from Google Fonts, which we’ll use later). I also defined a color matching Home Assistant’s blue.

# Create a font to use, add and remove glyphs as needed. 
font:
  - file: 'fonts/Arimo-Regular.ttf'
    id: arimo20
    size: 20
    glyphs: "!\"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖ abcdefghijklmnopqrstuvwxyzåäö"

# Create a Home Assistant blue color
color:
  - id: ha_blue
    hex: 51c0f2

From there, I updated the display configuration with a lambda. This is where ESPHome lets you embed small snippets of C++ to draw directly on the display.

display:
  - platform: ili9xxx
    model: ILI9341
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    rotation: 90
    invert_colors: false
    color_palette: 8BIT
    lambda: |-
      it.print(160, 140, id(arimo20), id(ha_blue), TextAlign::BOTTOM_CENTER, "Hello World!");
      it.rectangle(0, 0, 320, 240, id(ha_blue));

This example draws a blue rectangle and prints “Hello World!” centered on the screen. While it’s still a simple demo, it demonstrates the basic building blocks for creating a custom interface: drawing shapes, selecting fonts, choosing colors, and rendering text wherever you want it.

With those additions in place, the complete configuration looked like this:
esphome:
  name: esp-cyd-screen
  friendly_name: CYD Screen
  min_version: 2026.4.0
  name_add_mac_suffix: false

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_key

# Allow Over-The-Air updates
ota:
  platform: esphome
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.229
    gateway: 192.168.1.1
    subnet: 255.255.255.0
  ap:
    password: !secret ap_password

binary_sensor:
 - platform: status
   name: "Status"

sensor:
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
  - platform: uptime
    type: seconds
    name: Uptime Sensor

text_sensor:
  - platform: wifi_info
    ssid:
      name: ESP Connected SSID
    mac_address:
      name: ESP Mac Wifi Address
  - platform: version
    name: "ESPHome Version"
    hide_timestamp: true

output:
  - platform: ledc
    pin: GPIO21
    id: backlight_pwm

light:
  - platform: monochromatic
    output: backlight_pwm
    name: Display Backlight
    id: backlight
    restore_mode: ALWAYS_ON

spi:
  - id: tft
    clk_pin: GPIO14
    mosi_pin: GPIO13
    miso_pin: GPIO12

# Create a font to use, add and remove glyphs as needed. 
font:
  - file: 'fonts/Arimo-Regular.ttf'
    id: arimo20
    size: 20
    glyphs: "!\"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖ abcdefghijklmnopqrstuvwxyzåäö"

# Create a Home Assistant blue color
color:
  - id: ha_blue
    hex: 51c0f2

display:
  - platform: ili9xxx
    model: ILI9341
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    rotation: 90
    invert_colors: false
    color_palette: 8BIT
    lambda: |-
      it.print(160, 140, id(arimo20), id(ha_blue), TextAlign::BOTTOM_CENTER, "Hello World!");
      it.rectangle(0, 0, 320, 240, id(ha_blue));

After another compile and flash, I was greeted by my very own “Hello World” screen. It wasn’t much, but it marked the point where I stopped displaying ESPHome’s built-in examples and started controlling exactly what appeared on the display.

Using the Touchscreen

With the display working, the next obvious step was getting the touchscreen functional. Up to this point, everything had been one-way: the ESP32 could draw to the screen, but there was no way to interact with it yet.

That’s when I went back to the Decryption Blog post and realized the next step was going to be a bit more involved than I expected.

Calibrating the Touchscreen

This was the first part of the project that felt like real embedded debugging. The CYD uses an XPT2046 touch controller, which requires calibration before touch input becomes accurate. The calibration process is a little tedious, but you only need to do it once per screen.

The general process looks like this:

  1. Flash a test configuration that prints raw touch coordinates
  2. Tap each corner of the screen with a stylus
  3. Record the raw values
  4. Use those values to define calibration bounds

Luckily, the blog post I was following laid this out clearly enough that I didn’t have to reverse-engineer it.

There are a few pieces of config I had to add to interface with the touchscreen. First the SPI definition of how the controller should talk to the touchscreen (in addition to what I already had):

spi:
  - id: touch
    clk_pin: GPIO25
    mosi_pin: GPIO32
    miso_pin: GPIO39

then I added a touchscreen configuration that simply logged touch coordinates

touchscreen:
  platform: xpt2046
  id: my_touchscreen
  spi_id: touch
  cs_pin: GPIO33
  interrupt_pin: GPIO36
  calibration:
    x_min: 0
    x_max: 280
    y_min: 340
    y_max: 3860
  transform:
    swap_xy: true
  on_touch:
  - lambda: |-
      ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%d",
          touch.x,
          touch.y,
          touch.x_raw,
          touch.y_raw
      );
Here is the full calibration config
esphome:
  name: esp-cyd-screen
  friendly_name: CYD Screen
  min_version: 2026.4.0
  name_add_mac_suffix: false

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_key

# Allow Over-The-Air updates
ota:
  platform: esphome
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.229
    gateway: 192.168.1.1
    subnet: 255.255.255.0
  ap:
    password: !secret ap_password

binary_sensor:
 - platform: status
   name: "Status"

sensor:
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
  - platform: uptime
    type: seconds
    name: Uptime Sensor

text_sensor:
  - platform: wifi_info
    ssid:
      name: ESP Connected SSID
    mac_address:
      name: ESP Mac Wifi Address
  - platform: version
    name: "ESPHome Version"
    hide_timestamp: true

output:
  - platform: ledc
    pin: GPIO21
    id: backlight_pwm

light:
  - platform: monochromatic
    output: backlight_pwm
    name: Display Backlight
    id: backlight
    restore_mode: ALWAYS_ON

spi:
  - id: tft
    clk_pin: GPIO14
    mosi_pin: GPIO13
    miso_pin: GPIO12
  - id: touch
    clk_pin: GPIO25
    mosi_pin: GPIO32
    miso_pin: GPIO39

display:
  - platform: ili9xxx
    model: ILI9341
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    invert_colors: false
    color_palette: 8BIT
    auto_clear_enabled: true
    show_test_card: true

touchscreen:
  platform: xpt2046
  id: my_touchscreen
  spi_id: touch
  cs_pin: GPIO33
  interrupt_pin: GPIO36
  calibration:
    x_min: 0
    x_max: 280
    y_min: 340
    y_max: 3860
  transform:
    swap_xy: true
  on_touch:
  - lambda: |-
      ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%d",
          touch.x,
          touch.y,
          touch.x_raw,
          touch.y_raw
      );

With this flashed, every tap on the screen printed both calibrated and raw coordinates to the logs. Using the included stylus, I carefully tapped each corner and recorded the values.

The results looked something like this (your exact values will vary per unit):

[23:34:52.936][I][cal:107]: x=165, y=0, x_raw=193, y_raw=261
[23:35:11.883][I][cal:107]: x=133, y=318, x_raw=156, y_raw=3845
[23:35:26.152][I][cal:107]: x=239, y=317, x_raw=3759, y_raw=3839
[23:35:39.323][I][cal:107]: x=239, y=0, x_raw=3773, y_raw=240

Once I had those values, I looked at the raw x and y values and mapped them into the calibration block

calibration:
  x_min: 156
  x_max: 3773
  y_min: 240
  y_max: 3845

Hello World with Touch

With calibration working, the next step was to make the touchscreen actually do something. The post I was following had a simple example that toggles between two messages: “Hello World!” and “Goodbye World!” each time the screen is touched.

Similar to before, I needed a couple of supporting pieces: a font and a color definition. For this example, I used a font from Google Fonts instead of a locally sourced font

font:
  - file:
      type: gfonts
      family: Roboto
    id: roboto_large
    size: 40
    bpp: 4

color:
  - id: ha_blue
    hex: 51c0f2

Then I introduced a global variable to keep track of state. ESPHome globals are a simple but powerful way to store small bits of runtime state across updates (docs).

globals:
  - id: display_hello
    type: bool
    initial_value: 'true'

Now the interesting part: the display lambda. Instead of static text, the screen now renders different content based on that global state.

display:
  - platform: ili9xxx
    model: ILI9341
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    invert_colors: false
    color_palette: 8BIT
    rotation: 90
    auto_clear_enabled: true
    lambda: |-
      auto font = id(roboto_large);
      const char* text;

      if (id(display_hello)) {
        text = "Hello World!";
      } else {
        text = "Goodbye World!";
      }

      int x1, y1, text_width, text_height;

      it.get_text_bounds(0, 0, text, font, TextAlign::TOP_LEFT, &x1, &y1, &text_width, &text_height);

      int x = (it.get_width() - text_width) / 2;
      int y = (it.get_height() - text_height) / 2;

      it.print(x, y, font, id(ha_blue), TextAlign::TOP_LEFT, text);

Finally, I wired up the touchscreen event to flip that state every time the screen is touched:

touchscreen:
  platform: xpt2046
  id: my_touchscreen
  spi_id: touch
  cs_pin: GPIO33
  interrupt_pin: GPIO36
  update_interval: 50ms
  threshold: 400
  calibration:
    x_min: 156
    x_max: 3773
    y_min: 240
    y_max: 3845
  transform:
    swap_xy: true
  on_touch:
  - lambda: |-
      id(display_hello) = !id(display_hello);
Here is the full config with the new pieces added
esphome:
  name: esp-cyd-screen
  friendly_name: CYD Screen
  min_version: 2026.4.0
  name_add_mac_suffix: false

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_key

# Allow Over-The-Air updates
ota:
  platform: esphome
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.229
    gateway: 192.168.1.1
    subnet: 255.255.255.0
  ap:
    password: !secret ap_password

binary_sensor:
 - platform: status
   name: "Status"

sensor:
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
  - platform: uptime
    type: seconds
    name: Uptime Sensor

text_sensor:
  - platform: wifi_info
    ssid:
      name: ESP Connected SSID
    mac_address:
      name: ESP Mac Wifi Address
  - platform: version
    name: "ESPHome Version"
    hide_timestamp: true

output:
  - platform: ledc
    pin: GPIO21
    id: backlight_pwm

light:
  - platform: monochromatic
    output: backlight_pwm
    name: Display Backlight
    id: backlight
    restore_mode: ALWAYS_ON

spi:
  - id: tft
    clk_pin: GPIO14
    mosi_pin: GPIO13
    miso_pin: GPIO12
  - id: touch
    clk_pin: GPIO25
    mosi_pin: GPIO32
    miso_pin: GPIO39

font:
  - file:
      type: gfonts
      family: Roboto
    id: roboto_large
    size: 40
    bpp: 4

color:
  - id: ha_blue
    hex: 51c0f2

globals:
  - id: display_hello
    type: bool
    initial_value: 'true'

display:
  - platform: ili9xxx
    model: ILI9341
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    invert_colors: false
    color_palette: 8BIT
    rotation: 90
    auto_clear_enabled: true
    lambda: |-
      auto font = id(roboto_large);
      const char* text;

      if (id(display_hello)) {
        text = "Hello World!";
      } else {
        text = "Goodbye World!";
      }

      int x1, y1, text_width, text_height;

      it.get_text_bounds(0, 0, text, font, TextAlign::TOP_LEFT, &x1, &y1, &text_width, &text_height);

      int x = (it.get_width() - text_width) / 2;
      int y = (it.get_height() - text_height) / 2;

      it.print(x, y, font, id(ha_blue), TextAlign::TOP_LEFT, text);

touchscreen:
  platform: xpt2046
  id: my_touchscreen
  spi_id: touch
  cs_pin: GPIO33
  interrupt_pin: GPIO36
  update_interval: 50ms
  threshold: 400
  calibration:
    x_min: 156
    x_max: 3773
    y_min: 240
    y_max: 3845
  transform:
    swap_xy: true
  on_touch:
  - lambda: |-
      id(display_hello) = !id(display_hello);

After flashing this, I had a fully interactive screen: tap to toggle between two states, with the display updating immediately .(And confirmed that it works acceptably with a finger too not just the stylus).

It’s still a very simple interaction, but this is the point where the device stops being a “display” and starts feeling like a tiny UI system.

Touchscreen to navigate pages

Once I had basic touch input working, the next thing that caught my attention was an example from witnessmenow that implemented multiple pages and switched between them using touch input.

That version used button regions on the screen, but I decided to simplify things for now and just switch pages on any touch event. At this stage I wasn’t trying to build a polished UI but just exploring what ESPHome could handle comfortably.

I reused the font from earlier and kept the same color scheme, since it already worked well visually.

font:
  - file: 'fonts/Arimo-Regular.ttf'
    id: arimo24
    size: 24
    glyphs: "<>!\"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz"

color:
  - id: ha_blue
    hex: 51c0f2

From there, I defined three separate pages in the display configuration. Each page is just a simple lambda that fills the screen and prints a short label indicating which page is active.

display:
  - platform: ili9xxx
    id: esp_display
    model: ILI9341
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    invert_colors: false
    color_palette: 8BIT
    rotation: 90
    auto_clear_enabled: true
    pages:
      - id: page1
        lambda: |-
          it.fill(id(ha_blue));
          it.print(0, 10, id(arimo24), "This is the first page!");
      - id: page2
        lambda: |-
          it.fill(id(ha_blue));
          it.print(0, 10, id(arimo24), "This is the second page!");
      - id: page3
        lambda: |-
          it.fill(id(ha_blue));
          it.print(0, 10, id(arimo24), "This is the third page!");

Finally, I wired the touchscreen so that any touch cycles to the next page.

touchscreen:
  platform: xpt2046
  id: my_touchscreen
  spi_id: touch
  cs_pin: GPIO33
  interrupt_pin: GPIO36
  update_interval: 50ms
  threshold: 400
  calibration:
    x_min: 156
    x_max: 3773
    y_min: 240
    y_max: 3845
  transform:
    swap_xy: true
  on_touch:
    then:
      - display.page.show_next: esp_display
here is the full config

esphome:
  name: esp-cyd-screen
  friendly_name: CYD Screen
  min_version: 2026.4.0
  name_add_mac_suffix: false

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_key

# Allow Over-The-Air updates
ota:
  platform: esphome
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.229
    gateway: 192.168.1.1
    subnet: 255.255.255.0
  ap:
    password: !secret ap_password

binary_sensor:
 - platform: status
   name: "Status"

sensor:
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
  - platform: uptime
    type: seconds
    name: Uptime Sensor

text_sensor:
  - platform: wifi_info
    ssid:
      name: ESP Connected SSID
    mac_address:
      name: ESP Mac Wifi Address
  - platform: version
    name: "ESPHome Version"
    hide_timestamp: true

output:
  - platform: ledc
    pin: GPIO21
    id: backlight_pwm

light:
  - platform: monochromatic
    output: backlight_pwm
    name: Display Backlight
    id: backlight
    restore_mode: ALWAYS_ON

spi:
  - id: tft
    clk_pin: GPIO14
    mosi_pin: GPIO13
    miso_pin: GPIO12
  - id: touch
    clk_pin: GPIO25
    mosi_pin: GPIO32
    miso_pin: GPIO39

font:
  - file: 'fonts/Arimo-Regular.ttf'
    id: arimo24
    size: 24
    glyphs: "<>!\"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz"

color:
  - id: ha_blue
    hex: 51c0f2

display:
  - platform: ili9xxx
    id: esp_display
    model: ILI9341
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    invert_colors: false
    color_palette: 8BIT
    rotation: 90
    auto_clear_enabled: true
    pages:
      - id: page1
        lambda: |-
          it.fill(id(ha_blue));
          it.print(0, 10, id(arimo24), "This is the first page!");
      - id: page2
        lambda: |-
          it.fill(id(ha_blue));
          it.print(0, 10, id(arimo24), "This is the second page!");
      - id: page3
        lambda: |-
          it.fill(id(ha_blue));
          it.print(0, 10, id(arimo24), "This is the third page!");

touchscreen:
  platform: xpt2046
  id: my_touchscreen
  spi_id: touch
  cs_pin: GPIO33
  interrupt_pin: GPIO36
  update_interval: 50ms
  threshold: 400
  calibration:
    x_min: 156
    x_max: 3773
    y_min: 240
    y_max: 3845
  transform:
    swap_xy: true
  on_touch:
    then:
      - display.page.show_next: esp_display

After flashing the updated configuration, I could cycle through all three pages just by tapping the screen.

It’s a simple interaction, but it changes the feel of the device quite a bit. Instead of a single static screen or a single toggle, it starts to resemble a very minimal UI framework—something closer to a menu system.

One thing that stood out here is how clean ESPHome makes this pattern. Once the pages are defined, switching between them is essentially a single action.

I also noticed that the Google Fonts version of Roboto (used earlier in the project) looked noticeably sharper compared to the Arimo font. It’s a small detail, but it makes the UI feel a bit more modern even with minimal styling.

Conclusion

Overall, this first round of experimentation confirmed what I was hoping: ESPHome makes it surprisingly easy to build interfaces on these inexpensive displays. The examples here were intentionally simple, but they gave me a solid understanding of the display pipeline, touch input, and how ESPHome mixes YAML with small snippets of C++.

One thing that still feels slightly awkward is writing inline C++ inside YAML. It works well enough for small pieces of logic, but the lack of syntax highlighting and tooling makes it feel a bit rough compared to a traditional development environment. Even so, for this kind of embedded UI work, it’s a reasonable trade-off.

Next, I want to move beyond demos and build something genuinely useful. The goal is to create a small Home Assistant dashboard that can display sensor data and interact with devices directly from the screen. That should be a much more interesting test of what this little $15 display is capable of.