Skip to content
HotLayout Docs

Contract changelog

This page is a reference for whoever maintains the integration: what each version of the contract added since 8.0.0, newest first, and what it means for an app that has not been updated. It covers the widget schema, the theme contract and the data dictionary, which are versioned independently.

Related pages: index · compatibility · widgets · theme · localization · quickstart for Flutter · quickstart for React

How to read this page#

  • Current versions: widget schema 9.28.0, theme 4.0.1, data dictionary 1.7.0, icon set 1.0.0.
  • Every minor version adds only optional things. A renderer of an older minor keeps working without a change; see the rule on the compatibility page.
  • "Older renderer" says what a renderer that has not been updated shows for a document that uses the addition.
  • A major version is the exception: a renderer of the previous major does not draw the document at all. 9.0.0 and theme 4.0.0 are such versions; read moving from 8 to 9 first.
  • Publish a document that uses an addition when the apps with a renderer that reads it are with the users. The table below says which package version that is.

Which package reads which schema#

Schema @hotlayout/react hotlayout_flutter
9.28 0.26 not yet
9.27 not yet not yet
9.26 not yet not yet
9.25 0.23 not yet
9.24 not yet not yet
9.23 not yet not yet
9.22 not yet not yet
9.21 not yet not yet
9.20 not yet not yet
9.19 0.18 (0.17 performs half of the additions) 0.19 (0.18 performs half of the additions)
9.18 0.15, 0.16 0.16, 0.17
9.15 – 9.17 0.14 0.15
9.14 0.13 0.14
9.13 0.12 0.13
9.12 0.11 0.12
9.11 0.10 0.11
9.10 0.10 0.10
9.9 0.10 0.9
9.8 0.9 0.8
9.7 0.8 0.7
9.6 0.8 0.6
9.5 0.7 0.5
9.4 0.6 0.4
9.3 0.5 0.3
9.2 0.4 0.2
9.1 0.2, 0.3 0.1

Every row: theme contract 4.0.x. A newer package reads every older 9.x document. The notes under each version below name the package version that first drew it; the per-version changes of each package are in the CHANGELOG.md of its repository (React, Flutter).

Widget schema#

9.28.0 — reader rules for pictures, lists, theme numbers, names and focus#

Released 2026-10-19. A minor version without a new field: a document is not changed by a single byte. It states rules every renderer needs and that the contract did not say, so each renderer had found the same gaps on its own.

  • Picture address. Without the host's word a renderer loads only an https address of the openUrl form: a host name with a letter top-level label, a port 1…65535, no IP literal in any notation, no localhost, no user info. The host may replace the decision (resolveImageUrl in React); javascript: and vbscript: are never pictures. The network limits are data: a body of at most 10 485 760 bytes, at most 3 redirects, each judged by the same rule, no cookies and no Referer, 30 seconds per picture, the longer side decoded at no more than 4096 pixels; on the web pictures load lazily. Rules: x-imageSource; table fixtures/image-source/image-source-cases.json.
  • List budget. A screen draws at most 2550 instances of list templates, nested lists included, counted in document order depth first; what does not fit is not drawn and is reported. The builder does not publish a screen whose worst case (every list at its limit, 50 without one, nested lists multiplied) is above 2550 (two nested lists of 50), so every published screen is drawn whole; the budget only cuts a document made elsewhere. Rules: x-listBudget; table fixtures/repeater-filter/list-budget-cases.json.
  • Theme numbers. A number of a theme outside the bounds of the theme schema, or infinite, is replaced by the same token and field of the built-in theme of the same brightness when it has one, otherwise by the nearest bound, and reported. Rules: x-themeNumbers; table fixtures/theme-numbers/theme-number-cases.json.
  • The name of a pressable card leaves out text that paints nothing: opacity 0 on the text or a node between the card and it, or a text colour with opacity 0. Rules: x-silentText; table fixtures/accessible-name/silent-text-cases.json.
  • Focus of a modal layer that cannot be dismissed: on the web Tab may leave the renderer at either end; a full trap only when the host asks (trapFocus in React). Rules: x-modalFocus.
  • Older renderer: may load pictures from IP addresses and localhost with cookies and Referer, draw every instance of nested lists, draw a theme number as it is, announce transparent text, and keep Tab inside a layer that waits for an action.
  • Renderers: React 0.26.

9.27.0 — the record of a page stays live#

Released 2026-10-17. A minor version without a new field: a document is drawn as before while its data stay the same.

  • A live record. The record a screen got from a list row (and the record a modal layer or a carryRecord page inherits) follows the data: whenever the host gives new collections, the renderer finds the record again by the value of <item>.id — first in the collection the row came from, then in every other collection the document lists for the same Repeater.item — and shows the new values in place. The first record with the key wins and is taken whole; a record found nowhere keeps its last values; a record without an id (a non-empty string or a finite number) stays as it was opened. The stack and the screen-shown event do not change, and values with animateChanges count to the new number. Rules: x-liveRecord.
  • Data, not words: the block x-liveRecord at the root of screen-schema.json; new groups entitySourceCases, liveRecordCases and liveFlowCases in fixtures/scope/record-scope-cases.json; a screen fixture fixtures/live-record-screens.json.
  • Older renderer: the screen keeps the record of the navigation until it leaves the stack — it shows the old values until the person goes back.
  • Renderers: not yet released.

9.26.0 — long press, groups in a list from data, a sheet closed by a drag#

Released 2026-10-16. A minor version: every new field is optional and has no default, and a document without them is drawn as before.

  • Long press. longPress next to action on a pressable Column, Row, Container, Stack and on ListTile — the same action form (catalog, track, haptic, carryRecord, paramBindings). It fires while the finger is still down, after 450 ms within a touch slop of 10 logical pixels; the press of action then does not run. A container with only a long press is pressable. longPressLabel — the name of the action for assistive technologies — is required when the long press does something: a custom semantics action, the context menu key and Shift+F10. Never on a button. Rules: x-longPress.
  • Groups in a list from data. Repeater.groupBy — a path of the record (activity.day): records that follow one another with the same value form a group, and the second child of the Repeater is drawn before each group in the scope of its first record. Groups are formed after where and limit, in the order of the data. Only a list in a column. Rules: x-repeaterGroup.
  • A sheet closed by a drag. A modal sheet follows the finger down and closes when dragged past 0.3 of its height or thrown faster than 700 logical pixels a second — the same step back as a tap on the scrim. dismissible: false on the screen entry of a modal screen turns off the drag, the tap on the scrim and Esc. Rules: x-sheetDismiss.
  • Data, not words: the blocks x-longPress, x-repeaterGroup and x-sheetDismiss at the root of screen-schema.json; new tables fixtures/long-press/long-press-cases.json, fixtures/repeater-group/repeater-group-cases.json, fixtures/sheet-dismiss/sheet-dismiss-cases.json; a screen fixture fixtures/longpress-groups-sheet-screens.json.
  • Older renderer: a hold stays a press and a container with only a long press is not pressable; the records are drawn without group headers; a sheet has no drag, and dismissible: false is not read — the scrim and Esc still close it.
  • Renderers: not yet released.

9.25.0 — steps that fill smoothly, a glowing scale, a shadow in the colour of the card, press feedback, haptics#

Released 2026-10-16. A minor version: every new field is optional, and a document without them is drawn as before, except for two rules without a field: steps of a bar fill smoothly during a transition, and a pressed node responds to the finger.

  • Steps that fill smoothly. While the value of a ProgressBar with segments runs a transition (animateChanges, animateIn), the filled steps are a number with a fraction that goes from the steps at the start to the steps at the end with the curve of the transition; the step that is filling is filled in part. At rest a step is whole or empty, as before. Rules: x-segmentsTransition.
  • A glowing scale. glow: 'soft' | 'strong' on ProgressBar and CircularProgress: a blurred copy of the fill (its colour or gradient, with its ends) under the scale; the track does not glow. A bar with steps lights one step: the current one when currentColor paints it, otherwise the last step with any fill. It takes no room and is not clipped by the box of the node. Rules: x-progressGlow.
  • A shadow in the colour of the card. style.shadowColor: 'background': every layer of style.shadow takes the hue of the own background of the node and keeps its opacity; a gradient gives the stop nearest to its middle. Rules: x-shadowColor.
  • Press feedback. The node that performs the action of a press scales to 0.97 (a card, a row) or 0.96 (a button), or fades to 0.7 (an icon button, a ghost button, a navigation item), down in 80 ms and back in 160 ms with a slight overshoot; "reduce motion" fades only. pressFeedback: 'none' on a node turns it off. Rules: x-pressFeedback.
  • Haptics. haptic next to track on an action — light, medium, heavy, selection, success, warning — a request to the host once per press, before the action; the renderer never vibrates by itself. Rules: x-haptic.
  • Data, not words: the blocks x-segmentsTransition, x-progressGlow, x-shadowColor, x-pressFeedback and x-haptic at the root of screen-schema.json; new groups in fixtures/progress/progress-cases.json and fixtures/actions/action-cases.json, new tables fixtures/shadow-color/shadow-color-cases.json and fixtures/press-feedback/press-feedback-cases.json; a screen fixture fixtures/glow-press-screens.json.
  • Older renderer: steps switch on whole during a transition, scales do not glow, a shadow keeps its own colour, a pressed node does not move, and the host is asked for nothing.
  • Renderers: not yet released.

9.24.0 — values that grow when shown, a busy swipe, a dot that flashes, the top zone#

Released 2026-10-16. A minor version: every new field is optional, and a document without them is drawn as before.

  • Values that grow when shown. animateIn: true on Text, ProgressBar and CircularProgress: when the node is shown — the first screen, a screen pushed on top, a screen shown again by back or a section switch, a node that appears — the value grows from zero with the duration, curve and steps of animateChanges. A scale grows its value (a typed-in value too) while the max stands; a text grows its numbers from data. All nodes of a screen start together; a screen under a closed modal layer does not grow again.
  • Swipe to confirm: busy until the answer. Released past the threshold, the action runs at once and the thumb glides to the end and stays there with a spinner: until the shown screen changes, until a promise the host returned from the action settles (then it slides back), or — with no promise — for at most 8 seconds. Presses and gestures do nothing meanwhile; the state is announced as busy. The action none and an invalid action return at once. The block x-swipeConfirm of 9.22 is unchanged; x-swipeConfirmBusy refines it.
  • A dot that flashes. The dot of badgeWhen appears with a short flash (scale 0 → 1.25 → 1 in 320 ms) and fades (200 ms) when its condition changes while the screen is shown; a dot that is there when the screen opens does not move.
  • The top zone. pin: 'top' — a second word of the node field pin: children of the screen root in a row at the start (right after the app bar, or first) stand at the top under the app bar and do not scroll; the body scrolls below them and is clipped at their bottom edge. Not read with bodyTop: 'screenTop'.
  • Data, not words: the blocks x-valueAppear and x-swipeConfirmBusy at the root and x-style.layout.navBadgeMotion, x-style.layout.topZone in screen-schema.json; new groups in fixtures/value-transition/value-transition-cases.json, fixtures/swipe-confirm/swipe-confirm-cases.json and fixtures/bottom-zone/bottom-zone-cases.json, a new table fixtures/top-zone/top-zone-cases.json; a screen fixture fixtures/appear-swipe-dot-screens.json.
  • Older renderer: values are there at once, the thumb returns to the start after the action, the dot appears and disappears at once, and pinned nodes scroll with the body.
  • Renderers: not yet released.

9.23.0 — a filtered list, a gradient on text, a compact number#

Released 2026-10-16. A minor version: every new field is optional, and a document without them is drawn as before.

  • A filtered list. where on Repeater — a condition of the same form as visibleIf, judged on every record in the scope of that record. A record that does not pass does not exist for the layout: a grid packs without holes, a carousel counts its stops and dots by the cards drawn, numbers count the records drawn, and limit takes the first records that passed.
  • A comparison with another path. Any condition (visibleIf, styleWhen, disabledIf, where) may compare with the value under another data path: valuePath instead of value, with the same comparison operators. With no value under that path (or the empty string) a where holds for every record — a filter nobody set does not filter — and any other condition does not hold.
  • An empty state of a filtered list. isEmpty / isNotEmpty on a collection path may carry item and the same where: then only the records that pass are counted.
  • A gradient on text. style.textGradient on Text paints the glyphs with a gradient token of the theme or an own gradient of the same form as a gradient background, over the whole content box of the text. A part that names its own colour keeps it; color stays the fallback.
  • A compact number. notation: 'compact' beside format: 'number' (also in a template value): 48 200 is written «48.2K» in English and «48,2 тис.» in Ukrainian — one fraction digit, half up, the suffix from a closed table by language (other languages use the English one). Below 1 000 the number is written in full; a plural word agrees with the full value.
  • Data, not words: the blocks x-listFilter, x-textGradient and x-compactNumber at the root of screen-schema.json; a new table fixtures/repeater-filter/repeater-filter-cases.json, new groups in fixtures/inputs/input-cases.json, fixtures/template/template-cases.json and fixtures/rich-text/rich-text-cases.json; a screen fixture fixtures/filter-gradient-compact-screens.json.
  • Older renderer: every record of a list is drawn and counted, a comparison by valuePath has no operand, the text is painted with its fallback colour, and the number is written in full. Publish a filtered list when the renderer that reads 9.23 is with the users.
  • Renderers: not yet released.

