Digging through AliExpress, I came across this interesting LILYGO T-Display S3 AMOLED screen board. The T-Display S3 is a development board featuring an ESP32-S3 microcontroller and a built-in AMOLED display, ideal for IoT projects with vibrant visual output and more then enough processing power for small projects.

In this article, I’m sharing my process of flashing, setting up and rendering content on this AMOLED development board using ESPHome’s display engine. You can use it to push data from Home Assistant, create neat status displays and even use it as a controller.
The LILYGO T-Display-S3 AMOLED version is available on AliExpress as well as Amazon.
Different Versions of the LILYGO T-Display S3
Before diving further, it’s important to note that both versions of this board—Basic and Touch—are almost the same, with only a few differences in pinouts and features. These distinctions primarily relate to the touch functionality in the Touch version with it having an additional Touch_INT pin on GPIO21 and a Power Enable pin on GPIO38 to support the touchscreen, which are not present in the Basic version.
Currently, version 2.0 of this board has the following pinout:

- LILYGO T-Display S3 AMOLED Basic
- ESP32-S3R8 Dual-core LX7
- AMOLED 1.91 inches 240×536
- QSPI RM67162
- 16MB Flash, 8MB PSRAM
- Wi-Fi 802.11, BLE 5.0

- LILYGO T-Display S3 AMOLED Touch
- ESP32-S3R8 Dual-core LX7
- AMOLED Touch 1.91 inches 240×536
- QSPI RM67162
- 16MB Flash, 8MB PSRAM
- Wi-Fi 802.11, BLE 5.0
This guide is based on the AMOLED Basic version.
Initial Flash in ESPHome
Before we can setup the display component and actually render things on this board, it needs to be initialized in ESPHome. Create a new device in ESPHome, name it whatever you like and initialize it with the following code:
esphome:
name: lilygos3
friendly_name: LILYGO-S3 T-Display
platformio_options:
build_unflags: -Werror=all
board_build.flash_mode: dio
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
framework:
type: esp-idf
api:
encryption:
key: "XXXXX" #Replace
ota:
- platform: esphome
password: "XXXXX" #Replace
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "S3 Fallback Hotspot"
password: "zQ9tuPKIfFMu"
logger:
After the initial flash, nothing will be rendered on the screen. Since we haven’t configured the pinout for the spi
and display
components, the board will simply bootup and you will be able to access it wirelessly from your network.
The next step is to configure the display component, add a text font and render something simple using ESPHome’s display rendering engine. For this to work, the proper pins need to be set. From the official pinout images, here’s how the configuration should look like for the LILYGO T-Display S3 Basic Version:
spi:
id: qspi_bus
type: quad
clk_pin: GPIO47
data_pins:
- GPIO18
- GPIO7
- GPIO48
- GPIO5
display:
- platform: qspi_amoled
model: RM67162
id: s3_display
update_interval: 1s
dimensions:
height: 536
width: 240
color_order: rgb
brightness: 255
cs_pin: GPIO6
reset_pin: GPIO17
rotation: 0
lambda: |-
it.print(10, 20, id(font_roboto), Color(255, 255, 255), "Hello, SmartHomeScene!");
#Add a simple Google Font
font:
- file: "gfonts://Roboto"
id: font_roboto
size: 20
If you get something on the display, it means your configuration is correct and working:

Understanding ESPHome’s Display Rendering Engine
In order to create something useful on the LILYGO T-Display S3, ESPHome’s display rendering engine needs to be used. This engine can handle tasks like drawing shapes, displaying text with custom fonts, and even rendering images. This flexibility is achieved through ESPHome’s lambda system, which allows you to write C++ code to control the display.
In the lambda, you can write code like in any lambda in ESPHome. Display lambdas are additionally passed a variable called it
which represents the rendering engine object. When rendering display coordinates in ESPHome, the origin (0, 0) starts from the top-left corner of the screen. X-coordinates increase to the right, while Y-coordinates increase downward. Colors are rendered using RGB values, typically in the format 1
, where values range from 0-255 for each channel. Fonts are defined by specifying typefaces and sizes, allowing you to print text in various styles. Images are pushed to the display using bitmap data, typically preloaded or fetched via code, and rendered based on coordinate placement.
Here’s how a red circle centered in the middle looks like, using two helpers to get width
and height
of the screen automatically:
lambda: |-
// Draw a red circle in the middle of the display
it.filled_circle(it.get_width() / 2, it.get_height() / 2, 120, Color(255, 0, 0));

