Files
figcraft/docs/auto-layout-parity.md
2026-11-07 11:09:30 +07:00

26 KiB
Raw Permalink Blame History

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.removeAll takes 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.set applies 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