9.22.0 — a floating navigation bar, a dot on an item, swipe to confirm, a dashed border#

Released 2026-10-16. A minor version: every new field is optional, and a document without them is drawn as before.

  • A floating navigation bar. float: true on NavBar: the bottom zone (a pinned block and the bar) lies over the body, the body reaches the bottom of the screen and scrolls under it, and the end of the content gets the height of the zone so the last row scrolls clear. The bottom safe inset is outside the box of the bar — the pill hangs above the home indicator. Its look is the style of the node: margin, radius, backdropBlur, shadow. Read only where the bar stands at the bottom.
  • A dot on a navigation item. badgeWhen on NavItem — a condition of the same form as visibleIf. While it holds, the icon of the item carries a dot of the core token accent, 8 across, centred on the top corner at the end of the line. No number; the accessible name stays the label.
  • Swipe to confirm. confirm: 'swipe' on Button: a track with a thumb; the action (and its track event) runs only when the thumb is dragged past nine tenths of its travel and released. A tap without the drag does nothing; from the keyboard and for assistive technology it is an ordinary button.
  • A dashed border. style.border.style: 'dashed': dashes of 3 and gaps of 2 border widths along the centre line of the border, following the corner arcs, the count rounded so the loop closes evenly. The same room as a solid border.
  • Data, not words: the block x-swipeConfirm at the root and x-style.layout.navFloat, x-style.layout.navBadge, x-style.layout.borderDash in screen-schema.json; new groups in fixtures/bottom-zone/bottom-zone-cases.json, new tables fixtures/swipe-confirm/swipe-confirm-cases.json and fixtures/border-dash/border-dash-cases.json; a screen fixture fixtures/float-nav-swipe-screens.json.
  • Older renderer: an ordinary bar with the body above it, no dot, a solid border — and an ordinary button where a tap acts. Publish a swipe button when the renderer that reads 9.22 is with the users.
  • Renderers: not yet released.

9.21.0 — values that change smoothly, a ticking countdown, round ends, the app bar after scrolling#

Released 2026-10-16. A minor version: every new field is optional, and a document without them is drawn as before.

  • A value that changes smoothly. animateChanges: true on Text, ProgressBar and CircularProgress: when the data change a value already on the screen, a number counts from the old value to the new one (whole steps when both are whole, otherwise the digits of the format) and a scale grows. Duration (700 ms) and curve (e = 1 − (1 − p)³) are data, not fields. No animation on the first show, with reduce motion, when the language changes or when one side is not a number.
  • A countdown that ticks. The binding format countdownPart on a date path, with unit (days, hours 0–23, minutes 0–59, seconds 0–59) and an optional pad: 2. A template composes the parts with its own words — “{d} d {h} h {m} min” — and a plural word can agree with a part. The renderer redraws the countdown of the shown screen every second (with seconds) or every minute; a past date gives zeros.
  • Round ends. cap: 'round' on a progress bar and a ring: the ends of the fill (and the bar track) are half-circles of the thickness, kept inside the filled part; a small value is a dot, a full ring has no seam.
  • The app bar after scrolling. styleScrolled on the app bar node (background, color, shadow, backdropBlur — nothing that changes its box) lies over the bar while the body is scrolled under it; titleShown: 'scrolled' draws the title only then, keeping its place at rest.
  • Data, not words: the blocks x-valueTransition and x-countdown at the root and x-style.layout.progressCap, x-style.layout.appBarScrolled in screen-schema.json; a shared table fixtures/value-transition/value-transition-cases.json and new groups in template-cases.json, progress-cases.json, media-chrome-cases.json; a screen fixture fixtures/value-countdown-screens.json.
  • Older renderer: a value jumps, the ends are square, the app bar does not change on scroll — and an unknown format shows the date as it is, an ISO string in the middle of the sentence. Publish a countdown when the renderer that reads 9.21 is with the users.
  • Renderers: not yet released.

9.20.0 — motion of a node, blur under a layer, glass#

Released 2026-10-16. A minor version: every new field is optional, and a document without them is drawn as before.

  • Motion. motion on any node except the root Screen: float (the node drifts along axis — vertical, horizontal or both — by up to amplitude logical pixels) or pulse (it breathes in scale and, optionally, opacity). One cycle lasts duration ms, the first starts after delay ms. The motion never changes the layout — only how the node is painted — and the pose at any moment is one formula with a sine curve. With reduce motion on, the node stands at rest.
  • Blur under a layer. backdropBlur (1…40 logical pixels) on the entry of a modal screen blurs everything under the layer, with the app bar and navigation of the screen below; the scrim lies over the blur. On a page it is not read.
  • Glass. style.backdropBlur — the same number — blurs whatever is painted under the box of a node, within its radius; the node's own translucent background lies over it.
  • Data, not words: the blocks x-motion and x-backdropBlur at the root of screen-schema.json; a shared table fixtures/motion/motion-cases.json; a screen fixture fixtures/motion-blur-screens.json.
  • Older renderer: skips the fields — the node stands still, only the scrim lies under a layer, there is no glass.
  • Renderers: not yet released.

9.19.0 — free composition, money, the body under the app bar, write actions, parts of a row and a field#

Released 2026-10-12. A minor version: every new field is optional, and a document without them is drawn as before. Six groups of additions.

  • Free composition. Column, Row and Container accept a child of their own type, and a tree can be eight levels deep. Column, Row, Container and Stack can carry an action and a hidden label: the whole box becomes a button — a pressable card. A press goes to the innermost pressable under the point. The builder does not publish a pressable inside a pressable.
  • Money. Three optional fields of a binding beside format: 'currency' or 'number': currencyDisplay: 'symbol' (the sign of the currency from a closed table instead of its code), currencyPosition: 'before', sign: 'always' (a plus for a positive number; the minus is U+2212). Text.spans[place].parts (fraction, currency) gives the cents and the currency a style of their own. Without the fields a currency is written exactly as before.
  • The body under the app bar. Screen.props.bodyTop: 'screenTop' starts the body at the top edge of the screen: the app bar stands over a hero picture. The renderer adds the top safe-area inset itself, inside the box of the app bar.
  • Write actions. Two actions of the catalog that the renderer performs: setValue and toggleValue. They write a literal of the document under a data path — into the renderer's session and to the host's onWrite — and never reach onAction. Nodes of one parent that write the same path are one group of options, so a choice can be built from your own cards. Conditions isEmpty / isNotEmpty are valid on a path of any type.
  • Parts of a list row and of a text field. A list row: the style and the states of the trailing text, a caption under it, the colour and a tinted tile of the leading icon, titleMaxLines / subtitleMaxLines. A text field: the look of its box (fieldRadius, fieldBackground, fieldBorderColor, fieldBorderWidth, fieldHeight), a mark at the start and at the end (prefix / suffix), a character counter.
  • Descriptions, icons and dates from data. The alt of an image and of an icon can be a string of the document — a binding or a template, translated like any text. iconPath takes the name of an icon from data (only a name of the built-in set is drawn). dateParts beside format: 'date' chooses the parts of a date: dayMonth, weekdayDayMonth, dayMonthYear. unset in a width layer (styleOn / propsOn) removes a field on a wider class.
  • The value of onWrite now also comes from buttons and cards, not only from inputs. Treat the path and the value as input, as before.
  • Data, not words: the blocks x-money, x-dateParts, x-iconFromData, x-choice, x-conditions at the root of screen-schema.json; x-actions.write; new groups of rows in the shared tables under fixtures/.
  • Older renderer: does not read the new fields. A container with an action is not pressable; a currency is written with its code after the amount; the body starts under the app bar; setValue and toggleValue reach the host as an unknown action and write nothing; a list row and a text field have their built-in look.
  • Renderers: @hotlayout/react 0.18 and hotlayout_flutter 0.19 draw all of it. New for a React host: the prop safeAreaTop. hotlayout_flutter has no list of allowed write paths — the host sees every write in onWrite.

9.18.0 — adaptive layout by width#

Released 2026-10-12. A minor version: no existing document became invalid or changed. One document serves every width: it is built for a phone, and wider classes override it.

  • Width classes. The renderer measures the width of the box the screen is drawn in — not the window and not the device — and picks a class: compact below 600 logical pixels, medium from 600, expanded from 1024. The boundaries are data of the contract.
  • Added — fields of a node: showOn (the classes a node is shown on, independent of visibleIf), styleOn: { medium?, expanded? } (the same fields as style, laid over it from narrow to wide; a state of styleWhen is still stronger) and propsOn: { medium?, expanded? } (a closed list of layout properties per widget — content is never overridden).
  • Added — Screen.contentMaxWidth (320…1920): the content of the body, of the app bar and of the bottom zone stands in a centred column while the backgrounds stay edge to edge.
  • The navigation bar on a wide screen. On medium and expanded the bar stands at the top — inside the app bar when the screen has one, as a bar of its own otherwise. NavBar.widePlacement: 'bottom' keeps it at the bottom.
  • A modal sheet on a wide screen is a window in the middle, at most 560 wide. The content of the window is always compact.
  • Whitespace, named: in a shown string, runs of spaces, tabs and line breaks become one space and the edges are trimmed; a no-break space stays.
  • Data, not words: x-style.layout.widthClass, wideNav, wideModal, shownText; a shared table fixtures/adaptive/adaptive-cases.json; a screen fixture fixtures/adaptive-screens.json.
  • Older renderer: draws the phone layout at every width. It ignores showOn, so a node meant only for a wide screen is drawn on a phone too — mind this for a node in the leading zone of an app bar.
  • Renderers: @hotlayout/react 0.16; hotlayout_flutter 0.16 (classes, overrides, the content column) and 0.17 (navigation and windows on a wide screen). New for a React host: the prop initialWidthClass, for server rendering.

9.17.0 — text emphasis, list items, the own style of a ring label#

Released 2026-10-11. A minor version: no existing document became invalid or changed.

  • Added — emphasis: style.fontStyle (normal / italic) and style.textDecoration (none / underline / lineThrough) on a node; the same two and weight (regular / medium / semibold / bold) on a part of a text (Text.spans[place]). They are inherited like color and text style, and read by Text only. normal and none are real values: they cancel what was inherited.
  • Added — list items: Text.marker (none / bullet / number). The number counts the visible items of a group of neighbours; the width of the marker column follows the group as the document has it, so it does not jump with data.
  • Added — the label of a progress ring: CircularProgress.labelTypography and labelColor. The label does not scale with the ring and does not inherit the style of the node.
  • Not in this version: markup inside a string and line breaks (a second paragraph is a second Text), links in a part of a text, nested list levels, a marker of your own.
  • Data, not words: x-style.layout.textEmphasis, listMarker, progressLabel; new groups in fixtures/rich-text/rich-text-cases.json and fixtures/progress/progress-cases.json, and a table fixtures/list-marker/list-marker-cases.json.
  • Older renderer: draws the text upright, without a line and without a marker; a ring label in the built-in style.
  • Renderers: @hotlayout/react 0.14, hotlayout_flutter 0.15.

9.16.0 — an app bar from a design: a node in the leading zone, the title at the start, an own icon size#

Released 2026-10-11. A minor version: no existing document became invalid or changed.

  • Added — zone: 'leading' on a Button or an IconButton that is a child of an AppBar. The node stands in the leading zone instead of the back arrow, whatever showBack says, and is not counted among the actions. It has no built-in "back": what it does is its own action. A node hidden by its condition gives the zone back to showBack. It is at most half of the bar wide; the title shrinks first, the actions never.
  • Added — AppBar.titleAlign (start / center): start puts the title right after the leading zone.
  • Added — glyphSize (a whole number 8…128) on Icon and IconButton: the side of the drawing. It is stronger than the word size and is not multiplied by the system font scale. The word size stays in the document for an older renderer.
  • An app bar holds up to three children — a leading node and two actions. A third action is not published.
  • Data, not words: x-style.layout.appBarLeading, appBarTitle, ownGlyphSize; new groups in fixtures/media-chrome/media-chrome-cases.json; a screen fixture fixtures/app-bar-screens.json.
  • Older renderer: does not read zone. It draws the back arrow by showBack and takes the first two icon buttons as actions; the builder puts the leading node last among the children and keeps showBack, so an older app still has its arrow and both actions. The title is centred; an icon has the size of its word.
  • Renderers: @hotlayout/react 0.14, hotlayout_flutter 0.15.

