How to write LVGL Pro XML. This is the UI language of LVGL Pro: HTML-like markup that the Editor or the CLI turns into plain LVGL C code.
- Never invent an attribute. Every widget's exact API lives in
lvgl_widgets_xml/<version>/lv_*.xml. Read it before writing. Style properties and enums are inglobals.xmlin the same folder. - Match the project's LVGL version. If
project.xmldeclareslvgl_version="9.5.0"use thev9.5.0/schema folder. - Validate what you write.
lvglpro validate <project>gives precise errors. Thenscreenshotto see it. Guessing is not the same as knowing. - Reuse before you create. Look at the project's existing components and
globals.xmlfirst. A design system usually already has the button, the card, and the spacing scale you were about to reinvent.
| Root tag | What it is | Can hold |
|---|---|---|
<component> |
Reusable UI element, pure XML, no C. The workhorse. | animations, consts, api, styles, view, previews |
<screen> |
A full screen. Created as-is, no parameters. | consts, styles, view (no api, no previews) |
<widget> |
A widget backed by handwritten C. Needs a C parser, cannot be loaded from XML at runtime, and needs a recompile of the preview. | consts, api, styles, view, previews |
One file per element, and the filename becomes the name you use as a tag. my_button.xml is used as <my_button/>.
Write components unless you truly need C. Widgets require a C implementation plus an XML parser; reach for one only when the behavior cannot be expressed as composition plus data binding.
my_project/
├── project.xml ← targets and display sizes
├── globals.xml ← shared consts, styles, fonts, images, subjects
├── translations.xml ← optional
├── fonts/ images/
├── widgets/ components/ screens/
project.xml and globals.xml sit at the root. All src_path values are relative to that root.
<component>
<api>
<prop name="title" type="string" default="Untitled"/>
<prop name="icon" type="image" default=""/>
<slot name="trailing"/>
</api>
<consts>
<int name="gap" value="8"/>
</consts>
<styles>
<style name="style_row" bg_opa="0" pad_all="#space_md"/>
</styles>
<view extends="lv_obj" flex_flow="row" width="100%">
<style name="style_row"/>
<style name="style_row_pressed" selector="pressed"/>
<lv_label text="$title"/>
<lv_label text="{title . ' with expression'}"/>
<lv_obj name="trailing"/>
</view>
</component>| Prefix | Means | Example |
|---|---|---|
$name |
An <api> property of this element |
<lv_label text="$title"/> |
#name |
A constant from <consts> or globals.xml |
pad="#space_md" |
{ ... } |
An expression, evaluated once at creation | hidden="{!icon}" |
Inside { } you write bare identifiers, no $ or #.
<view> is the root object of the element and the parent of everything inside it. extends picks what it is built on:
<view extends="lv_button" width="100%"> <!-- the view IS a button -->componentcan extend a widget or another componentwidgetcan extend a widget onlyscreencannot extend anything
Attributes are lower_snake_case. Compound names use -: lv_chart-series, style_bg_color-knob-pressed. Colors accept 0xff0000, or the 3-digit short forms, like 0xf00.
XML reserved characters must be escaped in values. value="I'm here" is invalid, write I'm here.
bool, int, px, %, content, string, color, opa, plus the name-based types image, font, subject, style that resolve against globals.xml. Combine with |: type="px|%|content".
Arrays come in four forms. Items are separated by spaces, and string items are wrapped in '.
| Form | Meaning | Real example |
|---|---|---|
int[3] |
Fixed number of elements | |
string[NULL] |
Terminated by an element. The terminator can be any token, e.g. grid_dsc[LV_GRID_TEMPLATE_LAST] |
lv_buttonmatrix map |
int[count] |
Length is passed as a separate parameter in C | lv_chart values, lv_line points |
string[] |
No terminator and no count |
Three ways, in order of preference:
<!-- 1. Named style, defined once, reused -->
<styles>
<style name="style_card" bg_color="#color_panel" radius="#radius_default"/>
</styles>
<view>
<style name="style_card"/>
<style name="style_card" selector="pressed"/>
<style name="style_card" selector="knob|focused"/>
</view>
<!-- 2. Local style property, for one-off values -->
<lv_slider style_bg_opa-indicator-pressed="200"/>
<!-- 3. Bound style, applied when a subject matches -->
<bind_style name="style_dark" subject="subject_dark_theme_on" ref_value="1"/>Prefix style names with style_. Selectors combine parts and states with |.
Styles are initialized once, so $api_props cannot go into a <style>. This fails:
<style name="style_main" border_width="$thickness"/> <!-- invalid -->But constants can be used:
<style name="style_main" border_width="#thickness"/> <!-- valid -->Pass the property to a local style property instead: <lv_slider style_border_width-knob="$thickness"/>.
Subjects are the interface between the UI and the application. Define them in globals.xml:
<subjects>
<int name="subject_brightness" value="50"/>
<string name="subject_user" value="John"/>
</subjects>Only int, string and float are supported.
<!-- Simple: attribute binding -->
<lv_slider bind_value="subject_brightness"/>
<lv_label bind_text="subject_brightness" bind_text-fmt="%d %%"/>
<!-- Conditional: child element binding -->
<bind_flag_if_eq subject="subject_mode" flag="hidden" ref_value="0"/>
<bind_state_if_gt subject="subject_temp" state="checked" ref_value="30"/>bind_flag_* takes a flag, bind_state_* takes a state. Both come in _eq, _not_eq, _gt, _ge, _lt, _le. The lv_obj- prefix is optional.
States: default, checked, focused, focus_key, edited, hovered, pressed, scrolled, disabled.
Common flags: hidden, clickable, checkable, scrollable, floating, ignore_layout.
Binding beats callbacks. A radio group, a theme switch, or a value readout needs no C at all: write the subject with subject_set_int_event, read it with bind_state_if_eq.
All are children of a widget, all take trigger (clicked, long_pressed, value_changed, ...):
<event_cb callback="my_handler" trigger="clicked" user_data="ctx"/>
<screen_load_event screen="settings" trigger="clicked" anim_type="fade_in" duration="300"/>
<screen_create_event screen="about" trigger="long_pressed"/>
<subject_set_int_event subject="subject_lamp" value="2" trigger="clicked"/>
<subject_increment_event subject="subject_vol" step="-5" min_value="0" max_value="100"/>
<play_timeline_event timeline="timeline_load" target="self" trigger="clicked"/>screen_load_event needs <screen permanent="true"> on the target; screen_create_event needs permanent="false" (the default). event_cb assumes you implement void my_handler(lv_event_t * e) in C.
Evaluated once at creation, not reactive. For anything that changes at runtime, use data binding.
<lv_obj width="{columns * 100 + (columns - 1) * gap}"/>
<lv_label text="{'Room ' . room_id . ': ' . temp . ' °C'}"/>
<lv_obj hidden="{count == 0}"/>
<lv_obj style_bg_color="{is_on ? 0x00ff00 : 0x333333}"/>. concatenates. Strings use single quotes. There is no && or ||, comparisons cannot be chained, and ternaries cannot be nested.
<animations>
<timeline name="timeline_load">
<animation prop="translate_x" target="self" start="-30" end="0" duration="500"/>
<animation prop="opa" target="label" start="0" end="255" duration="500" delay="200"/>
<include_timeline target="icon" timeline="show_up" delay="300"/>
</timeline>
</animations>target="self" is the view; anything else is matched against a child's name. Play with <play_timeline_event>.
Expose an internal object as a place where the caller can add children:
<!-- card.xml -->
<api><slot name="body"/></api>
<view>
<lv_obj name="body" flex_flow="column"/>
</view>
<!-- caller -->
<card>
<card-body>
<lv_label text="Anything"/>
</card-body>
</card>The slot target is <component_name-slot_name>, and you can set normal object properties on it.
- Inventing an attribute instead of reading
lvgl_widgets_xml/. - Putting
$propinto a<style>. Use a local style property. - Expecting
{ }to update at runtime. It does not, that's data binding. - Using
bind_state_*with aflag=attribute, orbind_flag_*withstate=. screen_load_eventon a screen that isn'tpermanent="true".- Hard-coding
pad="8"andbg_color="0x1E232E"when#space_mdand#color_dark_panelalready exist inglobals.xml. - Building a component whose only job is one styled widget. Extend it instead:
<view extends="lv_label" style_text_font="font_h3"/>. - Reaching for
<widget>and C when composition plus binding would do. - Centering with flex and forgetting
style_flex_track_place="center", which centers the tracks themselves.
Install it once, globally, with npm:
npm install --global @lvgl/lvglproThis puts lvglpro on your PATH. Node 18 or newer is required (CI uses 22).
Set LVGLPRO_CLI_TOKEN to a Product or Platform license token, or pass --token. Prefer the environment variable so the token stays out of shell history and logs, and never commit it.
export LVGLPRO_CLI_TOKEN="..."
lvglpro --helplvglpro validate <project> --errorlimit 25
lvglpro generate <project>
lvglpro screenshot <project> screens/home.xml --out /tmp/home.png --delay 200
lvglpro run-all-tests <project>Every command needs the token. If it isn't set, say the XML is unverified rather than implying it was checked. lvglpro <command> --help lists the current options for any command.
Tests are XML too, a <test> root with a <view> and a <steps> block of click_at, wait, subject_set, subject_compare, and screenshot_compare.
| Question | Answer |
|---|---|
| What can this tag accept? | lvgl_widgets_xml/<version>/lv_*.xml |
| What style properties and enums exist? | lvgl_widgets_xml/<version>/globals.xml |
| How does feature X work? | docs/syntax/*.mdx |
| What does real, good XML look like? | templates/basic/, examples/lvgl_open/, tutorials/ |
| Anything about LVGL itself | The LVGL MCP server at https://lvgl.mcp.kapa.ai/, preconfigured in each project's .mcp.json. Prefer it over recalling LVGL APIs from memory. |