Documentation / Designing

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_*:

InsideOutside
Top left · Top mid · Top rightOut top left · Out top mid · Out top right
Left mid · Center · Right midOut left mid · Out right mid
Bottom left · Bottom mid · Bottom rightOut 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:

FlagEffect
HiddenNot drawn and not touchable. The usual thing to toggle from an event or a binding to show and hide a panel.
ClickableReceives presses. On by default for controls, off for labels and images.
CheckableA press toggles the Checked state - how a button becomes a toggle button.
ScrollableChildren 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 bubbleEvents are passed to the parent as well, so a panel can react to a tap on any child.
Gesture bubbleThe same for swipes - needed for a swipe on a widget to reach the screen's swipe handlers.
Ignore layoutExcludes a child from its parent's flex layout.
Overflow visibleDraws 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:

SettingIn a rowIn a column
Flex flowRow, Column, Row wrap, Column wrap, Row reverse, Column reverse. Reverse turns the order round, not the side they start from.
Main placeLeft, centre or right along the rowTop, middle or bottom down the column
Cross placeHow children of different heights line up with each other: top, middle or bottom of the rowHow children of different widths line up with each other: left, centre or right of the column
Track placeWhere the row itself sits in the container: top, middle or bottomWhere 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:

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:

KindA column isA row is
PixelsThat many pixels, whatever is in it.
As big as what is in itAs wide as the widest child that is in that column aloneAs tall as the tallest child that is in that row alone
A share of what is leftOnce 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:

What else has a say:

In each export

ExportWhat 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.
MicroPythonThe same calls on the object: set_grid_dsc_array() with two lists, set_grid_align() and set_grid_cell().
ESPHomelayout: 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:

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.