9.15.0 — alignment of children on both axes#

Released 2026-10-11. A minor version: no existing document became invalid or changed.

  • Added: Column.justify, Container.align and justify, Screen.align and justify — the same words as in Row. All five are optional and have no default written into a document.
  • The cross axis places only the children that do not stretch (a node with its own width, a button that is not full-width, a badge, an icon). A stretched child fills the width under any word.
  • The main axis acts only where there is free space: a column with a named style.height or style.minHeight, a layer that fills a stack, the body of a screen that is shorter than the visible area. A field without free space is not an error; the builder says "not in effect now" next to it.
  • In a row container the axes swap: align is the vertical axis.
  • gap stays the smallest distance between children; between with one child is start.
  • Data, not words: x-style.layout.alignment; a shared table fixtures/align/align-cases.json; a screen fixture fixtures/alignment-screens.json.
  • Older renderer: places the children at the start of both axes, as before.
  • Renderers: @hotlayout/react 0.14, hotlayout_flutter 0.15.

9.14.0 — a form for a survey: required fields, a disabled button, a number field#

Released 2026-10-10. A minor version: no existing document became invalid or changed, and there is no new widget. 9.13.0 taught a screen to ask; this version lets it hold the person until the answer is given.

  • Added — required: "required": true on TextField, Checkbox and ChoiceGroup. A text field is filled when its text can be heard (spaces alone are not an answer), a number field when there is a number, a checkbox when it is on, a choice group when an option is chosen. A Switch has no such field — it always has a value. A required text field and checkbox draw an asterisk after the label; a screen reader gets the state "required" instead of the asterisk.
  • Added — a button that waits for the form: "requiresFilled": true on Button and IconButton. The button is disabled while at least one required field of its screen — one that is shown and not disabled — is not filled. The renderer works it out itself, from the values under the paths of the fields: what the person typed, and what your data already holds, both count.
  • Added — disabledIf on a button: the same single condition as visibleIf (and as disabledIf of the inputs of 9.13). With requiresFilled, the button is disabled when either holds.
  • A disabled button, named: half transparent, on its place, still reachable with the keyboard and announced as disabled. Pressing it does nothing — your onAction is not called and no track event is sent.
  • Added — a number field: "valueType": "number" on TextField. It takes a path of the type number, opens the numeric keyboard and writes a number to your onWrite. A comma and a dot are both the decimal separator; Arabic-Indic digits are read; there are no thousands separators and no exponent — 1,200 is one point two, 1 200 is not a number.
  • When the typed text is not a number (12a, a lone minus) or the field is cleared, the field writes the empty string "" under the number path — it means "no number". Treat "" under a number path as an absent value; do not turn it into zero.
  • The value of onWrite is now string | boolean | number. Widen the type again.
  • Validation is still yours. There is no "looks like an email", no "at least 18" and no combination of conditions in a document; required only holds the button. A renderer of 9.13 ignores all of this, so a form can reach you empty: check what you receive, as before, and answer with the error text of a field.
  • A choice writes like an input: a ChoiceGroup with a bound value writes the chosen option to the session of the renderer as well as to onWrite, so conditions and buttons of the same screen react at once. A required ChoiceGroup must have a bound value — the builder refuses a required group with a literal one.
  • Action parameters, corrected: a value read by paramBindings becomes a parameter only when it is a non-empty string. A boolean or a number (of a number field too) does not; the literal from params acts. The note of 9.13.0 that said a boolean becomes "true" / "false" was wrong.
  • Data, not words: x-style.layout.form (required, filled, button, number, choice) in screen-schema.json; fourteen policy rows from unreadableRequired; three feature names; twelve new groups (300 rows) in fixtures/inputs/input-cases.json; a screen fixture fixtures/survey-screens.json.
  • Not in this version: validation rules and composed conditions; a text "fill in this field" and the reason a button is disabled; a mark for a required choice group (it has no label of its own); a disabled list row; own colours of a disabled button.
  • Older renderer: does not read the new fields — the button is always enabled, there is no asterisk, and a number field is a text field that writes a string to the number path. Publish a document with these fields when the apps with a 9.14 renderer are with the users.
  • Renderers: @hotlayout/react 0.13.0 draws all of it (onWrite now carries string | boolean | number — a TypeScript change for the host; no new props). hotlayout_flutter 0.14 draws it too; its onWrite may give a num.

9.13.0 — inputs: a text field, a switch, a checkbox#

Released 2026-10-10. A minor version: no existing document became invalid or changed, and no existing widget got a new field. Until now a screen showed things and let the person pick one of several (ChoiceGroup); from this version it asks.

  • Added: three widgets. TextField — a label above, the field (one line or several), a helper or an error below. Switch — a row with a label and a switch at the end: a setting that applies at once. Checkbox — a row with a box and a label: a consent, a choice. There are 28 widgets now.
  • Where the value goes: every input has value — { "kind": "binding", "path": "…" }, a path of the data dictionary of exactly the type of the input (text for a field, yes/no for a switch and a checkbox). The input shows the value under the path and writes the change to that path: into the session of the renderer — so conditions and texts on the same screen react at once — and to the host through the same callback a ChoiceGroup uses (onWrite(path, value)). There is no literal value and no fallback text: an input without data is empty, a toggle is off. A document whose input has no value is not published.
  • When it is written: on every change the person makes — every keystroke of a text field, every press of a toggle. The renderer adds no delay and does not wait for the field to lose focus, so an action pressed right after typing sees what was typed. Your backend probably does not want a request per keystroke: debounce in the host. Clearing a field writes the empty string. Text that is still being composed (IME) is not written.
  • What the host receives: a string from a text field, a boolean from a switch and a checkbox. onWrite used to carry strings only; widen the type. The paths are named by the document, so keep writablePaths (React) narrow.
  • Validation is yours. The contract has no "required" and no "email format". A text field has error — a text, usually bound to a string in your data. Check what was written and put the message under that path; a non-empty message puts the field into the error state and replaces the helper.
  • disabledIf: a condition of the same form as visibleIf on each of the three widgets. A disabled input takes no focus and writes nothing.
  • The keyboard, named: keyboard is one of five intents — text, email, number, phone, url — and the artifact says what each becomes on the web and in Flutter (x-style.layout.inputs.textField.keyboards). number is a keyboard, not a type: the value stays a string. There is no password, on purpose: the value travels to the host in the open, and a layout that arrives from the network must not be able to ask for a password. The action key of the keyboard moves to the next field or closes the keyboard; it never performs an action.
  • Autofill and privacy: a renderer asks the platform only for the hint the intent names (an email, a telephone number, a web address) and never for credentials, a payment card, a postal address or a one-time code. A value the platform put into a field before the person touched it is neither kept nor written. A host can turn every hint off. Still: a document decides which paths it writes and how a field looks, so treat the paths in writablePaths as data a layout can collect, and do not put there anything a layout from the network must not be able to ask for.
  • The look, named: the box of a text field, the track and the thumb of a switch, the box and the tick of a checkbox are data of the artifact, painted with the core colours of the theme — there is no new role group. The tick is the check drawing of the icon set. The control of a switch stands at the end of the row, of a checkbox at the start, and both follow the reading direction. The whole row is pressable. The style of the node is the box of the whole widget; style.color and style.typography are not read.
  • Limits, named: maxLength is counted in Unicode code points and the renderer cuts the input itself; lines is 1 to 8.
  • Where an input may stand: the screen root, Column, Row, Container, Tab. Not inside a Repeater: every row would write to the same path.
  • Data, not words: x-style.layout.inputs (rules, textField, switch, checkbox) in screen-schema.json; twenty policy rows from inputWithoutValue; three feature names; shared cases in fixtures/inputs/input-cases.json (248 rows); the fixture fixtures/inputs-screens.json.
  • Not in this version: a slider, a select and a date (the next minor, with the same form of value); a password and a one-time code; validation rules in the document; a counter of the length; a read-only field; a hidden label; a link inside the label of a checkbox; an own look of the field box.
  • Older renderer: does not know the three widgets and does not draw them at all — the user sees the form without its fields. Publish a document with inputs when the apps with a 9.13 renderer are with the users.
  • Renderers: @hotlayout/react 0.12.0 draws all of it. onWrite now carries string | boolean (a TypeScript change for the host), and there is a new prop autofill (default true; false turns every autofill hint off — pass it for a document from a source you do not control). The value of a field is user input: validate it on your backend, and throttle your own network writes — the renderer writes on every keystroke. hotlayout_flutter 0.12.0 does not draw 9.13 yet — it opens a 9.13 document and shows it without the inputs.

9.12.0 — the bottom zone: a navigation bar and a block pinned to the bottom#

Released 2026-10-10. A minor version: no existing document became invalid or changed, and its first frame is the same — a screen without the new things has no bottom zone at all. A screen has three zones since 9.9.0 (the app bar stays, only the body scrolls); this version fills the third one.

  • Added: two widgets — NavBar, the bottom navigation bar, and NavItem, its item (icon, label, action). A NavBar is the last child of a screen root (the mirror of AppBar, which is the first) and takes two to five items. It stays at the bottom while the body scrolls, runs from edge to edge, and is not drawn on a screen shown as a modal layer. There are now 25 widgets.
  • Added: pin: "bottom" — an optional field of a node, next to fill, weight, layer and pull. Read on exactly one node: the last child of the screen root, not counting the NavBar. That node leaves the scrolling body and stands in the bottom zone — a "Continue" or "Redeem" button under long content, always on screen. To pin several things, put them into one container. In a modal sheet the pinned block stands at the bottom of the sheet.
  • Added: the catalog action navigate.section (x-actions.section): the screen stack is reset to the named screen — everything above it, modal layers included, is closed. This is what an item of the bar does, and any other node may do it too. navigate is unchanged: it still puts a screen on top. Pressing the item of the screen you are on does nothing.
  • Added: the cause switch of the screen-shown event (x-screenShown.causes): depth 1, no record. A renderer remembers neither the stack nor the scroll position of a section, and back on a section screen is the host's, exactly like back on the first screen.
  • The geometry, named: from top to bottom the zone is the pinned block, the bottom padding of the root, the navigation bar, the safe inset. The body ends at the top edge of the zone and is clipped there, so a pinned block without a background shows the background of the screen, never the text sliding under it. With a pinned block the body ends with the gap of the screen instead of the bottom padding — the last line of the content always stops above the block.
  • The bar, named: equal shares of the width among the visible items, in reading direction; an icon above a one-line label that is cut with an ellipsis; paddings, the gap, the icon and the label are steps of the theme, so the height follows the theme (61 with the default one). Built-in look: the surface colour with a one-pixel top line of border; the active item is primary, the rest muted. style.background and style.border of the node replace the built-in ones. No new role group in the theme. The active item is the one whose action switches to the shown screen — there is no separate "selected tab" state.
  • The bottom safe inset is now the renderer's — for the bottom zone only. The renderer adds the platform inset under the content of the zone, and the background of the bar reaches the physical bottom. A host that already accounted for the inset passes zero. A screen without a bottom zone gets nothing, as before.
  • Named ahead: when text input arrives, a pinned block rides above the keyboard and the navigation bar stays under it (x-style.layout.bottomZone.keyboard).
  • One bar per section screen. The bar is a node of every section screen, not a field of the document: its labels are translated by node, like every other text. The builder keeps the copies equal — a section without the bar, or with the sections in another order, is not published — and puts the bar on the other sections with one action, translations included.
  • Data, not words: x-style.layout.bottomZone and x-style.layout.navBar in screen-schema.json; two values changed in x-style.layout.screen (bottom, body.area); sixteen policy rows from navBarNotLast; four feature names; shared cases in fixtures/bottom-zone/bottom-zone-cases.json and eleven flow rows named 9.12 sections: … in fixtures/actions/action-cases.json; a document to look at — fixtures/bottom-zone-screens.json.
  • Not in this version: one shared bar for the whole document (a component library); a remembered stack per section; a bar on a nested screen; a counter badge on an item; two pinned blocks; a block pinned to the top.
  • Older renderer: does not read pin — the block is the last child of the body and scrolls with it, nothing is lost. It does not draw the NavBar at all (an unknown widget): the user sees the first section and has no way to the others. navigate.section is handed to the host as an action of the app. Publish a document with a navigation bar only when the apps with a 9.12 renderer are out.
  • Renderers: @hotlayout/react 0.11.0 draws all of it, with two new props of the host — safeAreaBottom (number | 'env', the inset under the bottom zone) and stickyBottomOffset; with scroll="page" the bottom zone sticks to the bottom of the viewport. hotlayout_flutter 0.12 draws it; the inset comes from the MediaQuery of its own context.