Rotating the screen
Before the screen can be fully useful, it’s important to understand the rotation configuration parameter in ESPHome. You can add a rotation
to the screen and set it to 90
, 180
or 270
degrees in order to flip the components on the screen.
It’s very important to note that rotation is tightly correlated with the get_width()
and get_height()
helpers in ESPHome. When set to 90 or 270 degrees, the width and height are swapped, meaning what was previously considered the width (longer axis) becomes the height and vice versa. This ensures the dimensions adapt correctly to the display’s orientation, allowing proper rendering regardless of the rotation angle.
For example, if I draw a simple triangle spanning the available width and height, starting with 0, 0 coordinates, here’s how the helper would influence it’s size:




Since the screen’s width and height (X and Y axes) are fetched dynamically, the triangle’s size will vary based on the display dimensions. Keep this in mind when rendering content, as it can affect the layout. This will help you avoid frustrations and the need to constantly re-flash the device during testing.
Adding Fonts
Fonts can be added to ESPHome’s display component by uploading them locally or simply using Google Fonts. You need to assign IDs to each font, define it’s size and any extra characters if necessary. The ID is called in the lambda function of the display component, which will render the text you are asking it to, with the corresponding font. Here’s how fonts can be defined:
font:
- file: "fonts/Comic Sans MS.ttf"
id: comic_sans_20
size: 20
bpp: 2
- file: "gfonts://Roboto"
id: roboto_20
size: 20
- file:
type: gfonts
family: Roboto
weight: 900
id: roboto_16
size: 16
....
You can check out more examples and configuration parameters in ESPHome’s Font Component.
Adding Images and Icons
Very similarly to fonts, images can be loaded onto ESP32 devices with compatible screens. The Image Component also requires a unique ID, which is called upon in the lambda function of the display component. Here’s how images or icon can be defined and resized:
image:
- file: "smarthomescene.png" #Local image
id: my_image
resize: 100x100
type: RGBA #full color image
image:
- file: mdi:alert-outline #MDI Icon
id: alert
resize: 80x80
image:
- file: https://esphome.io/_images/logo.png #URL image
id: esphome_logo
resize: 200x162
For example, let’s say I want to render a local image at the center of the LILYGO T-Display S3. I need to upload the image in the ESPHome
folder where the main .yaml
file is located. Or, I could use another folder within this directory.

display:
.......
lambda: |-
// Render the image in the center of the display
it.image(it.get_width() / 2 - 175, it.get_height() / 2 - 77, id(smarthomescene));
image:
- file: "the-logo.png"
id: smarthomescene
resize: 350x155
type: RGBA
To center the image on the S3 display, you need to calculate the offset from the display’s center by subtracting half the image’s dimensions. First, divide the display width by 2 to get the horizontal center. Then, subtract half the image’s width to position it correctly. For example, with a 350-pixel wide image (the-logo.png), you subtract 175 (half the width). The same applies vertically: divide the display height by 2 and subtract half the image’s height (77 for a 155-pixel tall image). This ensures the image is centered on both axes.
Exposing Entities from Home Assistant
Just like any other ESPHome device, you can expose sensors and entities from Home Assistant to the LILYGO S3 and use it as a status display or controller. This can include any entity you wish, preferably some that you find a useful functionality for. For example, let’s say I want to use this display to showcase my office temperature and humidity with some nice icons. I want them laid out horizontally in the middle of the screen on the right side with an interesting and readable font. I want to keep the logo smaller on the left side:

First, I need to make the two sensors from Home Assistant available to the LILYGO board by adding this to the configuration:
sensor:
- platform: homeassistant
entity_id: sensor.office_sensor_temperature
id: office_temperature
name: "Office Temperature"
- platform: homeassistant
entity_id: sensor.office_sensor_humidity
id: office_humidity
name: "Office Humidity"
Next, I need to add the font and images (and icons) with an appropriate size. 60×60 pixels works nicely for the icons in my example, while keeping the image at 200 width corresponds to about half the display with some space in between:
font:
- file: "gfonts://Audiowide"
id: audiowide_font
size: 40
image:
- file: mdi:thermometer
id: temperature_icon
resize: 60x60
- file: mdi:water_percent
id: humidity_icon
resize: 60x60
- file: "the-logo.png"
id: smarthomescene
resize: 200x88
type: rgba
And finally, add the display rendering lambda to the code. This function uses the get_height
helper to read screen height and use it to center the image vertically (smarthomescene). The rest of the function statically sets the X and Y coordinates for the sensors with the appropriate fonts and positions them on the screen:
lambda: |-
int screen_height = it.get_height();
it.image(20, (screen_height - 88) / 2, id(smarthomescene));
it.image(260, 40, id(temperature_icon));
it.printf(320, 40, id(audiowide_font), Color(255, 255, 255), "%.1f°C", id(office_temperature).state);
it.image(260, 140, id(humidity_icon));
it.printf(320, 140, id(audiowide_font), Color(255, 255, 255), "%.1f%%", id(office_humidity).state);
Cycling Multiple Display Pages
ESPHome’s display component allows you to render multiple pages with different contents and cycle through them on a timer or on any input events. As the LILYGO T-Display S3 AMOLED display is equipped with two buttons on the front, we can use one of the buttons to change the information shown on the screen. The first button uses GPIO0
, which is a strapping pin so we want to avoid that. The second one uses GPIO21
, which is free to be used in this automation.
For this example, I created a second page with a greeting message, kept the original one with temperature and humidity as a second and created a third page with some basic info of my system monitor:

display:
- platform: qspi_amoled
...........
pages:
- id: page1
lambda: |-
it.printf(10, 100, id(audiowide_font), Color(255, 255, 255), "Hello SmartHomeScene!");
- id: page2
lambda: |-
int screen_height = it.get_height();
it.image(20, (screen_height - 88) / 2, id(logo));
it.image(260, 40, id(temperature_icon));
it.printf(320, 40, id(audiowide_font), Color(255, 255, 255), "%.1f°C", id(office_temperature).state);
it.image(260, 140, id(humidity_icon));
it.printf(320, 140, id(audiowide_font), Color(255, 255, 255), "%.1f%%", id(office_humidity).state);
- id: page3
lambda: |-
// CPU Usage
it.image(20, 20, id(cpu_icon)); // CPU icon
it.printf(100, 20, id(audiowide_font), Color(255, 255, 255), "CPU: %.1f%%", id(system_monitor_processor_use).state);
// Memory Usage
it.image(20, 100, id(memory_icon)); // Memory icon
it.printf(100, 100, id(audiowide_font), Color(255, 255, 255), "RAM: %.1fMB", id(system_monitor_memory_use).state);
// Disk Usage
it.image(20, 180, id(harddisk_icon)); // Disk icon
it.printf(100, 180, id(audiowide_font), Color(255, 255, 255), "DISK: %.1fGB", id(system_monitor_disk_use).state);
#Button on GPIO21 as trigger for page cycling
binary_sensor:
- platform: gpio
pin: GPIO21
name: "Page Switch Button"
on_press:
then:
- display.page.show_next: s3_display
- component.update: s3_display
# Fonts and icons for each image used
font:
- file: "gfonts://Audiowide"
id: audiowide_font
size: 40
image:
- file: mdi:thermometer
id: temperature_icon
resize: 60x60
- file: mdi:cpu-64-bit
id: cpu_icon
resize: 60x60
............
# Exposed sensors from Home Assistant
sensor:
- platform: homeassistant
entity_id: sensor.office_sensor_temperature
id: office_temperature
name: "Office Temperature"
name: "CPU Usage"
- platform: homeassistant
entity_id: sensor.system_monitor_memory_use
id: system_monitor_memory_use
name: "Memory Usage"
.............
Summary
There’s so much that can be done with ESPHome’s display rendering engine. It’s a powerful and flexible tool, but hardly simple to use. In my experience, it requires a lot of flashing, tweaking and re-flashing to get the desired effect you are after. If this article is not enough to get you started, I highly suggest you go over the official documentation and try to understand at least the most basic parameters before flashing this LILYGO T-Display S3 AMOLED screen.
The LILYGO T-Display S3 AMOLED development board is available on AliExpress and Amazon. Here’s a few links of where you can purchase the device:

AliExpress | AliExpress | AliExpress
United States | United Kingdom
Germany | Netherlands | France
Italy | Spain | Sweden
*If links fail to open, try disabling your AdBlocker.
After tweaking my code so many times, I managed to display some interesting data about my office setup. Here’s the full config from this article:
Full config example (click to expand)‎
esphome:
name: lilygos3
friendly_name: LILYGO-S3 T-Display
platformio_options:
build_unflags: -Werror=all
board_build.flash_mode: dio
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
framework:
type: esp-idf
api:
encryption:
key: "XXXXX"
ota:
- platform: esphome
password: "XXXXX"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "S3 Fallback Hotspot"
password: "XXXXX"
logger:
level: WARN
# Define QSPI interface for the AMOLED
spi:
id: qspi_bus
type: quad
clk_pin: GPIO47
data_pins:
- GPIO18 # TFT_D0
- GPIO7 # TFT_D1
- GPIO48 # TFT_D2
- GPIO5 # TFT_D3
# Configuration for the RM67162 AMOLED display
display:
- platform: qspi_amoled
model: RM67162
id: s3_display
update_interval: 10s
dimensions:
height: 536
width: 240
color_order: rgb
brightness: 255
cs_pin: GPIO6
reset_pin: GPIO17
rotation: 270
pages:
- id: page1
lambda: |-
it.printf(10, 100, id(audiowide_font), Color(255, 255, 255), "Hello SmartHomeScene!");
- id: page2
lambda: |-
int screen_height = it.get_height();
it.image(20, (screen_height - 88) / 2, id(logo));
it.image(260, 40, id(temperature_icon));
it.printf(320, 40, id(audiowide_font), Color(255, 255, 255), "%.1f°C", id(office_temperature).state);
it.image(260, 140, id(humidity_icon));
it.printf(320, 140, id(audiowide_font), Color(255, 255, 255), "%.1f%%", id(office_humidity).state);
- id: page3
lambda: |-
// CPU Usage
it.image(20, 20, id(cpu_icon)); // CPU icon
it.printf(100, 20, id(audiowide_font), Color(255, 255, 255), "CPU: %.1f%%", id(system_monitor_processor_use).state);
// Memory Usage
it.image(20, 100, id(memory_icon)); // Memory icon
it.printf(100, 100, id(audiowide_font), Color(255, 255, 255), "RAM: %.1fMB", id(system_monitor_memory_use).state);
// Disk Usage
it.image(20, 180, id(harddisk_icon)); // Disk icon
it.printf(100, 180, id(audiowide_font), Color(255, 255, 255), "DISK: %.1fGB", id(system_monitor_disk_use).state);
binary_sensor:
- platform: gpio
pin: GPIO21 # Button on GPIO21
name: "Page Switch Button"
on_press:
then:
- display.page.show_next: s3_display
- component.update: s3_display
font:
- file: "gfonts://Audiowide"
id: audiowide_font
size: 40
image:
- file: mdi:thermometer
id: temperature_icon
resize: 60x60
- file: mdi:water_percent
id: humidity_icon
resize: 60x60
- file: "the-logo.png"
id: logo
resize: 200x88
type: rgba
- file: mdi:tape-drive
id: harddisk_icon
resize: 60x60
- file: mdi:memory
id: memory_icon
resize: 60x60
- file: mdi:cpu-64-bit
id: cpu_icon
resize: 60x60
sensor:
- platform: homeassistant
entity_id: sensor.office_sensor_temperature
id: office_temperature
name: "Office Temperature"
- platform: homeassistant
entity_id: sensor.office_sensor_humidity
id: office_humidity
name: "Office Humidity"
- platform: homeassistant
entity_id: sensor.system_monitor_processor_use
id: system_monitor_processor_use
name: "CPU Usage"
- platform: homeassistant
entity_id: sensor.system_monitor_memory_use
id: system_monitor_memory_use
name: "Memory Usage"
- platform: homeassistant
entity_id: sensor.system_monitor_disk_use
id: system_monitor_disk_use
name: "Disk Usage"
#Toggle green LED under the left button
light:
- platform: binary #Toggle green LED under the left button
name: "Green LED"
output: green_led
output:
- platform: gpio
pin: GPIO38
id: green_led
Awesome article, as usual. Thank you!
I’m curious about the touch screen capability. Can you program button or switch elements to display and use touch to activate and deactivate them?
Yes, you can! Although, I don’t have the touch version to be able to test how good it works.
Any reviews/guide coming up from that Yellow ESP 2.8 inch display.
its around 10 euro’s and should be suitable for a all in one solution, if you want a simple display for sensor information.
Yeah, I have it on my to-do list. Works quite well, too!
Wow! Excellent timing and excelent intro! I was looking at this few weeks ago as a solution to display outside temp in the lobby of our house because there aren’t any not pricy smart OoB solutions for that and I came through this but I found that too much complicated for the setup. Thanks to this article I know the basics and I’m confident in making it and I think I van even extent my original plan to show weather forcast, travel time to work from Waze and upcomming bus departures.
Yes, there is no limit actually to what you can achieve with this. The touch version is even more flexible.
I want to re-do the whole thing in LGVL, which should be easier and much more capable.
Wow! LGVL looks like a way! Thanks! Singles day is here, so I’m going to buy that 😄
Once again a great article, thank you!!
Thank you for the feedback! Appreciated!
I’ve been looking for a functional code for about 2 years and now I could finally make this screen to work. Thank you so much, I was about to throw away these boards.
Humm.. as soon as I added the DISPLAY configuration the captive web portal stop working and the OTA update also is struggling to make changes. Something on this DISPLAY config interferes with the device capabilities to run its internal webserver.