// figma_workflows · auto_layout_spec
Auto Layout Is a Constraint System, Not a Convenience Toggle
Most people learn Auto Layout by nudging settings until a frame looks right, then get stuck the first time it stretches, clips, or refuses to shrink. This is a spec-level read of what it's actually solving underneath the panel — resolvable rules, not guesses — so you can predict the outcome before you touch anything.
§ 01
The Mental Model
A frame with Auto Layout applied does not have a size the way an ordinary rectangle does. Draw a plain rectangle and its width and height are data you typed in — fixed numbers, full stop. Apply Auto Layout to a frame and its size becomes the output of a small system of rules: how much space is available, what its children are asking for, and what its own sizing mode says to do with the difference. Nothing renders until that system is solved.
This is the same box model CSS Flexbox already implements, arriving at the same place from the same direction — Figma didn't invent a new spacing paradigm, it gave designers a panel over the one browsers already run. Every "why is my button suddenly full-width" moment is a resolution-order problem, not a bug: something upstream is asking for a size that has nowhere to come from, so Figma has to pick an answer for you.
Treat every Auto Layout frame as three declarations layered on top of each other, always in this order: how big is this frame allowed to be (its own sizing mode, relative to its parent), how is the space inside it divided (padding and gap), and how are its children positioned within what's left (alignment). Get the order right and the rest of this page is mostly vocabulary.
§ 02
Three Sizing Modes, One Rule Each
Every Auto Layout frame, and every direct child of one, picks a resizing mode independently for each axis — width and height are set separately, so a button can be Fill horizontally and Hug vertically at the same time.
Fixed — an explicit number. The frame ignores its children and its parent entirely on that axis. Use it for things that must never move: an icon slot, a fixed-width sidebar, a toggle switch.
Hug contents — the frame sizes itself to wrap its children plus its own padding, and nothing else. Its size on that axis is an output of what's inside it, not an input you set directly, which is why you resize a Hug frame by resizing its children or its padding — never the frame.
Fill container — the frame asks its parent for whatever space is left over after siblings and padding are accounted for. Its size on that axis is an output of its parent, not of its own content, so a Fill frame's content can overflow if the parent turns out smaller than expected — filling comes with no minimum guarantee unless you add one.
The one rule that explains almost every "unexpected" resize: Fill points up toward the parent, Hug points down toward the children. Set a frame to Hug on an axis, then give one of its direct children Fill on that same axis, and you've built two requests staring at each other with no third value to resolve against. Figma breaks the tie by quietly converting one side to Fixed — which looks like a bug the first time you see it, and is actually the only sane way to terminate an unsolvable loop. The fix isn't a setting, it's structural: Fill only means something once some ancestor on that axis has already landed on a real number, whether that's a Fixed frame, the artboard edge, or a browser viewport.
§ 03
Padding and Gap Are Different Properties
Two spacing values live on every Auto Layout frame, and it's worth being precise about which one you're reaching for, because they resize completely different things.
Padding is the space between the frame's own edge and its children — inside a Hug frame, padding is part of what the frame is hugging, so changing padding changes the frame's outer size. Padding can be set as one uniform number, a horizontal/vertical pair, or independently on all four sides.
Gap (sometimes labeled "item spacing") is the space between siblings, not between the frame and its edge. Gap can be a fixed number — every gap in the frame is exactly that many pixels — or set to distribute automatically, which pushes the first and last child flush to the padding edge and lets the gap absorb whatever room is left between them, growing as the frame grows.
The distinction matters most when a Hug frame "won't shrink": if you're dragging the gap value and the frame's outer bound isn't moving, that's correct behavior — gap only ever redistributes space between children, and padding is the only spacing value that changes a Hug frame's outer size.
§ 04
Alignment: Two Axes, Nine Positions
Every Auto Layout frame has a primary axis (the direction of flow — the axis Gap operates on) and a counter axis (perpendicular to it). Alignment is set independently for each, which is why the alignment control in the panel is a 3×3 grid rather than a single dropdown: three positions along the primary axis, times three along the counter axis, gives nine total combinations.
Primary-axis alignment controls how children pack along the direction of flow — start, center, end, or, via the Auto gap setting from the previous section, spread evenly with the leftover space pushed between items rather than gathered on one side.
Counter-axis alignment controls where children sit on the perpendicular axis — for a horizontal frame, that's vertical position (top, center, bottom); for a vertical frame, it's horizontal position (left, center, right).
This maps directly onto Flexbox's justify-content
(primary axis) and align-items (counter axis) —
see the handoff table in section eight for the full property
list.
§ 05
Nesting: How Constraints Cascade
The real power of Auto Layout only shows up once frames are nested, and the reason it holds together is that each frame solves its own local system independently, then hands its resolved size up to its parent as if it were a single, ordinary child — the parent doesn't know or care that its child is itself a small layout engine with three more children inside it.
Take the comment card above: an outer vertical frame set to Hug (the card itself), containing an inner horizontal frame for the byline (an avatar at Fixed size, a name set to Fill, and a timestamp at Fixed size), followed by a vertical frame for the body text, also set to Hug. Type a longer comment, and the body-text frame grows taller because its own Hug rule resolves against its now-taller text child. That new height rolls up to the outer card frame — also Hug — which grows to match. Nothing in this chain was told "make the card taller when the comment is long"; every frame just resolved its own three-declaration stack from section one, and the result composes correctly by construction.
The debugging consequence is the one worth remembering: a change three frames deep can only propagate upward through frames that are set to Hug on the relevant axis. The moment it hits a Fixed or Fill frame — a frame whose size on that axis doesn't come from its children — the change stops there and gets absorbed, either by clipping if there's no room, or by whitespace if there's too much. If resizing something isn't affecting a distant ancestor the way you'd expect, the frame sitting between them is the first place to check.
§ 06
Wrap and the Min/Max Escape Hatches
Horizontal and vertical flows (not the Grid flow) support a Wrap setting: once children run out of room on the primary axis, instead of overflowing or shrinking past their limits, the frame starts a new row — or column — and continues laying out there. Wrapped frames get a second, independent gap value for the space between rows, separate from the gap between items within a row.
Wrap on its own doesn't create a responsive layout — it just gives overflow somewhere to go. What decides when something wraps is the min-width (or min-height) constraint layered on top of a child's Fill or Hug sizing: a Fill child with a 200px minimum keeps growing to fill its row right up until the row can no longer offer it 200px, at which point it drops to the next line instead of shrinking further. Max-width and max-height work the same way in reverse, capping how far a Fill child is allowed to stretch.
Worth flagging plainly: min/max constraints across several Fill children inside a wrapped parent are one of the rougher edges of the engine as of this writing — resize order can produce values that look inconsistent across siblings that should be symmetric. If a layout depends on precise min/max behavior across many wrapped Fill items, test the actual file rather than assuming the numbers will resolve the way the panel implies.
§ 07
Absolute Position, On Purpose
Occasionally an element needs to sit on top of an Auto Layout
frame without taking part in its flow at all — a notification
badge overlapping the corner of an icon, a close button pinned
to a modal header regardless of how long the title text runs.
Toggling a layer to Absolute Position removes
it from the gap and alignment system entirely: it no longer
consumes a slot, doesn't push siblings, and is positioned
instead by direct offsets against its parent frame — the same
way position: absolute behaves against a
position: relative parent in CSS. Use it
deliberately and sparingly: it's an exception to the
constraint system, not a substitute for it, and a layout built
mostly out of absolutely positioned children has quietly
opted out of everything Auto Layout is for.
§ 08
Dev Handoff: Mapping to CSS Flexbox
None of the vocabulary above is Figma-specific — it's a panel over the same model Flexbox already implements, which is exactly why "translate this frame to code" is usually a mechanical exercise rather than a design decision, provided both sides are using the same terms.
| Figma property | CSS Flexbox equivalent |
|---|---|
| Direction: Horizontal / Vertical | flex-direction: row / column |
| Wrap | flex-wrap: wrap |
| Gap (fixed number) | gap |
| Gap: Auto / space between | justify-content: space-between |
| Padding | padding (shorthand or per side) |
| Resizing: Fixed | fixed width/height, flex-grow: 0, flex-shrink: 0 |
| Resizing: Hug contents | width: fit-content (no flex-grow) |
| Resizing: Fill container | flex: 1 1 0% |
| Primary-axis alignment | justify-content |
| Counter-axis alignment | align-items (or align-self per child) |
| Min / Max width or height | min-width / max-width, min-height / max-height |
| Absolute Position | position: absolute on a position: relative parent |
§ 09
Five Pitfalls That Look Like Bugs
-
01
Fill inside a Hug parent, same axis. The circular dependency from section two — Figma silently converts one side to Fixed. Fix the structure, not the symptom: give an ancestor on that axis a resolved size, or move the Fill child under a different parent.
-
02
Wrapping a shape-plus-text group instead of applying Auto Layout directly. Group a background rectangle with a text layer first, then apply Auto Layout to the group, and the shape and text end up negotiating Fill/Hug with each other instead of the text simply negotiating with the frame. Select the text layer alone and apply Auto Layout to it directly; give the resulting frame a fill color and corner radius, and it becomes the button background with one fewer layer to reason about.
-
03
Adjusting gap when the frame won't shrink. If a Hug frame's outer size isn't moving, check padding, not gap — gap only ever redistributes space between existing children.
-
04
Forgetting counter-axis stretch. Children default to Hug on the counter axis unless told otherwise, so a short child sitting next to a tall one in the same row won't automatically match its height. For a uniform row or column, set the shorter children's counter-axis sizing to Fill rather than adjusting alignment.
-
05
Trusting min/max precision across many wrapped Fill children. As noted in section six — verify in the actual file for anything layout-critical, rather than assuming the panel's numbers will resolve symmetrically.
§ 10
Quick Reference
| Property | What it does | Rule of thumb |
|---|---|---|
| Fixed | Ignores children and parent | Use for things that must never move |
| Hug contents | Sizes to children + padding | Resize by resizing children or padding, never the frame |
| Fill container | Takes leftover space from parent | Only meaningful once an ancestor has a resolved size |
| Padding | Space between frame edge and children | Changes a Hug frame's outer size |
| Gap | Space between siblings | Never changes a Hug frame's outer size |
| Wrap | New row/column on overflow | Pairs with min-width to decide when to wrap |
| Absolute Position | Removes a child from the flow | Deliberate exception, not a default |
Auto Layout rewards being read once, carefully, the way you'd read a spec instead of a tooltip. After that, most of what looks like unpredictable behavior turns out to be the constraint system doing exactly what it was asked to do.