Released 2026-10-10. A minor version: no existing document became invalid or changed, and there are no new widgets — both additions are fields of widgets you already draw, so an older renderer shows the row and the bar it always showed.

  • Added: carousel — an optional object in the props of a Row (only with scrollable: true) and of a Repeater (only with direction: "row", without columns). The sideways scroll snaps to the start of every card, and dots under it show the position. Three optional keys: perView (1–6 whole cards in view), peek (how much of the next card shows: a spacing token or 0–128 logical pixels; lg when absent) and indicator (dots or none). An empty object is a carousel of cards of their natural width.
  • The card width, named: (viewport − inline-start padding − perView × gap − peek) ÷ perView, never rounded. The padding of the node lies inside the scroll, so put the side padding on the carousel itself and the cards slide under the very edge. With perView the card's own width, minWidth and maxWidth are not read.
  • Stops and dots: the last card stops at the end of the scroll, not at its own start, so six cards with three in view have four stops — and four dots. One stop, or more than ten, draws no dots and keeps no room for them. The dots are not a control. Which card is open is the state of the renderer: nothing is written to the document and no event is sent to the host.
  • A data list in a row now scrolls — with the field only. Without carousel a Repeater laid out in a row is what it was.
  • Added: segments (2–12) and currentColor — optional fields of ProgressBar. The same scale cut into equal steps with a gap of the xs token: a step is filled whole or not at all (floor(ratio × segments)), the first unfilled step is the current one and takes currentColor when it is named. The gradient runs along the filled steps as along one fill. Labels under the steps ("W1…W6") are not part of the bar: build them as a row of texts with weight: 1 and the same gap.
  • Data, not words: x-style.layout.carousel and x-style.layout.progressSegments in screen-schema.json; ten policy rows from carouselOutsideHost; shared cases in fixtures/carousel/carousel-cases.json and three new groups in fixtures/progress/progress-cases.json; a screen to look at — fixtures/carousel-steps-screens.json.
  • Clarified before release (2026-10-10): the kind of a stepped bar (steps or continuous) is decided by the width the place gives it, never by its own measure, and its own minimum width is the same in both kinds — (steps − 1) × gap (progressSegments.minInlineSize: gaps); otherwise a bar in a place sized "by content" flipped between the kinds on every frame. A carousel keeps the same box with or without dots (carousel.shell), so the number of stops does not depend on whether dots are drawn. The open card belongs to the stack frame and is kept while the frame is in the stack, and each instance of a template in a data list has its own (carousel.active.heldBy, keyedBy).
  • Not in this version: a card width as a fraction or "two and a half cards"; snapping to the centre or by pages; an indicator for more than ten stops; an initial card; steps on the progress ring; the number of steps from data.
  • Older renderer: skips the fields. A Row scrolls without snapping, the children keep their own width, there are no dots. A Repeater laid out in a row does not scroll (it never did) — a carousel built from data is cut off at the edge. A stepped bar is a continuous bar with the same ratio.
  • React package: @hotlayout/react 0.10.0 draws a carousel (snapping, dots, the card width from the visible area, a list in a row that scrolls with the field) and stepped progress; the open card is the renderer's state, kept while the screen is in the stack. hotlayout_flutter 0.11 draws both.

9.10.0 — overlays with an offset: the place of a layer in a stack, a pulled child of a flow#

