Skip to content
Flow-UILive
On this page

Widget layout as data

Padding, margin, corners, gradients, and width come from the widget layout object and apply in one fixed modifier order.

Ayush MishraPublished 6 min read

Padding, margin, corners, gradients, and width come from the widget layout object and apply in one fixed modifier order. Widgets never style their own containers. That rule is how a server driven page stays visually consistent when five teams ship five widget types.

The layout concept and layout API list fields. This article is the order, the temptation to pad twice, and width in carousels.

The Flow-UI mark: two nodes joined through a filled centre

The chrome object#

layout.jsonjson
{
  "layout": {
    "margin":  { "top": 0, "left": 16, "right": 16, "bottom": 12 },
    "padding": { "top": 12, "left": 12, "right": 12, "bottom": 12 },
    "corner_radius": 12,
    "background": { "hex": "#FFFFFF", "dark_hex": "#1C1C1E" },
    "gradient": { "colors": [{ "hex": "#4F8CFF" }, { "hex": "#6CB8FF" }], "angle": 35 },
    "border": { "width": 1, "color": "#EAEAEA" },
    "width": "fill"
  }
}

Corner radius can be a number or per-corner object. Border can be dashed with dash_width and dash_gap in the Swift modifier path.

Application order (must match the modifier)#

WidgetLayoutModifier applies inside out:

  1. Padding (content to background edge)
  2. Width (fill, hug, or fraction)
  3. Background and gradient (gradient paints over background when both exist)
  4. Corner clipping (skipped when radius is zero, so shadows are not clipped for free)
  5. Border overlay
  6. Margin (space outside)

The concept doc lists padding, fill, clip, border, margin and does not always mention width as its own bullet. The modifier applies width after padding, before background. Backend authors should assume that stack. Do not send JSON that only looks right if a widget also pads in body.

DoNotPad.swiftswift
var body: some View {
    FlowText(content.title)
    // no .padding here; layout owns chrome
}
swift
.padding(8) // wrong if JSON also sends padding

Width#

fill, hug, or a fraction (0.75 or "0.75"). Fractions matter in carousels: peeking neighbours. In a vertical section, fill is the usual choice. Hug for chips and buttons that should not stretch.

Section layout is separate: arrangement, item spacing, insets. Do not put carousel snapping in widget layout. Snapping is section arrangement.

Gradient versus background#

Both can exist. Gradient draws on top of the solid background. Dark mode: both are ColorData (and gradient colors are color atoms). Send dark_hex or tokens so night appearance is not a washed blue on black.

Nested widgets#

Accordion children still have layout. Nested chrome is still the same modifier. A child with huge margin inside an accordion is a JSON choice, not a second engine.

What layout is not#

It is not flexbox. It is not a constraint solver. It is chrome around a view the widget already sized. If you need a custom split (image on the left, text on the right at a fixed ratio), that lives in the widget body, with JSON fields on data, not as generic CSS in layout.

You can ignore layout on a UIViewRepresentable wrapper, and then the page cannot restyle it. Prefer letting margin and width apply even to those wrappers.

Clipping every widget costs an offscreen pass and cuts shadows. The modifier skips clip when radius is zero. That is why a rectangular row can still drop a shadow if you add one in the view (you usually should not).

Exact modifier stack#

WidgetLayoutModifier.body is the source of truth:

  1. .padding from layout.padding (zero if omitted)
  2. WidgetWidthModifier for fill, hug, or fraction
  3. .background with a ZStack: solid ColorData then FlowGradient if gradient is present
  4. clipShape on an UnevenRoundedRectangle only when radii are non-zero
  5. .overlay stroke, dashed when dashWidth and dashGap are set
  6. .padding from layout.margin

Concept docs sometimes list padding, fill, clip, border, margin without calling out width as its own bullet. Trust the modifier. Backend authors should assume width sizes the padded content, not the margin box.

width JSON accepts "fill", "hug", a number, or a numeric string such as "0.75". In a vertical section, fill is the usual choice. Hug for chips that should not stretch. Fractions in a carousel make the next item peek. Section layout (arrangement, item spacing, insets) is a different object. Do not put snapping in widget layout.

Gradient versus background, dark mode#

Both can exist. Gradient draws on top of the solid fill. Both use color atoms, so dark_hex and tokens apply. Send dark variants on surfaces or you will ship a washed blue on black.

Accordion children still have layout. Nested chrome is the same modifier. A child with huge margin inside an accordion is a JSON choice, not a second engine.

Try the keys in the playground and keep the layout concept plus layout API open while you author. The schema article is the contract this chrome sits in. The atoms article is the paint inside the padded area.

Why a fixed order exists#

If padding applied after the border, the stroke would sit inside the padded hole. If margin applied before background, the fill would include the outer gap. If width applied after margin, a fraction would size the margin box and carousels would lie. The modifier is the contract. JSON authors and widget authors share it. Changing the order would restyle every page in production. Do not "fix" one widget by padding in body.

Dashed borders need both dash_width and dash_gap. Uneven corners use an object: top_left, top_right, bottom_left, bottom_right. A bare number rounds all four. Gradient angle of 0 flows leading to trailing, 90 top to bottom, matching the layout API.

Section insets are not widget margin. Mixing them is how a carousel gets double gutters. Put region padding on the section. Put row chrome on the widget.

Widget layout is data. One modifier order. Do not pad in the view if the JSON already did.

Width after padding is easy to miss when you read the concept list. Remember it when a carousel item is too wide: the fraction sizes the padded body. Reduce padding or reduce the fraction, not the widget's inner VStack spacing, if the peek looks wrong.

Authoring rules that keep chrome honest#

Send width: "fill" on feed rows. Send a fraction only when the section arrangement is carousel and you want a peek. Send hug on tag rails and button rows that should size to content. Do not send fill and then hug in the view with a fixed frame. The modifier already ran.

Background hex without dark_hex will look wrong at night unless you use a token the theme maps to a dynamic color. Gradient plus background is valid: gradient paints over. Border without corner radius still strokes a rectangle. Border with radius strokes the same UnevenRoundedRectangle used to clip.

If a designer wants inner spacing between title and image, that is the widget body (spacing on a VStack), not layout.padding. Padding is content to background edge. Margin is outside the chrome. Mixing those three is the usual "why is this row huge" bug.

The renderer applies this stack in pages, sheets, headers, footers, and nested children. One mental model. That is the point of layout as data. If a row looks wrong, read WidgetLayoutModifier before you pad the view. The order will not change to flatter one payload. Document the order in your backend handbook so authors stop guessing. The modifier is the handbook. Backend and iOS should paste that stack next to every layout example they share.

More on the blog.

Read the source

Flow-UI is MIT licensed. The schema, renderer and starter widgets live on GitHub.

GitHub

Get started with Flow-UI

Install the Swift package, register a widget, and render a page from JSON.

Get started