Using Eta.js Templates
Editor for ESPHome uses the Eta.js templating engine to dynamically generate YAML.
This makes it possible to avoid repetition, keep your configs modular, and build reusable building blocks for your devices.
Why Templates?
ESPHome YAML can get repetitive fast.
With templates you can:
- Reuse blocks of code across devices
- Dynamically change values (IDs, names, pins)
- Use conditions and loops to generate many similar components
- Keep large configs clean and maintainable
File Extension
Eta templates must use the .eta extension.
When the editor builds the final config:
.etafiles are rendered into YAML- Variables and logic inside them are processed
- The generated YAML is merged with other files
Example
Let's look at an example that will explain the usage of Eta templating
my-device/
├── .lib/
| ├── inputs.eta
| └── header.eta
└── device.eta
- .lib/inputs.eta
- .lib/header.eta
- device.eta
- Compiled code
binary_sensor:
<% for (let i = 0; i < it.count; i++) { %>
- platform: gpio
pin: <%= i %>
name: "Input <%= i %>"
<% } %>
<%
const ssid = it.ssid || "default_ssid"
const password = it.password || "changeme"
-%>
wifi:
ssid: "<%= ssid %>"
password: "<%= password %>"
<%~ include('./.lib/header', { password: "pass" }) %>
<%~ include('./.lib/inputs', { count: 7 }) %>
The final code will have wifi with default_ssid/pass credentials and there will be 7 GPIO inputs
wifi:
ssid: "default_ssid"
password: "pass"
binary_sensor:
- platform: gpio
pin: 0
name: "Input 0"
- platform: gpio
pin: 1
name: "Input 1"
- platform: gpio
pin: 2
name: "Input 2"
- platform: gpio
pin: 3
name: "Input 3"
- platform: gpio
pin: 4
name: "Input 4"
- platform: gpio
pin: 5
name: "Input 5"
- platform: gpio
pin: 6
name: "Input 6"
Basic Syntax
Eta uses JavaScript syntax inside special tags:
<% ... %>— execute JavaScript code (no output)<%= ... %>— evaluate and print a value
Loops
Loops are especially useful when generating multiple sensors, switches, or pins.
- file.eta
- file.yaml
binary_sensor:
<% for (let i = 0; i < 4; i++) { %>
- platform: gpio
pin: <%= i %>
name: "Input <%= i %>"
<% } %>
binary_sensor:
- platform: gpio
pin: 0
name: "Input 0"
- platform: gpio
pin: 1
name: "Input 1"
- platform: gpio
pin: 2
name: "Input 2"
- platform: gpio
pin: 3
name: "Input 3"
Conditionals
You can include YAML conditionally based on variables:
- file.eta
- file.yaml
<% if (it.debug) { %>
logger:
level: DEBUG
<% } else { %>
logger:
level: WARN
<% } %>
When imported with <%~ include('./.lib/file', { debug: true }) %>
logger:
level: DEBUG
Includes and Reuse
You can split templates into smaller files and include them where needed.
For shared code, store files in device or global .lib/ and import them whenever needed.
<%~ include('./.lib/file', { key: value }) %>- imports from devices
.libfolder
- imports from devices
<%~ include('./../.lib/file', { key: value }) %>- imports from global
.libfolder
- imports from global
Best Practices
- ✅ Keep logic simple — use Eta for structure, not business logic
- ✅ Group related components into their own template files
- ✅ Use
.lib/for shared templates across devices - ✅ Comment your templates for clarity
- ❌ Avoid overly complex JavaScript — configs should stay easy to read
Next Steps
- For more details about Eta.js check the official documentation or a (cheatsheet)[https://eta.js.org/docs/intro/syntax-cheatsheet]
- Explore Alternates to enable/disable files and folders for different device variants