26 KiB
Auto layout and constraints parity
Last reviewed: 2026-10-11 · Last updated: 2026-11-03 · Change: patch (a Cmd/Ctrl-click on a padding field sets all four sides with the value typed, as the Guide to auto layout's shortcut guide says; Suggest auto layout is its own command, as the help center says: it adds as many auto layout frames as needed, splitting a frame's layers into rows or columns by the gaps between them, and keeps the placement of the design where the gaps are even (#1823); baseline alignment puts a layer without text with its bottom on the line, as the help center's figure shows an icon next to text (#1825); fill children share the free space by their content area, border-box fashion, as the Flexbox article says (#1824); double-click an edge of an auto layout frame's box for Hug contents, Alt+double-click an edge of a child's box for Fill container, as the Guide to auto layout says (#1838); a child resized to the full available space of its auto layout frame becomes Fill container on that axis, as the Guide to auto layout says; a text keeps a max height or a number of max lines, not both, as the Guide to auto layout says: a max height sets max lines to Auto and max lines remove the max height; Remove all auto layout, as the help center's More layout options has it:
layout.removeAlltakes the auto layout off a frame and every frame nested in it in one undo step; grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); Layout sidebar: Add auto layout (+) is available for groups and converts a single group to a frame in place, with mask order, flow-only inference and Fill sizing preserved; adding auto layout to an empty frame keeps its size (#369);node.setapplies auto layout keys only to frames, with warnings (#603); core editor leftovers: auto gap handles, wrapped-line alignment; behaviour suite written from the public documentation, every rule's status; fixes and the missing settings it found) · Target: Figma Design (desktop app 126.10.7)
Auto layout is the most used layout feature in Figma and the one where small differences (a
hugging frame one pixel off, text wrapping at another width) break real files. Breadth ~98%,
ready ~74% (estimated): every rule of the public documentation is a test case with the sizes and
positions it implies (crates/layout/src/behaviour_tests.rs, 64 cases, and
crates/engine/src/al_behaviour_tests.rs through commands), and all pass; nothing yet against real
Figma files. Remaining: 3–6 h.
Adding auto layout to a group
With one group selected, Add auto layout converts that group into a frame in place, retaining its
id, name, parent and direct children. The Layout sidebar’s +, context menu, Shift+A and API calls
all dispatch layout.add, so they share the conversion and a single undo step restores the group.
The layout settings are inferred from visible, non-absolute children, matching the layout engine’s
flow. Hidden and absolute children therefore do not distort direction, gap or padding. A converted
group keeps Fill sizing on either axis in its auto layout parent. Sorting by position preserves
mask boundaries and the paint order within masked runs. The sidebar offers Add only when every
selected container has no auto layout, including mixed selections.
This follows the Figma behavior reported by the contributor and the sidebar workflow users know from similar design tools: start with a group and make its children participate in auto layout. Wrapping the group would instead make the entire group one layout item, leaving its children outside the new frame’s layout flow and adding an unnecessary level to the layer tree. Converting the selected container makes later changes to a child’s size reflow its siblings directly. Selecting several groups still wraps that selection in a shared auto layout frame.
Rules from the documentation
Status: pass (a case in the suite holds), fixed (the suite found a divergence, now fixed),
added (the setting was missing and now exists as a node.set prop and a Design panel control),
silent (the documentation doesn't say; the case pins the current behaviour).
| Rule | Status | Evidence / notes |
|---|---|---|
| Hug: the frame sizes to its children plus padding (the 40 px text + 10 px padding = 60 px example) | pass | sizing_rules |
| Fixed: the frame keeps its size whatever its children need | pass | sizing_rules |
| Fill: children share the space left after padding, gaps and fixed siblings; across the flow they stretch | pass | sizing_rules |
| Fill children share by their content area (border-box): a child with more padding or a thicker included inside stroke takes a little more, so the content areas come out the same | fixed | "Figma distributes space amongst fill container children by the children's content area instead of by their size, matching the CSS border-box model. A layer with a thicker stroke will take up slightly more of the available width or height, so that its inner content area matches its sibling's" (Use auto layout with CSS Flexbox in mind); the split went by outer size before. Min and max still clamp the outer size; a child's own insets are its padding and its included inside stroke (nested_tests::fill_children_share_by_content_area; #1824) |
| Fill on a child turns its hugging parent Fixed on that axis | pass | al_behaviour_tests::fill_on_a_child_turns_the_hugging_parent_fixed |
| A child resized to the full available space becomes Fill container on that axis | fixed | "Child objects of an auto layout frame will also be set to Fill container if they are manually resized to the full available space of the parent frame": a width or height given by a resize, the W/H fields or a canvas handle that equals the frame's size less its padding sets Fill there: across the flow for any child, along the flow for a lone child (beside siblings what is left depends on the gaps, the alignment and overflow, so it is left alone); not in a hugging or wrapping frame or a grid. It stayed Fixed before (al_behaviour_tests::a_child_resized_to_the_full_available_space_becomes_fill) |
| Hug only on auto layout frames (and text); Fill only in an auto layout flow, never on a top-level frame | pass | al_behaviour_tests::hug_and_fill_only_where_they_apply |
| Double-click an edge of an auto layout frame's box: Hug contents on that axis; Alt+double-click an edge of a child's box: Fill container | fixed | "Tip: Double-click the vertical or horizontal edge of an auto layout object's bounding box to set it to Hug contents."; "Hold ⌥ Option or Alt and double-click the vertical or horizontal edge of an auto layout object's bounding box to set it to Fill container." A vertical edge sizes the width, a horizontal one the height; any other layer keeps the double-click's old meaning (#1838). double_click_tests::a_double_click_on_an_auto_layout_edge_hugs_and_with_alt_fills |
| Min/max width and height bound Hug, Fill and Fixed sizes, on frames and children | pass | sizing_rules; a fill child at its max leaves the rest to the other fill children (silent on redistribution: CSS flexbox) |
| A hugging text wider than its max width wraps at the max | pass | sizing_rules |
| A text has a max height or a number of max lines, not both: a max height sets max lines to Auto, a number of max lines removes the max height | fixed | both coexisted; node.set {maxHeight} and {maxLines} and text.maxLines keep one of the two (a frame's max height is untouched); text_behaviour_tests::a_text_has_a_max_height_or_max_lines_not_both |
| Remove all auto layout: a frame and all of its nested objects lose their auto layout at once (More layout options) | added | layout.removeAll {ids?}, one undo step, in the canvas context menu beside Add auto layout; instances and the layers inside them keep theirs; al_nesting_tests::remove_all_auto_layout_strips_the_frame_and_every_frame_nested_in_it |
| Text filling a width wraps and its hugging frame grows with it | pass | sizing_rules |
| A hugging frame follows a child that grows, through nested levels, in one edit and one undo step | pass | al_behaviour_tests::a_hugging_frame_follows_its_children_through_nested_levels |
| Padding per side (uniform, vertical/horizontal, individual) | pass | padding_and_stroke_rules |
| A frame is never smaller than its padding (plus an included inside stroke), even when Fixed, hugging or given a smaller max | fixed | was free to shrink below its padding |
| Strokes included: only the frame's own inside stroke takes up space; center and outside strokes never do | fixed | every stroke alignment counted before; per-side inside weights count per side (silent on individual weights) |
| New auto layout includes inside strokes by default | fixed | the default was excluded |
| Strokes included / excluded setting in the Design panel | added | strokesIncluded existed as a prop only |
| Gap: fixed distance; negative gaps overlap | pass | gap_rules |
| Auto gap between: no space at the edges; a single child at the start | pass | gap_rules |
| Auto gap evenly (1:1 with the edges) and around (gaps twice the edges) | added | mainAlign: spaceEvenly / spaceAround, Design panel gap menu |
| An auto gap stops at 0 when children don't fit (no overlap) | fixed | went negative before |
| Alignment 3×3; with an auto gap only the cross alignment; a hugging frame shows no difference | pass | alignment_rules |
| Text baseline alignment, texts of different sizes | pass | alignment_rules |
| Baseline alignment of a layer without text (an icon next to text) | fixed | its bottom sits on the baseline, as the figure's caption says ("the bottoms of the icon and the word home are aligned on the red line"); it kept to the top of the line (#1825) |
| Horizontal wrap: a new row when the width runs out; gap between rows | pass | wrap_rules |
| Vertical wrap: a new column when the height runs out; not masonry; gap between columns | added | layout, Design panel wrap toggle, column gap field, on-canvas gap handles, drag reorder and arrow keys by column |
| Wrapped rows/columns as a block: packed at the start, centre or end the cross alignment names; stretched when every child fills across the flow; or the free space between them (one line or no free space: like packed) | added | content_align_tests (13 cases), al_content_align_tests; contentAlign, the Row/Column gap mode in the Design panel, REST and .fig both ways, align-content in CSS |
| Ignore auto layout: out of the flow, kept in the frame; constraints apply only then | pass | absolute_position_rules |
| Canvas stacking: last on top by default, first on top on request; the layer order stays | fixed + added | reverseZ was stored but never drawn: now the canvas, PNG, SVG and PDF exports and hit testing paint in that order; Design panel control |
Grid: fr tracks share by fraction; fixed tracks keep their size; hug tracks fit their content |
pass | grid_rules |
Grid fr tracks behave like the CSS fr unit: a track never gets less than its content |
fixed | was minmax(0, 1fr): a fixed-size child taller than its share overflowed into the next row |
| Grid placement left to right, top to bottom; rows grow; anchors; spans; gaps; padding | pass | grid_rules |
| Grid children align independently in their cells | added | gridAlignH / gridAlignV (Auto follows the frame), Design panel cell alignment |
A hugging grid with fr tracks |
silent | the file format calls it invalid; fr tracks hug their content |
| A hugging frame with an auto gap | silent | it hugs with the gap value, then shares the (no) extra space |
| One layout pass settles every case | pass | every case is laid out twice and must not move |
Features
| Feature | Status | Evidence / notes |
|---|---|---|
| Horizontal, vertical, wrap (both directions) | done | crates/layout |
Grid auto layout: fixed / hug / fr tracks, cells, spans, gaps, cell alignment |
done | fr like CSS fr |
| Gap, auto gap (between / evenly / around), padding (per side), alignment 3×3 | done | a Cmd/Ctrl-click on a padding field (the pair or a side) makes the value typed go to all four sides in one undo step (Guide to auto layout, Keyboard shortcut guide; padding_individual_tests) |
| Text baseline alignment | done | |
| Hug / Fill / Fixed sizing with coherent rules (Fill turns a hugging parent Fixed) | done | al_nesting_tests.rs, al_behaviour_tests.rs |
| Min/max width and height, redistribution | done | hugging text wraps at its max |
| Nested hug/fill settling in one pass (three levels, wrapping fill text, grid rows) | done | |
| Absolute position (ignore auto layout) with constraints | done | |
| Strokes included in layout (inside strokes), canvas stacking | done | |
| On-canvas padding and gap handles, incl. wrap row/column gap and grid gaps | done | an automatic gap's handle shows the distance it has; dragging it fixes the gap from there (silent in the documentation) |
| Drag to reorder (wrap-aware, by row or column), into grid cells, across nested frames | done | al_drag_tests.rs |
| Arrow keys move children one place (flows, rows, columns, grid cells, into neighbours) | done | al_keys_tests.rs |
| Constraints: left, right, left and right, centre, scale; top/bottom likewise | done | |
| Layout grids: columns, rows, grid; stretch/left/centre/right; variables bound | done | |
| Suggest auto layout | done | The target works out which layers belong together and adds as many auto layout frames as the design needs, in one step, keeping the placement. layout.suggest used to run layout.add and made one row of everything (#1823). Now it splits a frame's layers, by their boxes in the frame, along the vertical axis first: sorted by top edge, a layer joins the current row while it starts above the row's bottom (or together with it), and a gap, even a zero one, between the row and the next layer starts a new row. With two or more rows the frame becomes a Vertical auto layout whose items are the rows; otherwise the same split by columns gives a Horizontal one; if the layers overlap along both axes the frame gets no auto layout and the result carries a warning. A row or column of several layers goes into a new frame without fill or clipping, sized to it and inserted in its place (it hugs its content once the split gives it auto layout; a band whose own layers overlap along both axes gets none, with a warning, and stays a Fixed frame that cannot reflow those layers), and the split repeats inside it, to a depth of 16 and for up to 1,000 layers per frame (past that, or when a mask is among the layers, the layers are laid out in one list as Add auto layout does, with a warning). The frame keeps its size, Fixed: the gap is the distance between the bands, the padding along the flow is the first band's offset and the room left after the last, across the flow the smallest offset (alignment start), so with equal gaps and bands that start at the same offset every layer keeps its box exactly. When the gaps differ by more than 0.5 px the smallest is used and warnings says the later bands move; when the bands start at different offsets across the flow (a centred title above a card) they line up at the smallest, which moves the others, and warnings says so. Instances and layers inside them are refused with the error Convert to group gives, before anything changes; a main component works. direction sets the top level only. A single group becomes a frame in place and several layers are wrapped in a frame (both hug), as Add auto layout does; a frame with fewer than two layers is laid out as Add auto layout does. One undo step. Tests in crates/engine/src/layout_suggest_tests.rs: two_rows_of_two_squares_become_a_vertical_frame_of_two_horizontal_rows, suggesting_auto_layout_is_one_undo_step, three_squares_in_a_row_get_one_horizontal_layout_without_nesting, three_squares_in_a_column_get_one_vertical_layout_without_nesting, a_header_above_two_columns_of_squares_gives_a_row_per_line, a_header_above_a_tall_layer_beside_a_column_nests_three_levels, unequal_gaps_use_the_smallest_and_say_so, overlapping_layers_leave_the_frame_without_auto_layout_and_say_so, a_group_becomes_a_frame_in_place_and_follows_the_rules, several_layers_are_wrapped_in_a_frame_that_is_then_split, a_direction_forces_the_top_level_and_nested_frames_follow_the_gaps, a_frame_with_one_layer_is_laid_out_like_add_auto_layout, add_auto_layout_still_makes_a_single_row, a_centred_title_above_a_card_is_lined_up_at_the_card_edge_with_a_warning, instances_and_layers_inside_them_are_refused_and_a_main_component_works. Not attempted: overlapping layers, keeping uneven offsets across the flow (they line up at the smallest, with a warning), grids |
| "Add auto layout" on a selection | done | the Layout sidebar exposes Add auto layout (+) for groups and frames using the same layout.add command as the context menu; a single group becomes a frame in place, keeping its id, name and direct children; adding auto layout to an empty frame keeps its size, Fixed (it collapsed to 20×20); a frame with content still hugs it, as does a new frame wrapped around a selection (#369). node.set with auto layout keys on a mix of layers applies them to the frames and warns about the rest instead of failing the whole call (#603) |
| Real Figma files laid out identically | not checked | needs .fig or REST import of real files (gaps #1, #3) |
| Performance: re-flow only where sizes or places changed | done | measured in cargo xtask bench (components at scale) |
Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-11-05 | patch | M2, #1823: Suggest auto layout is no longer Add auto layout under another name: layout.suggest splits a frame's layers into rows (Vertical) or else columns (Horizontal) by the gaps between their boxes, wraps each band of several layers in a new frame without fill or clipping that hugs it, and repeats inside it (16 levels, 1,000 layers per frame at most). The frame keeps its size; gap and padding come from the bands, so layers keep their boxes where the gaps are even and the bands start at the same offset; instances and layers inside them are refused; uneven gaps (smallest used), bands that start at different offsets across the flow (lined up at the smallest) and layers that overlap along both axes come back as warnings. A band frame hugs only when its own split succeeds: a band whose layers overlap along both axes gets no auto layout and stays Fixed, so it cannot reflow those children. direction sets the top level only; one undo step; layout.add is unchanged. Tests in layout_suggest_tests.rs |
| 2026-11-02 | patch | M2: a Cmd/Ctrl-click on a padding field (the horizontal or vertical pair, or a side with Individual padding open) makes the value typed go to all four sides in one undo step (Guide to auto layout, Keyboard shortcut guide); a plain click still edits that axis or side alone. The modifier was ignored before. Test: padding_individual_tests |
| 2026-11-03 | patch | M2, #1825: baseline alignment of a layer without text (an icon next to a text) puts its bottom on the line's baseline, as the help center's figure shows ("the bottoms of the icon and the word home are aligned on the red line"); it kept to the top of the line. A layer reaching below the text's baseline moves the text down, and a line with no text lines the layers' bottoms up (the documentation is silent; CSS baseline alignment does the same). Tests: alignment_rules (three cases), engine baseline_alignment_puts_an_icons_bottom_on_the_text_baseline. Numbers unchanged. |
| 2026-11-02 | patch | M2, #1824: fill container children share the free space by their content area: each one's own insets (padding and an included inside stroke, from the same insets the frame uses) come off the free space before the equal split and go back on its share, so a child with more padding or a thicker inside stroke takes a little more and the content areas match ("Figma distributes space amongst fill container children by the children's content area instead of by their size, matching the CSS border-box model", Use auto layout with CSS Flexbox in mind). Min and max still clamp the outer sizes and the frozen-share loop is as it was; children without insets split as before. Test: nested_tests::fill_children_share_by_content_area |
| 2026-11-02 | patch | M2, #1838: a double-click on an edge of an auto layout frame's bounding box sets Hug contents on that axis, Alt+double-click on an edge of a child's Fill container ("Double-click the vertical or horizontal edge of an auto layout object's bounding box to set it to Hug contents", "Hold ⌥ Option or Alt and double-click ... to set it to Fill container", Guide to auto layout); a vertical edge sizes the width, a horizontal one the height; the Keyboard shortcuts panel lists both gestures. Test: double_click_tests::a_double_click_on_an_auto_layout_edge_hugs_and_with_alt_fills. Numbers unchanged. |
| 2026-11-02 | patch | M2: a child of an auto layout frame resized to the full space available to it on an axis (the frame's size less its padding: across the flow for any child, along the flow for a lone child) becomes Fill container on that axis, whether by transform.resize, the W/H fields or a canvas handle ("Child objects of an auto layout frame will also be set to Fill container if they are manually resized to the full available space of the parent frame", Guide to auto layout). Any other size keeps the axis Fixed as before; beside siblings along the flow, in a hugging or wrapping frame, in a grid and for a layer that ignores the layout nothing changes. Test: al_behaviour_tests::a_child_resized_to_the_full_available_space_becomes_fill |
| 2026-11-01 | patch | M2: a text layer keeps a max height or a number of max lines, not both ("Text layers cannot have both a max height and a set number of max lines", Guide to auto layout): node.set {maxHeight} on a text sets its max lines to Auto, and node.set {maxLines} or text.maxLines with a number removes its max height. Found by a documentation audit of this page. Test: text_behaviour_tests::a_text_has_a_max_height_or_max_lines_not_both. Numbers unchanged |
| 2026-11-01 | minor | M2: Remove all auto layout ("To remove auto layout from a frame and all of its nested objects at once, right-click on the frame and go to More layout options > Remove all auto layout", Toggle on auto layout in designs): layout.removeAll {ids?} strips the frames and every frame nested in them in one undo step, leaving instances and their layers alone; in the canvas context menu beside Add auto layout and in the Actions palette. Found by a documentation audit of this page. Test: al_nesting_tests::remove_all_auto_layout_strips_the_frame_and_every_frame_nested_in_it. Numbers unchanged |
| 2026-10-30 | minor | M8, #1083: grid placement finds free cells on a bitmap, a word at a time, with the same placements; crafted grids of wide children no longer stall a layout pass. Numbers unchanged. |
| 2026-10-29 | minor | M2 review fixes: conversion preserves mask runs and Fill sizing per axis; layout inference uses visible, non-absolute flow children; the sidebar hides Add for mixed selections containing existing auto layout. Regression coverage includes rendered mask pixels, visible-child positions, parent layout stability and the sidebar accessibility tree. Numbers unchanged |
| 2026-10-29 | minor | M2: the Layout sidebar exposes Add auto layout (+) for groups. layout.add converts one selected group into a frame in place rather than wrapping it: same id, name and direct children; the layout arranges those children, and one undo restores the group. Sidebar, menu, shortcut and API calls all use the corrected command. This follows the contributor’s observed Figma behavior and the workflow familiar to users of similar design tools. Tests cover direct-child reflow, stable hierarchy, undo/redo and wrapping a multi-group selection. Numbers unchanged |
| 2026-10-17 | minor | Community batch #753: adding auto layout to an empty frame keeps its size, Fixed, while a frame with content hugs it (#369); node.set applies auto layout keys to the frames among its targets and warns about the others, and a font style the family lacks warns with the nearest face (#603). Checked over MCP. Numbers unchanged |
| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (gap_handle_tests). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. |
| 2026-10-13 | minor | Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (contentAlign: spaceBetween, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and .fig, written to .fig, align-content in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. |
| 2026-10-11 | minor | Behaviour suite from the public documentation (64 layout cases, 8 command tests). Fixed: padding floor, inside-only included strokes (and included by default), auto gaps stop at 0, grid fr like CSS fr, canvas stacking actually drawn and hit. Added: auto gaps evenly/around, vertical wrap, grid cell alignment, strokes and canvas stacking controls. Breadth 95% → 98%, ready 68% → 74%, remaining 6–12 h → 3–6 h |
| 2026-10-10 | major | Created from the 2026-10-10 re-measure |