Position and layout
Anchors and offsets rather than coordinates - and what that buys you when the screen changes.
LVGL does not position objects by absolute coordinate, and neither does the designer. A widget has an alignment - one of seventeen anchors on its parent - and an X and Y offset from that anchor. Dragging on the canvas keeps the alignment and rewrites the offsets, so a widget you put bottom-right stays bottom-right when the screen size changes.
Alignment anchors
Nine are inside the parent and eight are outside it, mirroring LV_ALIGN_*:
| Inside | Outside |
|---|---|
| Top left · Top mid · Top right | Out top left · Out top mid · Out top right |
| Left mid · Center · Right mid | Out left mid · Out right mid |
| Bottom left · Bottom mid · Bottom right | Out bottom left · Out bottom mid · Out bottom right |
The Out anchors place a widget relative to its parent's outside edge, which is how LVGL positions a label next to a slider it does not contain. On the canvas an out-aligned child can sit beyond its parent's box; the parent's Overflow visible flag decides whether the device draws it.
The offset is signed. Center with X=-40 is forty pixels left of centre; Bottom right with Y=-10 is ten pixels up from the bottom.
Size
Width and height are in device pixels. New widgets get a sensible default per type (a slider is 180 × 12, a switch 50 × 25); the resize handles change both, and Shift keeps the aspect. The validator warns when a widget extends beyond the screen, since anything past the edge is cut off on the device.
The alignment row
Under Geometry in the Inspector is a row of six buttons - align left, centre horizontally, align right, align top, centre vertically, align bottom. One selected widget is aligned within the room its parent gives its children - align left puts it against the left edge of that room, centre horizontally centres it in it. That room is not the parent's outline: it is inside the parent's border and padding, in a Tab view it is the tab's page beside the tab bar, and in a Window it is the body under the header. It is the same room a child's Align anchors are measured in, so Align Top left with X and Y at 0 and the align-left and align-top buttons put a widget in the same place. Several selected widgets are aligned to each other, against the bounding box of the selection, so align left lines them all up with the leftmost. With three or more selected, the distribute controls space them evenly between the first and the last, horizontally or vertically.
Z-order
Later children draw on top of earlier ones. Bring to front, Bring forward, Send backward and Send to back re-order the selection among its siblings, and the Hierarchy panel shows the result. The generator creates objects in this order, which is what sets the z-order on the device.
Nesting
Panels, containers, tab pages and - in fact - any widget can hold children. Drop a widget onto a panel to make it a child, or drag it in the Hierarchy. A child's alignment and offsets are relative to its parent - to the room inside the parent's border and padding, which is where LVGL measures them from - so a panel can be moved as one thing. A border on one side only is counted differently by the two LVGL versions: LVGL 9 moves the contents in on that side alone, LVGL 8 on all four sides, and the canvas follows the version the project is for. Deleting a parent deletes its children.
Flags
The Flags section is LVGL's lv_obj_add_flag() switches. The ones people actually
reach for:
| Flag | Effect |
|---|---|
| Hidden | Not drawn and not touchable. The usual thing to toggle from an event or a binding to show and hide a panel. |
| Clickable | Receives presses. On by default for controls, off for labels and images. |
| Checkable | A press toggles the Checked state - how a button becomes a toggle button. |
| Scrollable | Children beyond the box can be scrolled to with a finger. Off makes a panel clip instead. A Panel, a Container and a screen start with it off; a List and a Tileview start with it on, because scrolling is what they are for. |
| Event bubble | Events are passed to the parent as well, so a panel can react to a tap on any child. |
| Gesture bubble | The same for swipes - needed for a swipe on a widget to reach the screen's swipe handlers. |
| Ignore layout | Excludes a child from its parent's flex layout. |
| Overflow visible | Draws children that extend outside the parent. |
The full list also includes Click focusable, Scroll elastic, Scroll momentum, Scroll one,
Scroll chain (horizontal and vertical), Scroll on focus, Scroll with arrow, Snappable, Press
lock, Advanced hit-test and Floating. A flag you leave alone is not written; one you change is,
as lv_obj_add_flag or lv_obj_remove_flag (lv_obj_clear_flag
on LVGL 8). Scrollable is the one exception: LVGL makes every plain object scrollable, which is
rarely what a layout wants, so the Arduino and MicroPython exports take it away from a screen and
from every container you have not switched it on for. ESPHome keeps its own default, which is
scrollable.
Flex layout
Set a container's Layout to Flex and LVGL arranges its children: in a row or a column, in the order they have in the tree. A child in a flex container has no position of its own. Its Align, X and Y are not read at all - on the panel or on the canvas, which lays the children out with the same arithmetic LVGL uses - and it cannot be dragged. Left, right and centre are settings of the container:
| Setting | In a row | In a column |
|---|---|---|
| Flex flow | Row, Column, Row wrap, Column wrap, Row reverse, Column reverse. Reverse turns the order round, not the side they start from. | |
| Main place | Left, centre or right along the row | Top, middle or bottom down the column |
| Cross place | How children of different heights line up with each other: top, middle or bottom of the row | How children of different widths line up with each other: left, centre or right of the column |
| Track place | Where the row itself sits in the container: top, middle or bottom | Where the column itself sits in the container: left, centre or right |
Cross and Track are the two people mix up. A row is only as tall as its tallest child, and a column only as wide as its widest; Cross place lines the children up inside that strip, and Track place moves the strip. So a column of buttons is centred in its container with Track place: Center - Cross place on its own only centres the narrower buttons against the widest one. A row is set in the middle of its container's height the same way. Main and Track also take Space evenly, Space around and Space between, which share out whatever room is left over.
What else has a say:
- Order - a child's place in the tree is its place in the flow. Reorder it in the hierarchy, or with the Front and Back buttons under Geometry.
- Gaps - the container's Column padding is the gap between children along a row, and its Row padding the gap between rows; in a column the two swap. Its ordinary padding is the margin round the whole arrangement.
- Size - a child keeps its own width and height. A flow that wraps starts a new row or column when the next child would not fit.
- Opting out - the Ignore layout or Floating flag takes one child out of the flow, and its own Align, X and Y apply again. A hidden child takes no room, so the others close up round it.
LVGL has no setting that aligns one child differently from its siblings, and two things it does have are not in the designer yet: a child that grows to take the room left over, and a child that starts a new row.
Grid layout
Set a container's Layout to Grid and it becomes columns and rows, with each child in a cell. Where a flex layout is one line of things that may wrap, a grid is a table: a column lines up all the way down, a row all the way across, and a child can cover more than one of either. As in a flex container, a child of a grid has no position of its own - its Align, X and Y are not read and it cannot be dragged - and the canvas, Preview and every export place it with the same arithmetic LVGL uses.
Switched on, a grid starts as two equal columns with a row for each pair of children, so there is something tidy to look at straight away. Select the grid, or any child of it, and its cells are outlined on the canvas with each column and row numbered from 0.
Columns and rows
Under the container's Layout the columns and the rows are two lists. Each has a kind of size:
| Kind | A column is | A row is |
|---|---|---|
| Pixels | That many pixels, whatever is in it. | |
| As big as what is in it | As wide as the widest child that is in that column alone | As tall as the tallest child that is in that row alone |
| A share of what is left | Once the pixel and content tracks and the gaps are taken, what is left is divided among the shares: a track of 2 shares is twice one of 1. Three columns of 1 share each are three equal columns. | |
A child that covers two columns is not counted when a content column is sized, and nor is one that is hidden. Tracks that add up to more than the container run out of it; the container does not grow.
Columns sit and Rows sit say where the tracks go when they do not fill the container: at the start, the centre or the end, or spaced - Space evenly, Space around and Space between, which share the room left over between the tracks in place of the gap. While a column is a share of what is left there is no room left over across, so start, centre and end all look the same.
A child's cell
Under a child's Geometry:
- Cell - The next free cell puts it in the first cell that is free, reading across each row and then down, in the order of the tree. Rows are added, each like the last one in the list, when the children need more than the grid has. A cell of its own gives it a column and a row, counted from 0, and it stays there whatever is added round it.
- Columns wide and Rows tall - how many tracks it covers.
- Across its cell and Down its cell - left, centre or right; top, middle or bottom; or Stretch to fill, which makes it the cell's width or height in place of its own.
What else has a say:
- Gaps - the container's Column padding is the gap between columns and its Row padding the gap between rows. Its ordinary padding is the margin round the whole grid.
- Opting out - the Ignore layout or Floating flag takes one child out of the grid, and its own Align, X and Y apply again. A hidden child keeps its cell and takes no room in it.
- Two in one cell - allowed, and drawn one over the other in tree order: it is how a caption is put on a picture. The Problems tab mentions it, since it is as often a slip.
- A cell the grid does not have - column 5 of a grid with three - is held to the last column or row, and the Problems tab says which child and where it was asked for.
In each export
| Export | What is written |
|---|---|
| Arduino (LVGL 9 and 8) | Two static arrays of track sizes ending in LV_GRID_TEMPLATE_LAST, lv_obj_set_grid_dsc_array(), lv_obj_set_grid_align() where the tracks do not sit at the start, and lv_obj_set_grid_cell() for every child. |
| MicroPython | The same calls on the object: set_grid_dsc_array() with two lists, set_grid_align() and set_grid_cell(). |
| ESPHome | layout: with type: grid, grid_columns, grid_rows and the two aligns, and on each widget grid_cell_row_pos, grid_cell_column_pos, the spans and grid_cell_x_align / grid_cell_y_align. |
LVGL has no automatic placement of its own - a child with no cell is in column 0, row 0, on top of every other such child - so every export writes each child's cell out in full, the ones the designer chose for it included.
Three things are different on ESPHome, and the export follows ESPHome in each:
- ESPHome would place a widget with no cell itself, in the first free cell. It is never asked to: the designer has already said where each goes, and both a row and a column are written for every widget, since ESPHome refuses one without the other.
- ESPHome refuses two widgets in one cell unless the layout says
multiple_widgets_per_cell: true. The export writes that line for a grid that has two in a cell - and for one with a child the grid leaves alone, which ESPHome still wants a cell for and which is written into the first. - The gaps are options of the layout there (
pad_row,pad_column), not styles of the widget, and a share is spelledFR(2).
Two things differ between LVGL versions, and the canvas draws whichever the project is for. Shares that do not divide evenly are rounded differently - 70 pixels among three equal columns is 23, 24, 23 on LVGL 9 and 23, 23, 24 on LVGL 8. And LVGL 8 has no margins, so a chart with axes in a cell is placed by its plot, with its axes outside the cell; on LVGL 9 the axes are inside it.
Not in the designer: a grid that takes its tracks from its parent's ("subgrid"), right-to-left grids, and a button's own caption, which on the board is one more child of a button that is a grid - put a Label in the cell it belongs in.