Released 2026-10-10. A minor version: no existing document became invalid or changed. A badge on the corner of an icon, chips on the edge of a progress ring, a ribbon across the corner of a card, a sheet that slides over the header and a row of overlapping avatars can now be built. Mind the number: 9.10.0 is the first version with a two-digit minor — compare versions as numbers, never as strings.

  • Added: layer — an optional field of a node, beside fill and weight. A direct child of a Stack with this field is an overlay: it stands against the box of the stack and never changes its size. Four optional keys: alignHorizontal / alignVertical (its own alignment; no key — the alignment of the stack), offset (x: { "toward": "start" | "end", "by": … }, y: { "toward": "top" | "bottom", "by": … }; by is a spacing token or 0–128 logical pixels) and rotate (whole degrees from −180 to 180, clockwise, about the centre of the layer). An empty object is an overlay too.
  • Order, named: align, then shift along the axes of the stack, then rotate. In layout the layer keeps its unshifted, unrotated box. In right-to-left languages the horizontal shift and the sign of the angle are mirrored.
  • Clipping has no new word. An overlay is cut by whoever cut before: a node with a radius, a node with a named width or height, a scroll, the screen root. A stack without those does not cut. So a badge sticks out of a card when the card is a layer of the stack, and a ribbon is cut by the card when the card is the stack itself. A renderer whose stack clips by default must turn that off.
  • Taps: an overlay with an action is hit where it is drawn, inside the box of the stack. Nothing is promised about the part that sticks out — not that it is tappable, not that it lies over what follows the stack. Shift an interactive overlay inwards.
  • Added: pull — an optional field of a node: a spacing token or 0–128 logical pixels. The node and everything after it move towards the start of the main axis of the parent, so the node lies over what precedes it and the parent gets shorter. The gap of the parent stays: the overlap is pull minus the gap. Legal in a Screen, a Column, a Container and a Row without wrapping and without scrolling; not together with weight. The later child is drawn over the earlier one and takes the taps in the overlap.
  • Data, not words: x-style.layout.layer and x-style.layout.pull in screen-schema.json; ten policy rows from layerOutsideStack; shared cases in fixtures/offset/offset-cases.json; a screen to look at — fixtures/offset-screens.json.
  • Clarified before release (2026-10-10): pull on an AppBar is not allowed — the bar is a zone of the screen, not a child of the flow. The builder rejects the document, and a renderer does not read the field (pull.onAppBar).
  • Not in this version: a negative style.margin (there is none, on purpose); a "do not clip" word; pull inside a data list; rotation outside a stack.
  • Older renderer: skips both fields. A layer stands by the alignment of the stack and takes part in sizing it — the badge lies on the centre of the icon; a pulled node stands where the flow puts it — the sheet under the header, the avatars side by side. Nothing crashes or disappears. Feature names: layerPlacement, flowPull.
  • React package: @hotlayout/react 0.10.0 draws layer (the overlay does not size the stack, shifted and rotated, mirrored in right-to-left) and pull (a negative margin on the outer element of the node's place). hotlayout_flutter 0.10 draws both.

9.9.0 — images of any proportion and size, app bar actions, a window in the middle, a status bar request#

Released 2026-10-10. A minor version: no existing document became invalid or changed. A row of round avatars, a banner of any proportion, an app bar with its own actions on a gradient, and a "mission completed" window in the middle of the screen can now be built — and the geometry of all four is data of the contract, not a choice of each renderer.

  • Added: Image.aspectRatio also takes an own proportion — a pair { "width": 343, "height": 120 } of whole numbers from 1 to 1200. Only the ratio matters. The ratio must stay between 1 : 8 and 8 : 1; the builder does not publish a pair outside it (image_ratio_out_of_range), and a renderer clamps one.
  • Named, not new: the box of an image is the box of its node (x-style.layout.image). style.width and style.height give the size; a named height is stronger than the proportion — a 56 × 56 image with aspectRatio: "16:9" is 56 × 56. The proportion only gives the height from the width, never the width from the height. minHeight / maxHeight clamp the height either way. The natural size of the file is never read.
  • A circle is not a new word. It is equal style.width and style.height with a radius of half the side. The pill token is a number of the theme (72 in the built-in one), so it makes a circle up to a side of 144; a larger circle names its radius as an own value. The radius of the node clips the picture in every renderer.
  • Added: the actions of an app bar. An AppBar takes up to two children, and they are IconButton nodes: an icon, an accessible name, an action with track and paramBindings, visibleIf and style — everything an icon button has anywhere else. There is no array of actions in the props.
  • Named, with one changed reading: the layout of the app bar (x-style.layout.appBar). Three zones in reading order: the back arrow, the title, the actions. The title block stands in the center of the bar while it fits, and is shifted just off a zone when it does not. The empty counterweight opposite the back arrow is gone: a short title stands where it stood, a long one gets the width of one arrow back. Feature name: appBarTitleRoom.
  • Changed reading, named: the app bar is fixed; only the body of the screen scrolls. A screen has three zones (x-style.layout.screen): the app bar (the first child of the screen root), the body (every other child — the only thing that scrolls, vertically), and a bottom zone that is named and empty in 9.9. The background of the screen root lies under all zones and does not move. The body is clipped at the bottom edge of the app bar, so a transparent bar shows the background, never the text travelling under it. The padding of the screen root is split: top and sides go to the bar, sides and bottom to the body (inside its scroll), with the gap of the screen instead of the top one. The first frame of every document is unchanged; only scrolling differs. The Flutter package always pinned the bar; the builder preview and the React package scrolled it with the content. Feature name: appBarFixed. (An earlier draft of 9.9.0 said the opposite — «the app bar scrolls with the content» — and was withdrawn before release.)
  • Web block without its own height: where the screen grows with the page and the page scrolls, the host decides. By default the app bar sticks to the top while the screen is in view and paints the background of the screen root under itself; the host may switch that off, and then the bar is the top row of the block. There is no field for it in the document.
  • A modal layer has the same zones: when the content is taller than the available height, the body of the layer scrolls and its app bar stays. The area of a layer is the area the host gave the screen: a window stands in the centre of that area.
  • An action of the bar is painted by itself, not by the bar: style.color of the app bar colors the title and the back arrow; an action takes the color of its own role or its own style.color.
  • Added: ScreenEntry.modalPlacement — where a modal layer lies: bottom (a sheet, also without the field) or center (a window in the middle). It is a field beside presentation: "modal", not a third presentation: an older renderer shows a window as a sheet and keeps the layer, the record and the dismissal.
  • Named: the geometry of a modal layer (x-style.layout.modalLayer). A window is as wide as the screen area minus a margin of xl on each side, never wider than 400, as tall as its content but not taller than the area minus the same margin; taller content scrolls inside the layer. The scrim is black at 0.45. A tap on the scrim, the system back and Esc are all navigate.back.
  • Changed reading, named: a modal layer has no built-in look. Its background, radius and shadow are the style of the screen root, as for a page. The React package used to put a surface background under the layer. Feature name: modalLayerLook.
  • Added: ScreenEntry.statusBarIcons — light or dark: a request to your app about the icons of the system status bar while the screen is shown. The renderer never touches the system bar: the request arrives in the screen-shown event, and your app decides (x-statusBar). No field — no request.
  • Added: two optional fields of the screen-shown event, modalPlacement and statusBarIcons (x-screenShown.optionalFields). They are present only when the screen names them, so the events of a 9.8 document did not change.
  • Added: fourteen policy keys — imageNamedHeight, unreadableImageRatio, imageRatioOutOfRange, appBarScroll, appBarChildren, appBarExtraActions, appBarActionColor, unknownModalPlacement, modalPlacementOnPage, modalLayerLook, modalDismiss, modalOverflow, statusBarIcons, unknownStatusBarIcons; see compatibility.
  • Clarified before release (2026-10-10): a renderer draws at most two app bar actions — the first two visible ones in document order (appBarExtraActions: draw-first-two; with eight actions the title had no width). A modal layer lies over the whole page under it, app bar included (modalLayer.above) — each layer of the stack is its own stacking context. The first shown screen is always a page, also in the event (presentation: "page", no modalPlacement), even if a broken document names it modal. In a web block without its own height (scroll="page") the layer lives in the part of the block visible in the window: a sheet stands on the lower edge of that part, a window in its centre, with no position: fixed (withoutOwnHeight.layerArea); for the exact area give the block its own height (scroll="screen"). A sticky app bar paints the background of the screen root in the size of the root, tied to its top — the first frame is the same as with scroll="screen"; and a control that gets keyboard focus in the body stays below the sticky bar (scroll-margin-block-start).
  • Added: a shared table of cases, fixtures/media-chrome/media-chrome-cases.json (the proportion, the image box, the picture in the box, the radius, the app bar zones, the zones of a screen with and without an app bar — at rest and scrolled —, the modal layer on 320 / 393 / 768 and its area, the screen-shown event), two flows with a window in fixtures/scope/record-scope-cases.json, and the fixture fixtures/media-chrome-screens.json in three languages.
  • Not included, on purpose: the bottom zone of a screen (a bottom navigation bar, a button pinned to the bottom — the zone is named, the next minor fills it); an app bar that hides on scroll; content under the app bar; a third action or an overflow menu; a window that cannot be dismissed by a tap outside; the width of a window as a document field; a width derived from the height of an image; the background of the system bars.
  • Older renderer: a renderer of 9.8 draws an own proportion as 16:9 (the React package 0.9: without a proportion), does not draw the actions of an app bar (the React package 0.9 draws them after the title), shows a window as a bottom sheet, and hands over no status bar request. The React package of 9.8 scrolls the app bar with the content; the Flutter package of 9.8 already keeps it fixed.
  • React package: @hotlayout/react 0.10.0 implements all of it, with three new props of the host — scroll ('page' / 'screen'), chrome ('sticky' / 'static') and stickyOffset — because the block of a web screen may have no height of its own: by default it grows and the host page scrolls, the app bar sticks to the top and paints the background of the screen root under itself. The renderer never touches the system status bar; the request arrives in onScreenChange. Flutter package: not updated yet — it draws a 9.9 document as a 9.8 one.

9.8.0 — action catalog, analytics event, screen-shown event, record for the next page#

Released 2026-10-10. A minor version: no existing document became invalid. A manager now picks an action from a list instead of typing a string, an action can report an analytics event, the host can be told which screen the user sees, and a page opened by a button can be about the same record.

  • Added: the action catalog, x-actions in the artifact. Six named actions: none, navigate, navigate.back (performed by the renderer) and openUrl, deeplink, close (performed by your app). type is still an open string: any other action reaches your app as before. See actions.
  • The renderer opens nothing by itself. A document is untrusted input. openUrl, deeplink and close are handed to your action handler like any other action — parsed and checked: standard when the parameters passed the form, invalid (param, problem) when they did not. An invalid action is still handed over and must never be run.
  • The form of an address is data, not a platform URL parser: x-actions.forms.httpsUrl.pattern (lowercase https://, a host name of at least two labels ending in letters or punycode — so no IP address in any notation and no single-word name — a port from 1 to 65535, no user name before the host, ASCII only, at most 2048 characters) and x-actions.forms.appLink (an app scheme; forbiddenSchemes lists the schemes that are never an app link — javascript, data, file, intent, http, https and others). The packages will ship an optional helper that runs standard actions; it follows an app link only when its scheme is in the list you give it.
  • Added: action.paramBindings — a parameter taken from data: { "url": "offer.url" }. The path is read when the node is pressed, with the record of its list row. Only a non-empty string is a value; anything else leaves the literal from params (the fallback). A string that fails the form of an address is not replaced by the fallback: the action is invalid.
  • Added: action.track — the analytics event of a press: { "event": "offer_open", "params": { … } }. It is not an action: any action may carry it, and a button that only measures is none with track. Your app gets it once per press, before the action, with the same node, screen and record.
  • Added: action.carryRecord: true on navigate — the opened page gets the record of the node even when it is opened by a button outside a list ("Choose delivery" on a details screen). Without the field the rule of 9.2.0 applies unchanged: such a page has no record.
  • Added: the screen-shown event, x-screenShown in the artifact. It fires once when the top of the screen stack becomes another frame: entry (the first screen; the host restarted the flow), forward (a navigate, a modal sheet included), back (the screen beneath is shown again). There is no "hidden" event. It carries documentId, screenId, entry, presentation, cause, depth and record.
  • Changed: a value of action.params may be up to 2048 characters long (it was 128): an address did not fit. A key and a value of params must have the schema form (a string value; a key that is not empty, has no spaces and is not __proto__, constructor or prototype); a renderer skips an entry that does not.
  • Stricter in the builder, named: a document with openUrl or deeplink that has no address, or an address of the wrong form, is no longer published. It is still read.
  • Added: fourteen policy keys — standardActionExecution, standardActionParams, invalidStandardAction, actionParamBinding, closeAction, actionTrack, screenShown, screenShownOnBack, screenShownOnServer, carryRecord, and four refinements before release: unbindableParamBinding (a transition target is never read from data), keyAutoRepeat (a held key is one press), stackScreenGone (a new version of the document without a screen of the stack starts the flow again), actionParamComposition (a parameter is whole data or whole literal). Feature names: actionCatalog, actionParamBinding, actionTrack, screenShownEvent, carryRecord. See compatibility.
  • Added: a shared table of cases, fixtures/actions/action-cases.json (108 rows of the address form, 38 of the structure, 27 of the validator, 55 of what the host gets, 28 flows of presses and shows), six flows with carryRecord in fixtures/scope/record-scope-cases.json, and the fixture fixtures/actions-screens.json.
  • Not included, on purpose: chains of actions and "if-then" conditions; a number from data as a parameter; bindings in the parameters of an analytics event; an analytics name of a screen other than screenId; a "screen hidden" event.
  • Older renderer: a renderer of 9.7 hands openUrl, deeplink and close to the host as unknown actions, unchecked; it does not read paramBindings (the host gets the literal), track (no event) or carryRecord (the page has no record).
  • React package: @hotlayout/react 0.9.0 implements all of it. onAction gets standard / invalid; onTrack(event, source) is new; onScreenChange now carries the screen-shown event (documentId, entry, cause, depth, record beside the old screenId, name, presentation); runStandardAction(action, { openUrl, deeplinkSchemes, openDeeplink, close }) is the optional helper, and openInNewTab a ready openUrl. The renderer itself opens nothing.
  • Flutter package: hotlayout_flutter 0.8.0 reads document contract 9.8.0: a standard action reaches the host parsed and checked (ButtonAction.standard / invalid), runStandardAction runs it with the executors the host gives, onTrack delivers the analytics event, onScreenShown reports each shown screen (onScreenChanged is deprecated), and carryRecord keeps the record for the next page. The package opens nothing by itself and has no new dependencies.

9.7.0 — progress bar and ring: thickness, track color, gradient, size#

Released 2026-10-10. A minor version: no existing document became invalid. A progress bar and a progress ring can be drawn as the designs ask: a white ring on a gradient, a gradient bar, a ring of any size around an icon.

  • Added: three optional fields, the same in ProgressBar and CircularProgress: thickness (a spacing token or a whole number of dp), trackColor (a color reference) and gradient (a gradient token of the theme or an own gradient). Without a field the scale is drawn as before: thickness sm, track secondary, a solid color fill. See widgets.
  • Added: the size of a ring comes from the box of its node. style.width and style.height (and their min… / max… limits) on a CircularProgress name the diameter: the smaller named side of the content box. There is no separate "diameter" field. Without them the word size gives the diameter, as before.
  • Added: x-style.layout.progress — the geometry and the paint of both widgets as data: how the fill share is computed, what lies under what, where a gradient runs, where the arc starts. See compatibility.
  • The gradient runs along the filled part of the scale — from the start of the scale to the current value — so the whole transition is visible at any value. Its angle is not used. On a ring it runs along the arc.
  • Changed reading, named: a CircularProgress that already had style.width or style.height used to be a ring of the size word at the start of that box; now the ring takes that size. A ring in a box larger than itself (a second named side, a row weight, a stack fill layer) stands in the center. Feature name: ringDiameterFromBox.
  • Now stated, and renderers are being aligned to it: the arc lies inside the circle (the outer diameter equals the diameter); the track lies under the whole scale and the fill is painted over it; a scale whose limit is not a positive number is empty, not full; the end of the bar fill is rounded; the arc starts at the top, grows clockwise and is not mirrored in right-to-left scripts. Feature name: progressGeometry.
  • No new policy keys. A token the theme does not declare in a new field is tokenWithoutFieldDefault (the field is absent); a gradient with unordered stops is unorderedGradient (not drawn, the fill is the solid color). Feature names: progressLook, ringDiameterFromBox, progressGeometry.
  • Added: a shared table of cases, fixtures/progress/progress-cases.json (15 rows of the fill share, 31 of the ring geometry, 5 of the arc, 12 of the bar, 21 of the paint), and the fixture fixtures/progress-screens.json in English, Ukrainian and Arabic.
  • Not included, on purpose: rounded ends of the arc; a gradient along the whole scale (a "temperature" scale); a label of the ring in its own font or color; a ring that stretches to a share of a row.
  • Older renderer: a renderer of 9.6 does not know the three fields and draws the scale as before — the progress and its value stay, the look is lost. It draws a ring with a named style.width at the size of the size word.
  • React package: @hotlayout/react 0.8.0 draws it all (thickness, track, gradient along the filled part, ring diameter from the box, arc inside the circle).
  • Flutter package: hotlayout_flutter 0.7.0 reads document contract 9.7.0: thickness, trackColor and gradient on the bar and the ring, the ring diameter from the node box. It also fixes two older differences from the builder canvas: the arc now lies inside the circle, and a Stack placed in a Row takes its natural width.

9.6.0 — rich text: parts of a line in their own style#

Released 2026-10-10. A minor version: no existing document changed. A part of a text can have its own font and color — "1,850 Points" with a large number and a small caption on one baseline, or a bold word in a sentence.

  • Added: Text.spans — an optional map from the name of a template value of content to a style (color, typography). There is no new widget: rich text is a Text whose content is a template. The string is translated as a whole, so every language puts the parts in its own order, and the style follows the name. See widgets.
  • Added: a template value of kind text — a word with its own name ({ "kind": "text", "text": "double" }). It gives a piece of text that is not data a name a style can be attached to. It is translated under its own key, <screenId>/<nodeId>/<field>.values.<name>.text, and may be used in any template.
  • Added: x-style.layout.richText — the rules of the line as data: parts stand on one baseline; every line of the paragraph has the same height, that of the tallest declared part; who is the strut among equals. See compatibility.
  • Added: seven policy keys — spanWithoutPlace, unreadableSpans, unreadableSpanStyle, unknownSpanField, unusedSpan, emptySpanText, richTextSemantics. Feature names: textSpans, templateWord.
  • Added: a shared table of cases, fixtures/rich-text/rich-text-cases.json (34 rows of text and pieces, 10 rows of the line strut), and the fixture fixtures/rich-text-screens.json in English, Ukrainian and Arabic.
  • Validation: a style for a name the template does not declare is refused (text_span_without_place).
  • Accessibility: a screen reader hears a text with parts as one node with the whole string.
  • Not included, on purpose: underline and strike-through; a font change without naming a token; states and actions of a part; parts in other texts (titles, list rows, button labels).
  • Older renderer: a renderer of 9.5 does not know spans and draws the same string in the one style of the node — the emphasis is lost, the text is not. It does not know a value of kind text either: its place is empty (the rest of the sentence stays), so update the renderers before you publish documents with such words.
  • React package: @hotlayout/react 0.8.0 draws rich text: parts on one baseline, one line pitch for the paragraph, one text node for a screen reader.
  • Flutter package: hotlayout_flutter 0.6.0 reads document contract 9.6.0: Text.spans and the template word.

9.5.0 — icons in buttons and badges, button sizes, the icon button#

Released 2026-10-09. A minor version: no existing document changed. A button and a badge can carry an icon, a button has three sizes, and there is a round button without visible text.

  • Added: Button.icon and Button.iconPosition (start or end, in reading direction), and Button.size (sm, md, lg). All three are optional and have no default in the schema: without them a button is drawn exactly as before. What applies without the field is data — x-style.layout.defaultButtonSize (md) and x-style.layout.iconBesideText.defaultPosition (start).
  • Added: Badge.icon and Badge.iconPosition. The "chip" of a design — "4 days left", "LIMITED", "Medium ●" — is a badge with an icon and its own colors (style); there is no separate Chip widget, and a badge has no action.
  • Added: the widget IconButton — a round button with an icon and no visible text (back, info, close). Fields: icon, label (the accessible name, translatable, required and not empty), variant (the same three roles as a button: primary, secondary, ghost), size, action. See widgets.
  • Added: x-style.layout.buttonSizes (padding and text token of each size, in theme tokens), x-style.layout.iconButton (the geometry of the icon button: its side equals the height of a button of the same size) and x-style.layout.iconBesideText — one rule for an icon beside a text. See compatibility.
  • Changed interpretation — the trailing slot of a list row. A ListTile whose trailing text has an icon is now drawn by the same rule: a gap (xs) between the text and the icon, the icon centred on the line of the text and in the color of the text. The trailing text itself is onSurface unless a color is named (x-style.layout.listTileTrailingColor). The contract was silent before, and renderers differed: one drew no gap, another drew the text and the icon muted. No document changed.
  • Added: three policy keys — unknownIconBesideText, emptyTextBesideIcon, iconButtonWithoutLabel. Feature names: iconBesideLabel, iconBesideTextRule.
  • Added: a shared table of cases, fixtures/icon-text/icon-text-cases.json (39 rows: order of the parts in both writing directions, sizes, the accessible name), and the fixture fixtures/buttons-chips-screens.json in English, Ukrainian and Arabic.
  • Validation: an IconButton without an accessible name is refused (accessible_name_required) — an empty literal, a binding with an empty fallback, an empty template.
  • Not included, on purpose: pressed and disabled states of a button; a pressable chip (a filter is a widget with a state); a color for the icon different from the text.
  • Older renderer: a renderer of 9.4 draws a button and a badge without the icon and at the default size. It does not know IconButton: nothing is drawn in its place, so a screen may lose its "back" button — update the renderers before you publish documents with icon buttons.
  • Clarified before release: places where renderers could differ on the same document were closed. A date in a language the platform has no data for is shown as it arrived (missingDateLocaleData); the calendar of a date is always Gregorian (dateCalendar); a date must be a real calendar moment, not only the right shape — 30 February, month 13, 24:00, :60 are not dates (dateValueValidity); numbers round half away from zero over their shortest decimal text (numberRounding), so 1.005 is 1.01; a plural form for a fraction follows the CLDR fraction rules (pluralFractionOperands); the limit of 2^53 − 1 applies to the absolute value; an empty accessible name is decided by a character class given as data (x-accessibleName); a button label is centered (x-style.layout.buttonLabelAlign). New rows in the shared table: 171 in total. Renderers must follow these before the release.
  • React package: @hotlayout/react 0.7.0 draws button sizes, icons in buttons and badges and IconButton (a real <button> named by its label). In the trailing slot of a list row the text and the icon are now 4 px apart and centered on the text line — a visible change for rows that already had an icon there.
  • Flutter package (clarifications of 9.5.0 before release): a date must be a real calendar moment; rounding is half away from zero over the decimal form of the number; the plural word of a fractional number follows CLDR operands; the number limit is symmetric; an icon-button name made of invisible characters counts as missing; the label of a button is centred.
  • Flutter package: hotlayout_flutter 0.5.0 reads document contract 9.5.0. New: Button.size (sm / md / lg), Button.icon and Badge.icon with iconPosition (start / end, in reading order), the IconButton widget. Changed look: the trailing text of a list row and the icon beside it are onSurface instead of muted; the label line of a button and a badge is exactly the line height of its font in every script (Arabic buttons were a few dp taller). A 9.5.0 document read by 0.4.0 shows buttons and badges without icons at the default size and nothing in place of an IconButton.

9.4.0 — text templates, numbers and plurals#

Released 2026-10-09. A minor version: no existing document changed. A string can now hold several values, a number can be written with separators or as money, a date as “days left”, and a word can agree with a number.

  • Added: a third kind of string, kind: 'template' — a translatable string with named places ({done} / {total} steps) and a map of values. It is accepted wherever a string is text for a person (Text.content, labels, titles, subtitles, the text of a trailing slot); a string that identifies an option does not take it. See data dictionary.
  • Added: three values of binding format: number (group separators of the language, up to three fraction digits), currency (amount, a no-break space, an ISO 4217 code from the new currency field of the binding) and daysUntil (whole days until a date, rounded up, never below zero).
  • Added: a template value of kind plural — a word that agrees with a number by the CLDR plural categories of the language: { "kind": "plural", "of": "days", "forms": { "one": "day", "other": "days" } }. The forms are translated like any other string.
  • Added: thirteen policy keys, from unknownStringKind to readerClock, and the x-template block. Feature names: textTemplate, numberFormats. See compatibility.
  • Added: a shared table of cases, fixtures/template/template-cases.json (96 rows: input, data, language, clock → expected text), and the fixture fixtures/template-text-screens.json in English, Ukrainian and Arabic.
  • Changed in the builder: a text field has a third mode, Template; the Languages section translates templates and plural forms and warns about a placeholder that has no value and a plural form a language needs.
  • Validation: currency needs a code and a code needs currency (binding_currency_mismatch); a plural must agree with a number (template_plural_target) and needs a document that names its language (plural_needs_language).
  • Clarified before release: an empty string in the data is “no value” and shows the fallback; a date is only an ISO 8601 string with a time and a zone (x-template.dateTimePattern) — "1850" or 2026-10-14 is shown as is; a template value of an unknown kind is an empty place; when there is no data, a numeric fallback (0) picks the plural form; numbers above 2^53 − 1 are not formatted. Six more policy keys, from unknownTemplateValueKind to missingNumberLocaleData.
  • Not included, on purpose: percent, compact numbers (“1.2K”), hours and minutes until a moment, currency symbols.
  • Older renderer: a renderer of 9.3 shows an empty string for a template and the plain value for a number format (1850, an ISO date). Nothing fails, but the text is missing — update the renderers before you publish documents with templates.
  • React package: @hotlayout/react 0.6.0 draws templates, the three formats and plural forms, and has a new prop now — the clock daysUntil counts from. Pass the same moment on the server and in the browser, otherwise the two can count different days.
  • Flutter package: hotlayout_flutter 0.4.0 reads document contract 9.4.0 and data dictionary 1.2.0: sentences with named places, the formats number, currency (amount, no-break space, ISO code — 120 JOD) and daysUntil, plural words by CLDR category. New parameter now on renderDocument, renderScreen and ScreenFlow; new field LocalizedStrings.sourceLocale. Changed: a binding whose data is a map, a list or an empty string shows its fallback text (ChoiceGroup.value is not affected). A date is only a string of the contract's form — ISO 8601 with time and zone; date, dateTime, time and daysUntil show anything else as it is (a date without a zone used to be read as local time). number and currency do not apply beyond ±(2⁵³ − 1). A 9.4.0 document read by 0.3.0 shows template texts empty and numbers without separators.

9.3.0 — the icon library#

Released 2026-10-09. A minor version: the list of icon names grew, nothing else in a document changed. A 9.2 document is valid and looks the same.

  • Added: 322 icon names — IconToken now lists 332 (it had ten). They are accepted wherever an icon is: Icon.name, the leading icon of a ListTile, the icon in its trailing slot. The names are the names of Lucide icons (flame, calendar, credit-card, shopping-cart, …).
  • Added: the drawings are contract data now. A new artifact, icons.json, has a drawing for every name, with a version of its own (iconSetVersion, 1.0.0). Before, each renderer had its own pictures and the same screen looked different on the canvas and in an app. See compatibility.
  • Unchanged: the ten names that existed before (star, trophy, gift, target, clock, check, chevron, user, info, lock) keep the drawings the builder canvas always showed. They are in the same artifact.
  • Changed: x-mirroredIcons lists 29 names instead of one — the arrows and chevrons that point left or right, log-in, log-out, undo, redo, reply, forward, send, external-link, trending-up, trending-down, list, list-checks. See localization.
  • Added: two policy keys. unknownIcon: 'empty-box-same-size' — an icon name the renderer does not have is not drawn, and its box keeps its size; the default of the field (star) is not used. unreadableIconValue: 'as-unknown-icon' — an icon value that is not a string is treated the same way. Feature name: iconLibrary.
  • Added: the fixture fixtures/icon-library-screens.json — every icon of the set on one screen, and a section on direction to open in Arabic.
  • Changed in the builder: the icon field is a picker with a search (by name and by keyword, in English) instead of ten chips.
  • Older renderer: a renderer of 9.2 knows ten names. For any other name the web package leaves an empty box of the icon's size; the Flutter renderer of 9.2 draws a star. Nothing fails and the layout is intact, but the icon is missing or wrong — update the renderer before you publish documents that use the new names.
  • Licence: the drawings other than the ten are from Lucide (ISC; some derive from Feather, MIT). The notices are in THIRD-PARTY-NOTICES.md; keep them with the icon set when you ship it.
  • React package: @hotlayout/react 0.5.0 embeds icon set 1.0.0 and draws all 332 names; it adds about 60 KB of compiled JavaScript (18 KB gzipped) and ships THIRD-PARTY-NOTICES.md.
  • Flutter package: hotlayout_flutter 0.3.0 reads 9.3.0: it draws all 332 names from the built-in icon set (about 65 KB) and leaves an empty box for a name it does not have. In 0.3.0 the ten original icons change from Material Icons to the builder's drawings.

9.2.0 — which record a screen sees#

Released 2026-10-09. A minor version: the form of a document did not change, the contract gained statements.

  • Added: the rule for the most common flow — a list and a details screen. A row of a Repeater has a navigate action; the details screen binds to the same paths (item.name). Eight policy keys: actionRecord: 'node-record-scope' (every action carries the record scope of its node, and the host gets it with the action), navigateFromListItem: 'open-with-node-record', navigateOutsideListItem: 'page-without-record-modal-keeps-record', recordLifetime: 'while-screen-in-stack', dataLayerOrder: 'record-session-host-data', absentInDataLayer: 'read-next-layer', navigateParams: 'for-host-not-data', pickedChoice: 'shown-while-bound-value-unchanged'. See compatibility.
  • Added: x-dataScope in the artifact — the data layers of a screen as a list, strongest first: record, session, host, data.
  • Added: fixtures/scope/record-scope-cases.json — one table of cases for every renderer.
  • Changed in the builder: the preview and the prototype follow the rule; the binding panel says where a record field will come from ("from the list row that opened the screen").
  • React package: @hotlayout/react 0.4.0 gives the host source.record with every action.
  • Known limits: the record is a snapshot taken when the row was pressed; an entry point does not carry a record; a page opened by a button of a details screen has no record (a modal screen has).
  • Older renderer: a 9.1.0 renderer carries no record. A details screen opened from a list row shows the host's data or the fallback texts — the same for every row — and the host is not told which row was pressed. Nothing fails; the screen is about the wrong record.
  • Renderer: keep a record with every screen of the stack, read data through the four layers in order, and pass the record scope to the host with every action. Check the implementation against the shared table.
  • Backend: nothing changes in what you return. Give every record of a collection a field that identifies it (id): it is how the app tells you which record the user acted on. Treat that id as user input.
  • Documents: a 9.0.0 or 9.1.0 document is read unchanged, without migration.

9.1.0 — which language a device gets#

Released 2026-10-09. A minor version: the form of a document did not change, the contract gained statements.

  • Added: the rule that matches the locale of a device (ar-EG, pt_BR) to the languages of a document (ar, pt). Seven policy keys: localeNormalization: 'underscore-as-hyphen-case-insensitive', localeMatching: 'bcp47-lookup-truncate' (RFC 4647 Lookup: exact match, then without the last subtag, until none is left), localeChildFallback: 'none' (ar never picks ar-EG), unmatchedLocale: 'use-default-locale', localeResolution: 'once-per-document', directionFollows: 'shown-translation-locale', formattingLocale: 'requested-when-matched-else-default'. Feature name: localeLookup. The rule in full: localization.
  • Added: fixtures/locale/locale-cases.json — one table of cases for every renderer.
  • Added: x-style.layout.iconGlyphBox (inline: 'typography-font-size', block: 'typography-line-height') — the box of an icon as data. The rule itself dates from 9.0.0; only the key is new.
  • Changed in the builder: the Languages section warns about a regional translation without its base language (ar-EG without ar).
  • Known limit: zh-TW falls to zh; see the localization page.
  • Older renderer: a 9.0.0 renderer compares the device locale with the document's tags literally. For ar-EG it shows the source language, left to right. Nothing fails and nothing is missing; the screen is in the wrong language.
  • Renderer: resolve the language once per document with the rule above and take translations, date formats and the direction from that one answer. Check the implementation against the shared table.
  • Documents: a 9.0.0 document is read unchanged, without migration.

9.0.0 — one style for every node#

Released 2026-10-08, together with theme 4.0.0. A major version.

  • Changed: the look of a node lives in node.style and nowhere else. The same fifteen fields on every widget: color, typography, padding, margin, width, height, minWidth, maxWidth, minHeight, maxHeight, radius, border, background, shadow, opacity. The fields that described the look left props; see the table below.
  • Changed: a style field accepts a token or a value of the document's own. A color is a token, a tint of a token ({ token, opacity }) or a value ({ value, opacity? }). Spacing and radius are a token or a number. A text style is a token or a token with edits ({ token, fontSize?, fontWeight?, lineHeight?, letterSpacing? }). A gradient and a shadow are a token or a custom value whose colors are color references. The same forms apply to the part colors and gaps that stayed in props.
  • Changed: token names are no longer a closed list in the schema. A name is a string of a fixed shape; the theme declares which names exist. The x-tokens block names the shape and the required core.
  • Changed: style.background is { kind: 'color' | 'gradient' | 'image', … }; in 8.x it was a color token.
  • Added: color and typography are inherited down the tree; at the root of a screen they are onSurface and bodyM. Text reads both. The color is also read by Icon, ListTile, AppBar, Tab and Accordion, part by part (x-style.colorParts): main parts take the named color as it is, secondary parts (a subtitle, a chevron, an inactive tab title) take it at opacity 0.6 (x-style.secondaryOpacity). Until somebody names a color, the composite widgets keep their built-in colors. "Named" is a flag in the inheritance context, not a comparison with the root color. The leading icon of a ListTile is a main part: a plain glyph with no disc. Button and Badge read no inherited color; a text style is read only by Text.
  • Added: a Button or a Badge with a style of its own overrides its theme role, one slot at a time.
  • Added: Screen.props.gap — the gap between the children of the screen root (a spacing reference, default sm). Both renderers used to draw sm from their own code; now it is a field of the document. Remove the built-in gap.
  • Changed: boxes that used to be built into widgets are in the document now. ListTile and AppBar have no built-in vertical padding; Accordion has no built-in padding, radius, background or shadow — all of it is style (the builder writes the same values; migration writes them explicitly). Remove them from the renderer or they are drawn twice. Icon is a plain glyph of the size props.size, with no disc.
  • Added: the x-style block: how states merge, what is inherited and who reads it (colorParts, secondaryOpacity, unknownInheritedToken), the box model (radiusClips, opacityAppliesTo, fillOverridesSize, screenExcluded), widgets with a role. Feature names: openTokens, tokenOrValue, sharedNodeStyle. The x-gradientGeometry block is in both artifacts, and x-tokens.optionalInTheme lists the three state colors that are in the core but optional in a theme.
  • Added: policy keys, each one closing a place where two renderers would have disagreed silently: unknownStyleField: 'ignore-field', gradientStopOrder: 'non-descending', unorderedGradient: 'do-not-draw', invertedSizeRange: 'min-wins', unknownSideToken: 'side-is-zero', unknownBoxToken: 'omit-field', unknownInheritedToken: 'keep-parent', boundSizeOutOfRange: 'use-fallback', unknownBackgroundKind: 'omit-field', unknownWidgetStyle: 'ignore-style', hiddenInitialTab: 'first-visible', diagonalGradientGeometry: 'css-to-corner', literalOutOfRange: 'clamp', screenBoxFields: 'ignore', scrollPadding: 'inside-scroll', sizedBoxOverflow: 'clip', allLayersFilled: 'ignore-fill', scrollableRowChildWidth: 'row-viewport', sizedChildInTightSlot: 'start-top'. Each one is on the compatibility page.
  • Added (twelve places closed before the release): twelve more policy keys, each closing a place where the word existed but the thing it pointed to was not defined: sizeOutsideRange: 'clamp-to-range', malformedSideValue: 'side-is-zero', initialTabOutOfRange: 'first-visible', nonNumericLiteral: 'lower-bound', fillOutsideStack: 'ignore' in the widget schema; partialThemeDefaults: 'built-in-theme-of-same-brightness' and zeroScaleToken: 'none-is-zero' in the theme. New x-style keys: unknownTokenInCase: 'keep-base-field' (a state field the theme would not apply does not erase the base field), tabTitleColorContext, sizeIncludes (a size is the whole box with border and padding), boxTokenFields, fillScope, roles.neutral, roles.roleBorderAppliesTo, roles.withoutBorderSlot, and x-style.layout (lists, so that a word is not a request to invent a list: iconGlyphTypography, listTileIconSizes, scrollableRow, tightSlots, sizedBoxKeys). x-tokens.zeroScale: none in spacing, radius and elevation is always zero. Details on the compatibility page.
  • Added (five silences closed before the release): columnChildWidth: 'stretch-unless-natural' — a child of a column parent is stretched to the content width of the parent minus its own margin; only a node with its own style.width and the widgets in x-style.layout.column.naturalWidth keep a natural width, and they follow the align of the parent. Until now the contract was silent and the two renderers disagreed: the builder stretched, Flutter hugged the content. rowInUnboundedWidth: 'natural-width-ignore-weights' — a row without a finite width ignores the weights of its children. nullNumericField: 'absent', invalidWeight: 'no-weight', missingShadowLayerField: 'zero' — what "not a number" means and where a field is simply missing; nonNumericLiteral is confirmed for every bounded numeric field, in props too. protoKeyWithoutValidation: 'drop-entry' in both policies; incompleteThemeAppliesTo: 'reader-without-validation' in the theme policy. x-style.layout.badgePadding — the built-in padding of a badge is xs by sm.
  • Added (three more words before the release): naturalWidthInTightSlot: 'natural-start-top' — a Badge, a Button that is not full width, an Icon, a CircularProgress and a vertical Divider / Spacer in a Grid cell keep their natural width at the logical start and the top instead of stretching over the cell (x-style.layout.tightSlotNaturalWidth). justifyInNaturalRow: 'nothing-to-distribute' — a row that is as wide as its content has no free space, so its justify does nothing; a row that is a layer of a Stack is placed by the alignment of the stack. x-style.weightShareIncludes: ['margin'] — the share of a weighted child is its whole place in the row, its own margin included. x-style.layout.column.childWidthWithoutDefiniteParent: 'widest-natural' — a column that gets no width from its parent is as wide as its widest natural child. invalidWeight now also covers a fractional weight: 1.5 is no weight, it is not rounded.
  • Added (nine places found by comparing two renderers on one screen): unweightedRowChild: 'natural-shrink-proportionally' — a row child without a weight takes its natural width (the content on one line, no wider than the row); children that do not fit shrink in proportion to their natural widths, except a node with its own style.width and the widgets in x-style.layout.column.naturalWidth; shares divide what is left, and a share is zero when nothing is left. unweightedStretchInRow: 'share-of-one' — stretching content (x-style.layout.scrollableRow.stretchToViewport) without a weight and without a style.width takes what is left as a share of weight 1. Data: x-style.layout.row. The list of rows covered by rowInUnboundedWidth is wider: any row child without a weight, a child of a wrapping row, a row inside a column without a definite width. The built-in geometry of a Button is data now — x-style.layout.buttonPadding (sm × md), buttonTypography (titleS), buttonSize (label-plus-padding-plus-border): no minimum size, no invisible touch-target padding, and a border is added to the size. Theme policy: missingLetterSpacing: 'zero'.
  • Changed: scrollableRowChildWidth is max(row-viewport, own-width) and names its stretching content as a list; a tight slot is a Grid cell or a cell of a Repeater that has columns — a Repeater without columns is not one.
  • Changed: a theme and a document are stricter at the door: fontWeight is a multiple of 100 (multipleOf: 100); the key __proto__ is refused in themes and action.params (propertyNames.pattern); a tree deeper than 32 levels is refused before parsing (max_depth_exceeded). The builder's data dictionary and a state with an undeclared token behave as described above. A role border is 1 dp only for a role with a border slot on a known color; the builder writes --product-role-<group>-<role>-border-width for it.
  • Added: a diagonal gradient angle (45, 135, 225, 315) is the exact geometry of CSS to <corner>, not a line from corner to corner; fill overrides style.width and style.height; the corner radius clips the background and the content; opacity applies to the whole box with its content and shadow.
  • Removed: the rule "the node's style is stronger than the widget's props". The two no longer describe the same thing. A state (styleWhen) therefore no longer recolors a part of a widget — the fill of a progress bar, the line of a divider.
  • Renderer: read the look from style with one box model for every widget — margin, then the box (size, background, border, radius, shadow, opacity), then padding, then the content. Add no defaults of your own: a node without style has no background and no padding. Sides and corners are logical. A state is laid over the base style by top-level fields. Check that a token exists in the theme before you use it. The full rules are in the widget reference; what to do with an undeclared token is on the compatibility page.
  • Why major: a field that was a token string now also accepts an object, and fields moved between props and style. A renderer of 8.x would fail on the first, and draw a screen without backgrounds on the second.
  • Older renderer (8.x): does not draw 9.x documents; the major differs. A 9.x renderer does not draw 8.x documents either.

Fields that moved:

Widget 8.x 9.0.0
Screen props.background, props.padding style.background, style.padding. The only Screen prop is the new gap.
AppBar props.background style.background
Column, Row, Grid, Stack, Repeater, ChoiceGroup props.padding style.padding
Container props.padding, props.radius, props.background style.padding, style.radius, style.background
Container props.elevation style.shadow
Container props.border (a color token) style.border: { width: 1, color }
Container props.height style.height, a number or a binding
Text props.color, props.typography style.color, style.typography
Image props.radius style.radius
Icon props.color style.color
any node style.background: a color token style.background: { kind: 'color', color }

Before and after:

{ "type": "Container",
  "props": { "direction": "column", "gap": "sm", "padding": "lg", "radius": "lg", "elevation": "medium",
             "background": { "kind": "color", "color": "surface" } } }
{ "type": "Container",
  "props": { "direction": "column", "gap": "sm" },
  "style": { "padding": "lg", "radius": "lg", "shadow": "medium",
             "background": { "kind": "color", "color": "surface" } } }

Migration. You do not migrate documents. The builder migrates a draft when it is opened, and the document becomes 9.x when it is published again. A published 8.x document stays 8.x until then. The migrated document looks the same: defaults that 8.x renderers applied silently are written into style. The order of steps for an app in the field is in moving from 8 to 9.

8.20.0 — format of a bound value#

Released 2026-10-07.

  • Added: optional format on a text binding: date, dateTime or time. Two policy keys: unknownBindingFormat and unparsableBoundValue, both render-as-is. Feature name: bindingFormat.
  • Renderer: when the value is an ISO 8601 string, show it as a date in the language of the screen and the device's time zone; never format the fallback; do not format a binding that has no format. Details: localization.
  • Older renderer: skips the unknown key and shows the ISO string, as before.

8.19.0 — colors for element states#

Released 2026-09-15, together with theme 3.4.0.

  • Added: three color token names: locked, inProgress, completed. Feature name: elementStateColors.
  • Renderer: accept the names wherever a color is accepted. Their values are optional in a theme; see theme 3.4.0 below.
  • Older renderer: applies unknownToken: 'fallback-to-field-default'. The screen is whole; a locked element is not distinguished by color.

8.18.0 — a missing value is empty#

  • Added: policy key missingConditionValue: 'absent-is-empty'. Feature name: missingDataIsEmpty. No document changed by a byte; the meaning of a condition on missing data is now defined.
  • Renderer: when the value at a condition's path is absent, isEmpty holds and every other operator does not. Check for absence before you switch on the operator. unknownVisibilityOp stays stronger: an unknown operator leaves the node visible.
  • Older renderer: treats every condition on missing data as not met, so an empty-state block does not appear when there is no data.

8.17.0 — share of the row width#

  • Added: optional node field weight, an integer from 1 to 4. Feature name: rowWeights.
  • Renderer: children with a weight split the row in proportion; a child without one keeps its natural width and takes no share. A weight overrides the widget's own width. Legal only in a row of finite width: a Row without wrap and without scrollable, or a Container with direction: 'row'.
  • Older renderer: draws every child at its natural width; the proportion is lost.

8.16.0 — label inside the progress ring#

  • Added: CircularProgress.labelFormat: valueOfMax, percent or none. The default is none, unlike the progress bar, because the center of the ring belongs to the document.
  • Renderer: draw the label in the center when the value is not none.
  • Older renderer: draws the ring without a label.

8.15.0 — right-to-left scripts#

  • Added: the x-mirroredIcons block in the schema artifact. It currently lists chevron.
  • Renderer: take the writing direction from the language the document is shown in, using CLDR, not a list of your own; mirror the listed icon tokens in a right-to-left script. Details: localization.
  • Older renderer: draws every language left-to-right.

8.14.0 — a layer that fills the stack#

  • Added: optional node field fill. Feature name: stackFill.
  • Renderer: inside a Stack, a child with fill: true takes the size of the stack instead of its own. The field is legal only inside a Stack, and at least one layer must stay unfilled so that the stack has a size.
  • Older renderer: draws the layer at its own size. A layer without content of its own, such as a gradient band, is not visible at all.

8.13.0 — a grid from data#

  • Added: Repeater.columns (2–4). Feature name: repeaterGrid.
  • Renderer: lay the instances of the template out as a grid with that many columns, using the same cell-width rule as Grid. columns together with direction: 'row' is an invalid document.
  • Older renderer: draws the records as one column. Every record is there; the layout is lost.

8.12.0 — screen presentation#

  • Added: optional presentation on a screen entry: page or modal. No field means page. Feature name: modalScreens.
  • Renderer: draw a modal screen as a layer over the previous screen, which stays visible underneath. The contract does not fix the size of the layer. The first screen shown cannot be modal; if a broken document says so, draw it as a page.
  • Older renderer: opens the modal as an ordinary screen. Content and "back" work; the context underneath is lost.

8.11.0 — five properties of existing widgets#

  • Added: Row.scrollable, Container.border, Container.height, Image.fit (cover or contain), and an optional icon in the trailing slot of ListTile. Every default equals the previous behavior. Feature names: rowScroll, containerFrame, imageFit.
  • Renderer: a scrollable row scrolls sideways and its children keep their natural width; if scrollable and wrap are both set, scroll. A given height is exact, not a minimum.
  • Older renderer: draws a plain row that clips what does not fit — the only loss in this version that costs content; a card has no border and takes the height of its content; an image is drawn with cover.

8.10.0 — named entry points#

  • Added: optional document field entryPoints, a map from a public name to a screenId. Policy key unknownEntryPoint: 'use-initial'. Feature name: entryPoints.
  • Renderer: let the host open a document by entry name; open the initial screen for a name the document does not have.
  • Older renderer: always opens the initial screen.

8.9.0 — node states#

  • Added: optional node field styleWhen, a list of "condition → style". Feature name: nodeStyleCases.
  • Renderer: apply the first case whose condition holds, laid over the base style; with no matching case use the base style. Evaluate at render time, because the data can change without the document being re-read. The condition has the same form as visibleIf.
  • Older renderer: draws the node with its base style; states are not distinguished.

8.8.0 — tabs#

  • Added: widgets Tabs and Tab.
  • Renderer: draw one panel at a time under a shared strip of titles. The title is on each Tab. Tab is the only widget with exactly one legal parent.
  • Older renderer: applies unknownWidget: 'render-children'.

8.7.0 — progress ring#

  • Added: widget CircularProgress.
  • Renderer: the same scale as the progress bar, drawn as an arc. Diameter and stroke width derive from theme values, not from fixed pixels.
  • Older renderer: applies unknownWidget: 'render-children'; the ring has no children, so nothing is drawn in its place.

8.6.0 — stack#

  • Added: widget Stack.
  • Renderer: draw the children as layers at one point; the first child is the lowest layer.
  • Older renderer: applies unknownWidget: 'render-children'.

8.5.0 — grid#

  • Added: widget Grid.
  • Renderer: put exactly columns children in a row (2–4) at equal widths, on any screen width. This is not Row with wrap, which fits as many as the natural widths allow.
  • Older renderer: applies unknownWidget: 'render-children'.

8.4.0 — lists from data#

  • Added: widget Repeater and collections in the data dictionary.
  • Renderer: draw the one template once per record of the collection named by source. Inside the template, the record is visible under its item prefix (tasks → task.title) and is laid over the outer data, not instead of it. Draw at most 50 records.
  • Older renderer: applies unknownWidget: 'render-children'.

8.3.0 — style override on a node#

  • Added: optional node field style with color, typography and background. Each refers to a token, never to a value. Feature name: nodeStyle.
  • Renderer: the node's style is stronger than the widget's own props. A field the widget does not have is ignored.
  • Older renderer: draws the node by the theme, as before.

8.2.0 — image source from data#

  • Added: a binding branch of ImageSource: { kind: 'binding', path, fallback }. The fallback is an asset token, not an address. Feature name: boundImageSource.
  • Renderer: take the image address from data at path; draw the fallback asset when there is no value.
  • Older renderer: sees an unknown kind in the image source.

8.1.0 — container direction#

  • Added: Container.direction: column or row. The default is column. Feature name: containerDirection.
  • Renderer: stack the container's children in the given direction.
  • Older renderer: draws a column, as a container always did.

8.0.0 — text without a length limit#

A major version. No widget was added and no field changed shape.

  • Changed: text meant for people lost its maximum length; the length-limited text types used for it were merged into BindableString, which has no maximum. Policy key overlongText: 'render-as-is'. Feature name: unboundedText.
  • Renderer: draw text of any length and let the widget's own overflow properties (Text.maxLines, Text.overflow) decide. Do not truncate by a number of your own and do not reject the document.
  • Why major: compatibility is one-way. A 7.x document is valid in 8.0.0, but a renderer of 7.x would reject an 8.0.0 document with a long title as a whole.
  • Older renderer (7.x): does not draw 8.x documents; the major differs.

Theme contract#

4.0.1 — label of the secondary and ghost button in the built-in dark theme#

Released 2026-10-09. A patch.

  • Changed: in the built-in theme dark, the roles buttonVariant.secondary and buttonVariant.ghost name onSurface as foreground; it was primary. The label was #5b55ea on #111827, 3.32:1, below the 4.5:1 that WCAG AA asks of text; it is now 16.1:1. The border of secondary is still primary. The built-in theme light is unchanged.
  • Why not a lighter primary: the same color is the background of the primary button under a white label. No single value reads as text on the dark surface and also carries white text.
  • Not changed: the structure of a theme, the dictionaries, the policy. A theme of your own is not affected: its roles say what you wrote in them.
  • Renderer: nothing, except refreshing a bundled copy of the theme. A renderer of 4.0.0 reads 4.0.1 unchanged.

4.0.0 — the theme declares its own tokens#

Released 2026-10-08, together with schema 9.0.0. A major version.

  • Changed: six dictionaries are open: color, gradient, spacing, radius, elevation, typography. Each has a required core — the names that existed in 3.4.1 — and may hold up to 64 names of your own.
  • Changed: a color is #rrggbb or #rrggbbaa, the alpha last. Policy key colorAlpha: 'rrggbbaa-alpha-last'.
  • Changed: a gradient angle may also be 45, 135, 225 or 315. These are the exact geometry of CSS to <corner>, not CSS degrees and not a line from corner to corner. gradientAngleSpace is now axis-as-css-diagonal-css-to-corner (it was axis-aligned-only), plus diagonalGradientGeometry: 'css-to-corner'; the formula is the x-gradientGeometry block.
  • Changed: a role may name any color of the same theme, including one of your own. Policy key unknownRoleColor: 'neutral-slot': only the slot with the undeclared color becomes neutral, not the whole role.
  • Changed: a shadow layer has limits — offsets and spread within ±64, blur within 0–128, at most four layers.
  • Added: policy keys customTokens: 'declared-by-theme', missingCustomToken: 'treat-as-unknown-token', gradientStopOrder: 'non-descending' (equal positions are in order) and unorderedGradient: 'do-not-draw'. The x-rules block: seven rules the embedded schema cannot express.
  • Removed: policy key unknownToken: 'ignore'. A name outside the core is normal now.
  • Renderer: read the six dictionaries as open maps and keep no list of names of your own. Read #rrggbbaa with the alpha last. Draw the diagonal angles as CSS to <corner>. Details: theme.
  • Why major: a renderer of 3.x validates a theme against a closed list of names and rejects a theme with one color of your own as a whole. No value of the built-in themes changed.
  • Older renderer (3.x): does not read a 4.x theme; it draws with its fallback theme and reports it.

3.4.1 — text of the source field#

Released 2026-10-08. A patch.

  • Changed: the source text of the two built-in themes. Structure and token values are unchanged.
  • Renderer: nothing, except refreshing a bundled copy of the theme. A renderer of 3.4.0 reads 3.4.1 unchanged.

3.4.0 — letter spacing and state colors#

Released 2026-09-15, together with schema 8.19.0.

  • Added: optional letterSpacing on a text style, in logical pixels, from −2 to 8. Optional colors locked, inProgress, completed. The x-optionalColors block with their fallback tokens.
  • Renderer: pass letterSpacing through unchanged; absent means zero. When a state color is missing, use the documented fallback — locked → muted, inProgress → accent, completed → success — and do not invent another. Do not convert letter spacing to em.
  • Older renderer: ignores the new field and names. A theme without the three colors looks exactly as it did.

3.3.0 — a second theme#

  • Added: a dark theme in the built-in set. The contract allowed several themes from the start; this is the first set with more than one.
  • Renderer: nothing new. Take the theme by defaultThemeId, or let the host choose one by brightness.

Data dictionary#

1.7.0 — a task key and completed tasks#

Released 2026-10-17. A minor version: a path and a collection were added at the end of the standard dictionary, and the samples of the task collections got a key; nothing else changed. They are the sample data for the live record of schema 9.27.0.

  • Added — the key of a task: task.id (text).
  • Added — the collection doneTasks (record prefix task): completed tasks; its first record is the first task of tasks after it is done.
  • Renderer, backend: nothing for a renderer. A backend that wants the record of a page to stay live gives every record an id that does not change when the record moves from one collection to another.

1.6.0 — a history of activities#

Released 2026-10-16. A minor version: paths and a collection were added at the end of the standard dictionary; nothing else changed. They are the sample data for the groups of schema 9.26.0.

  • Added — a record of a history: activity.title (text), activity.points (a number), activity.day (text the backend composes — "Today", "Yesterday", "Wed, Oct 14").
  • Added — the collection activities (record prefix activity), records in the order of the backend, newest first.
  • Renderer, backend: nothing for a renderer. This is the standard, demonstration dictionary; a project dictionary of your own is not touched.

1.5.0 — amounts, the last operation, paths that actions write#

Released 2026-10-12. A minor version: paths were added at the end of the standard dictionary; nothing else changed. They are the sample data for what schema 9.19.0 added.

  • Added — amounts with a fraction: account.balance, account.income, account.spent (negative), account.limit — numbers. A path carries no currency: the code stands in the document, beside the format.
  • Added — the last operation: account.lastIcon (the name of an icon, for iconPath), account.lastStatus, account.lastMerchant (text), account.lastAt (a date).
  • Added — paths written by setValue and toggleValue: account.balanceHidden (yes/no), account.period (text), survey.score (a number), survey.topicTravel, survey.topicFood, survey.topicBooks (yes/no — several topics chosen at once are several flags; a dictionary has no set under one path).
  • Renderer, backend: nothing for a renderer. This is the standard, demonstration dictionary; a project dictionary of your own is not touched.

1.4.0 — the paths of a survey#

Released 2026-10-10. A minor version: five paths were added at the end of the standard dictionary; nothing else changed.

  • Added: survey.goal, survey.name, survey.comment (text), survey.age (a number — the path a number field of schema 9.14.0 writes to), survey.agreed (yes/no).
  • Samples: every path has a non-empty sample, so with the sample data a form bound to these paths is already filled. The builder shows the empty form in the preview state "No data".
  • Renderer, backend: nothing for a renderer. A backend gets a number under a number path — or the empty string when the person cleared the field or typed something that is not a number.

1.3.0 — paths a screen writes to#

Released 2026-10-10. A minor version: eight paths were added at the end of the standard dictionary; nothing else changed.

  • Added: form.fullName, form.email, form.phone, form.comment (text), form.phoneError (text — an error message the host puts there after checking the phone), form.agreed, settings.notifications, settings.marketing (yes/no). They are the paths the input widgets of schema 9.13.0 write to in the fixtures and in a new document.
  • Your own dictionary: an input can be bound only to a path of exactly its type. If the screens of your project ask the person for something, add a path for every answer — and a text path for every error message you want to show under a field.
  • Renderer, backend: nothing for a renderer. A backend that receives the writes gets a string for a text path and a boolean for a yes/no path, under the names of these paths.

1.2.0 — a price and the moment of the samples#

Released 2026-10-09. A minor version: a path and a key were added.

  • Added: the path offer.price (number, sample 120) — an amount of money, shown with the currency format of schema 9.4.0. The dictionary does not name the currency: the code is a literal in the document.
  • Added: an optional key of the artifact, x-sampleNow — the moment the samples are imagined at (2026-10-10T09:00:00.000Z in the standard dictionary). Previews and tests that draw the samples take it as “now”, so “days left” does not depend on the day they run. It promises nothing to a backend. A project dictionary keeps the key if it has one; the editor in the admin does not show it yet.

1.1.2 — a sharper sample image#

Released 2026-10-09.

  • Changed: the sampleValue of item.image and of the items collection records is now a WebP picture of 720 px (data:image/webp;base64,…, 7–9 KB each) instead of a PNG of 480 px, which looked grainy when stretched across a card. Paths, types and entities are unchanged.
  • Renderer, backend: nothing, but a renderer that draws these samples must accept a data:image/webp address, not only data:image/png. The backend sends real values.

1.1.1 — a real sample image#

Released 2026-10-09.

  • Changed: the sampleValue of item.image is now a small real picture instead of a flat color tile. Paths, types and entities are unchanged.
  • Renderer, backend: nothing. Sample values are illustrations for the builder; the backend sends real values.

1.1.0 — an absent collection is empty#

Released 2026-10-08.

  • Changed: x-delivery.absentCollection is now empty (it was not-empty). The artifact now says the same as the widget schema has said since 8.18.0 (missingConditionValue: 'absent-is-empty').
  • Renderer: nothing. Renderers have behaved this way since 8.18.0; the statement changed, not the behavior.
  • Backend: a collection you do not send is shown as an empty list. Sending [] is still recommended.