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
httpsaddress of theopenUrlform: a host name with a letter top-level label, a port 1…65535, no IP literal in any notation, nolocalhost, no user info. The host may replace the decision (resolveImageUrlin React);javascript:andvbscript: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 noReferer, 30 seconds per picture, the longer side decoded at no more than 4096 pixels; on the web pictures load lazily. Rules:x-imageSource; tablefixtures/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; tablefixtures/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; tablefixtures/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; tablefixtures/accessible-name/silent-text-cases.json. - Focus of a modal layer that cannot be dismissed: on the web
Tabmay leave the renderer at either end; a full trap only when the host asks (trapFocusin React). Rules:x-modalFocus. - Older renderer: may load pictures from IP addresses and
localhostwith cookies andReferer, draw every instance of nested lists, draw a theme number as it is, announce transparent text, and keepTabinside 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
carryRecordpage 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 sameRepeater.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 anid(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 withanimateChangescount to the new number. Rules:x-liveRecord. - Data, not words: the block
x-liveRecordat the root ofscreen-schema.json; new groupsentitySourceCases,liveRecordCasesandliveFlowCasesinfixtures/scope/record-scope-cases.json; a screen fixturefixtures/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.
longPressnext toactionon a pressableColumn,Row,Container,Stackand onListTile— 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 ofactionthen 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 theRepeateris drawn before each group in the scope of its first record. Groups are formed afterwhereandlimit, 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: falseon 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-repeaterGroupandx-sheetDismissat the root ofscreen-schema.json; new tablesfixtures/long-press/long-press-cases.json,fixtures/repeater-group/repeater-group-cases.json,fixtures/sheet-dismiss/sheet-dismiss-cases.json; a screen fixturefixtures/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: falseis 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
ProgressBarwithsegmentsruns 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'onProgressBarandCircularProgress: 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 whencurrentColorpaints 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 ofstyle.shadowtakes 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.
hapticnext totrackon 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-pressFeedbackandx-hapticat the root ofscreen-schema.json; new groups infixtures/progress/progress-cases.jsonandfixtures/actions/action-cases.json, new tablesfixtures/shadow-color/shadow-color-cases.jsonandfixtures/press-feedback/press-feedback-cases.json; a screen fixturefixtures/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: trueonText,ProgressBarandCircularProgress: 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 ofanimateChanges. 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
noneand an invalid action return at once. The blockx-swipeConfirmof 9.22 is unchanged;x-swipeConfirmBusyrefines it. - A dot that flashes. The dot of
badgeWhenappears 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 fieldpin: 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 withbodyTop: 'screenTop'. - Data, not words: the blocks
x-valueAppearandx-swipeConfirmBusyat the root andx-style.layout.navBadgeMotion,x-style.layout.topZoneinscreen-schema.json; new groups infixtures/value-transition/value-transition-cases.json,fixtures/swipe-confirm/swipe-confirm-cases.jsonandfixtures/bottom-zone/bottom-zone-cases.json, a new tablefixtures/top-zone/top-zone-cases.json; a screen fixturefixtures/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.
whereonRepeater— a condition of the same form asvisibleIf, 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, andlimittakes 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:valuePathinstead ofvalue, with the same comparison operators. With no value under that path (or the empty string) awhereholds 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/isNotEmptyon a collection path may carryitemand the samewhere: then only the records that pass are counted. - A gradient on text.
style.textGradientonTextpaints 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;colorstays the fallback. - A compact number.
notation: 'compact'besideformat: '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-textGradientandx-compactNumberat the root ofscreen-schema.json; a new tablefixtures/repeater-filter/repeater-filter-cases.json, new groups infixtures/inputs/input-cases.json,fixtures/template/template-cases.jsonandfixtures/rich-text/rich-text-cases.json; a screen fixturefixtures/filter-gradient-compact-screens.json. - Older renderer: every record of a list is drawn and counted, a comparison by
valuePathhas 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: trueonNavBar: 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.
badgeWhenonNavItem— a condition of the same form asvisibleIf. While it holds, the icon of the item carries a dot of the core tokenaccent, 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'onButton: a track with a thumb; the action (and itstrackevent) 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-swipeConfirmat the root andx-style.layout.navFloat,x-style.layout.navBadge,x-style.layout.borderDashinscreen-schema.json; new groups infixtures/bottom-zone/bottom-zone-cases.json, new tablesfixtures/swipe-confirm/swipe-confirm-cases.jsonandfixtures/border-dash/border-dash-cases.json; a screen fixturefixtures/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: trueonText,ProgressBarandCircularProgress: 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
countdownParton a date path, withunit(days,hours0–23,minutes0–59,seconds0–59) and an optionalpad: 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.
styleScrolledon 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-valueTransitionandx-countdownat the root andx-style.layout.progressCap,x-style.layout.appBarScrolledinscreen-schema.json; a shared tablefixtures/value-transition/value-transition-cases.jsonand new groups intemplate-cases.json,progress-cases.json,media-chrome-cases.json; a screen fixturefixtures/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.
motionon any node except the rootScreen:float(the node drifts alongaxis—vertical,horizontalorboth— by up toamplitudelogical pixels) orpulse(it breathes inscaleand, optionally,opacity). One cycle lastsdurationms, the first starts afterdelayms. 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-motionandx-backdropBlurat the root ofscreen-schema.json; a shared tablefixtures/motion/motion-cases.json; a screen fixturefixtures/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,RowandContaineraccept a child of their own type, and a tree can be eight levels deep.Column,Row,ContainerandStackcan carry anactionand a hiddenlabel: 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:
setValueandtoggleValue. They write a literal of the document under a data path — into the renderer's session and to the host'sonWrite— and never reachonAction. Nodes of one parent that write the same path are one group of options, so a choice can be built from your own cards. ConditionsisEmpty/isNotEmptyare 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
altof an image and of an icon can be a string of the document — a binding or a template, translated like any text.iconPathtakes the name of an icon from data (only a name of the built-in set is drawn).datePartsbesideformat: 'date'chooses the parts of a date:dayMonth,weekdayDayMonth,dayMonthYear.unsetin a width layer (styleOn/propsOn) removes a field on a wider class. - The value of
onWritenow 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-conditionsat the root ofscreen-schema.json;x-actions.write; new groups of rows in the shared tables underfixtures/. - Older renderer: does not read the new fields. A container with an
actionis not pressable; a currency is written with its code after the amount; the body starts under the app bar;setValueandtoggleValuereach the host as an unknown action and write nothing; a list row and a text field have their built-in look. - Renderers:
@hotlayout/react0.18 andhotlayout_flutter0.19 draw all of it. New for a React host: the propsafeAreaTop.hotlayout_flutterhas no list of allowed write paths — the host sees every write inonWrite.
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:
compactbelow 600 logical pixels,mediumfrom 600,expandedfrom 1024. The boundaries are data of the contract. - Added — fields of a node:
showOn(the classes a node is shown on, independent ofvisibleIf),styleOn: { medium?, expanded? }(the same fields asstyle, laid over it from narrow to wide; a state ofstyleWhenis still stronger) andpropsOn: { 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
mediumandexpandedthe 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 tablefixtures/adaptive/adaptive-cases.json; a screen fixturefixtures/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/react0.16;hotlayout_flutter0.16 (classes, overrides, the content column) and 0.17 (navigation and windows on a wide screen). New for a React host: the propinitialWidthClass, 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) andstyle.textDecoration(none/underline/lineThrough) on a node; the same two andweight(regular/medium/semibold/bold) on a part of a text (Text.spans[place]). They are inherited like color and text style, and read byTextonly.normalandnoneare 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.labelTypographyandlabelColor. 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 infixtures/rich-text/rich-text-cases.jsonandfixtures/progress/progress-cases.json, and a tablefixtures/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/react0.14,hotlayout_flutter0.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 aButtonor anIconButtonthat is a child of anAppBar. The node stands in the leading zone instead of the back arrow, whatevershowBacksays, 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 toshowBack. It is at most half of the bar wide; the title shrinks first, the actions never. - Added —
AppBar.titleAlign(start/center):startputs the title right after the leading zone. - Added —
glyphSize(a whole number 8…128) onIconandIconButton: the side of the drawing. It is stronger than the wordsizeand is not multiplied by the system font scale. The wordsizestays 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 infixtures/media-chrome/media-chrome-cases.json; a screen fixturefixtures/app-bar-screens.json. - Older renderer: does not read
zone. It draws the back arrow byshowBackand takes the first two icon buttons as actions; the builder puts the leading node last among the children and keepsshowBack, 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/react0.14,hotlayout_flutter0.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.alignandjustify,Screen.alignandjustify— the same words as inRow. 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.heightorstyle.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:
alignis the vertical axis. gapstays the smallest distance between children;betweenwith one child isstart.- Data, not words:
x-style.layout.alignment; a shared tablefixtures/align/align-cases.json; a screen fixturefixtures/alignment-screens.json. - Older renderer: places the children at the start of both axes, as before.
- Renderers:
@hotlayout/react0.14,hotlayout_flutter0.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": trueonTextField,CheckboxandChoiceGroup. 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. ASwitchhas 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": trueonButtonandIconButton. 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 yourdataalready holds, both count. - Added —
disabledIfon a button: the same single condition asvisibleIf(and asdisabledIfof the inputs of 9.13). WithrequiresFilled, 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
onActionis not called and notrackevent is sent. - Added — a number field:
"valueType": "number"onTextField. It takes a path of the type number, opens the numeric keyboard and writes a number to youronWrite. A comma and a dot are both the decimal separator; Arabic-Indic digits are read; there are no thousands separators and no exponent —1,200is one point two,1 200is 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
onWriteis nowstring | 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;
requiredonly 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 theerrortext of a field. - A choice writes like an input: a
ChoiceGroupwith a bound value writes the chosen option to the session of the renderer as well as toonWrite, so conditions and buttons of the same screen react at once. A requiredChoiceGroupmust have a bound value — the builder refuses a required group with a literal one. - Action parameters, corrected: a value read by
paramBindingsbecomes a parameter only when it is a non-empty string. A boolean or a number (of a number field too) does not; the literal fromparamsacts. 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) inscreen-schema.json; fourteen policy rows fromunreadableRequired; three feature names; twelve new groups (300 rows) infixtures/inputs/input-cases.json; a screen fixturefixtures/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/react0.13.0 draws all of it (onWritenow carriesstring | boolean | number— a TypeScript change for the host; no new props).hotlayout_flutter0.14 draws it too; itsonWritemay give anum.
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 aChoiceGroupuses (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 novalueis 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.
onWriteused to carry strings only; widen the type. The paths are named by the document, so keepwritablePaths(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 asvisibleIfon each of the three widgets. A disabled input takes no focus and writes nothing.- The keyboard, named:
keyboardis 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).numberis a keyboard, not a type: the value stays a string. There is nopassword, 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
writablePathsas 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
checkdrawing 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. Thestyleof the node is the box of the whole widget;style.colorandstyle.typographyare not read. - Limits, named:
maxLengthis counted in Unicode code points and the renderer cuts the input itself;linesis 1 to 8. - Where an input may stand: the screen root,
Column,Row,Container,Tab. Not inside aRepeater: every row would write to the same path. - Data, not words:
x-style.layout.inputs(rules,textField,switch,checkbox) inscreen-schema.json; twenty policy rows frominputWithoutValue; three feature names; shared cases infixtures/inputs/input-cases.json(248 rows); the fixturefixtures/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/react0.12.0 draws all of it.onWritenow carriesstring | boolean(a TypeScript change for the host), and there is a new propautofill(defaulttrue;falseturns 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_flutter0.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, andNavItem, its item (icon,label,action). ANavBaris the last child of a screen root (the mirror ofAppBar, 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 tofill,weight,layerandpull. Read on exactly one node: the last child of the screen root, not counting theNavBar. 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.navigateis unchanged: it still puts a screen on top. Pressing the item of the screen you are on does nothing. - Added: the cause
switchof 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
surfacecolour with a one-pixel top line ofborder; the active item isprimary, the restmuted.style.backgroundandstyle.borderof 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.bottomZoneandx-style.layout.navBarinscreen-schema.json; two values changed inx-style.layout.screen(bottom,body.area); sixteen policy rows fromnavBarNotLast; four feature names; shared cases infixtures/bottom-zone/bottom-zone-cases.jsonand eleven flow rows named9.12 sections: …infixtures/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 theNavBarat all (an unknown widget): the user sees the first section and has no way to the others.navigate.sectionis 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/react0.11.0 draws all of it, with two new props of the host —safeAreaBottom(number | 'env', the inset under the bottom zone) andstickyBottomOffset; withscroll="page"the bottom zone sticks to the bottom of the viewport.hotlayout_flutter0.12 draws it; the inset comes from theMediaQueryof its own context.
9.11.0 — a carousel and a stepped progress bar#
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 thepropsof aRow(only withscrollable: true) and of aRepeater(only withdirection: "row", withoutcolumns). 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;lgwhen absent) andindicator(dotsornone). 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. WithperViewthe card's ownwidth,minWidthandmaxWidthare 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
carouselaRepeaterlaid out in a row is what it was. - Added:
segments(2–12) andcurrentColor— optional fields ofProgressBar. The same scale cut into equal steps with a gap of thexstoken: a step is filled whole or not at all (floor(ratio × segments)), the first unfilled step is the current one and takescurrentColorwhen 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 withweight: 1and the same gap. - Data, not words:
x-style.layout.carouselandx-style.layout.progressSegmentsinscreen-schema.json; ten policy rows fromcarouselOutsideHost; shared cases infixtures/carousel/carousel-cases.jsonand three new groups infixtures/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
Rowscrolls without snapping, the children keep their own width, there are no dots. ARepeaterlaid 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/react0.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_flutter0.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, besidefillandweight. A direct child of aStackwith 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": … };byis a spacing token or 0–128 logical pixels) androtate(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 ispullminus the gap. Legal in aScreen, aColumn, aContainerand aRowwithout wrapping and without scrolling; not together withweight. The later child is drawn over the earlier one and takes the taps in the overlap. - Data, not words:
x-style.layout.layerandx-style.layout.pullinscreen-schema.json; ten policy rows fromlayerOutsideStack; shared cases infixtures/offset/offset-cases.json; a screen to look at —fixtures/offset-screens.json. - Clarified before release (2026-10-10):
pullon anAppBaris 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;pullinside 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/react0.10.0 drawslayer(the overlay does not size the stack, shifted and rotated, mirrored in right-to-left) andpull(a negative margin on the outer element of the node's place).hotlayout_flutter0.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.aspectRatioalso 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.widthandstyle.heightgive the size; a named height is stronger than the proportion — a 56 × 56 image withaspectRatio: "16:9"is 56 × 56. The proportion only gives the height from the width, never the width from the height.minHeight/maxHeightclamp the height either way. The natural size of the file is never read. - A circle is not a new word. It is equal
style.widthandstyle.heightwith a radius of half the side. Thepilltoken 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
AppBartakes up to two children, and they areIconButtonnodes: an icon, an accessible name, an action withtrackandparamBindings,visibleIfand 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.colorof the app bar colors the title and the back arrow; an action takes the color of its own role or its ownstyle.color. - Added:
ScreenEntry.modalPlacement— where a modal layer lies:bottom(a sheet, also without the field) orcenter(a window in the middle). It is a field besidepresentation: "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 ofxlon 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 allnavigate.back. - Changed reading, named: a modal layer has no built-in look. Its background, radius and shadow are the
styleof the screen root, as for a page. The React package used to put asurfacebackground under the layer. Feature name:modalLayerLook. - Added:
ScreenEntry.statusBarIcons—lightordark: 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,
modalPlacementandstatusBarIcons(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", nomodalPlacement), 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 noposition: 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 withscroll="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 infixtures/scope/record-scope-cases.json, and the fixturefixtures/media-chrome-screens.jsonin 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/react0.10.0 implements all of it, with three new props of the host —scroll('page'/'screen'),chrome('sticky'/'static') andstickyOffset— 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 inonScreenChange. 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-actionsin the artifact. Six named actions:none,navigate,navigate.back(performed by the renderer) andopenUrl,deeplink,close(performed by your app).typeis 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,deeplinkandcloseare handed to your action handler like any other action — parsed and checked:standardwhen 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(lowercasehttps://, 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) andx-actions.forms.appLink(an app scheme;forbiddenSchemeslists the schemes that are never an app link —javascript,data,file,intent,http,httpsand 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 fromparams(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 isnonewithtrack. Your app gets it once per press, before the action, with the same node, screen and record. - Added:
action.carryRecord: trueonnavigate— 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-screenShownin 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(anavigate, a modal sheet included),back(the screen beneath is shown again). There is no "hidden" event. It carriesdocumentId,screenId,entry,presentation,cause,depthandrecord. - Changed: a value of
action.paramsmay be up to 2048 characters long (it was 128): an address did not fit. A key and a value ofparamsmust have the schema form (a string value; a key that is not empty, has no spaces and is not__proto__,constructororprototype); a renderer skips an entry that does not. - Stricter in the builder, named: a document with
openUrlordeeplinkthat 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 withcarryRecordinfixtures/scope/record-scope-cases.json, and the fixturefixtures/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,deeplinkandcloseto the host as unknown actions, unchecked; it does not readparamBindings(the host gets the literal),track(no event) orcarryRecord(the page has no record). - React package:
@hotlayout/react0.9.0 implements all of it.onActiongetsstandard/invalid;onTrack(event, source)is new;onScreenChangenow carries the screen-shown event (documentId,entry,cause,depth,recordbeside the oldscreenId,name,presentation);runStandardAction(action, { openUrl, deeplinkSchemes, openDeeplink, close })is the optional helper, andopenInNewTaba readyopenUrl. The renderer itself opens nothing. - Flutter package:
hotlayout_flutter0.8.0 reads document contract 9.8.0: a standard action reaches the host parsed and checked (ButtonAction.standard/invalid),runStandardActionruns it with the executors the host gives,onTrackdelivers the analytics event,onScreenShownreports each shown screen (onScreenChangedis deprecated), andcarryRecordkeeps 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
ProgressBarandCircularProgress:thickness(a spacing token or a whole number of dp),trackColor(a color reference) andgradient(a gradient token of the theme or an own gradient). Without a field the scale is drawn as before: thicknesssm, tracksecondary, a solidcolorfill. See widgets. - Added: the size of a ring comes from the box of its node.
style.widthandstyle.height(and theirmin…/max…limits) on aCircularProgressname the diameter: the smaller named side of the content box. There is no separate "diameter" field. Without them the wordsizegives 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
CircularProgressthat already hadstyle.widthorstyle.heightused to be a ring of thesizeword 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 isunorderedGradient(not drawn, the fill is the solidcolor). 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 fixturefixtures/progress-screens.jsonin 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.widthat the size of thesizeword. - React package:
@hotlayout/react0.8.0 draws it all (thickness, track, gradient along the filled part, ring diameter from the box, arc inside the circle). - Flutter package:
hotlayout_flutter0.7.0 reads document contract 9.7.0:thickness,trackColorandgradienton 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 aStackplaced in aRowtakes 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 ofcontentto a style (color,typography). There is no new widget: rich text is aTextwhosecontentis 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 fixturefixtures/rich-text-screens.jsonin 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
spansand 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 kindtexteither: its place is empty (the rest of the sentence stays), so update the renderers before you publish documents with such words. - React package:
@hotlayout/react0.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_flutter0.6.0 reads document contract 9.6.0:Text.spansand 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.iconandButton.iconPosition(startorend, in reading direction), andButton.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) andx-style.layout.iconBesideText.defaultPosition(start). - Added:
Badge.iconandBadge.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 separateChipwidget, 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) andx-style.layout.iconBesideText— one rule for an icon beside a text. See compatibility. - Changed interpretation — the trailing slot of a list row. A
ListTilewhose 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 isonSurfaceunless 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 fixturefixtures/buttons-chips-screens.jsonin English, Ukrainian and Arabic. - Validation: an
IconButtonwithout 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,:60are 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/react0.7.0 draws button sizes, icons in buttons and badges andIconButton(a real<button>named by itslabel). 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_flutter0.5.0 reads document contract 9.5.0. New:Button.size(sm/md/lg),Button.iconandBadge.iconwithiconPosition(start/end, in reading order), theIconButtonwidget. Changed look: the trailing text of a list row and the icon beside it areonSurfaceinstead 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 anIconButton.
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 newcurrencyfield of the binding) anddaysUntil(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
unknownStringKindtoreaderClock, and thex-templateblock. 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 fixturefixtures/template-text-screens.jsonin 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:
currencyneeds a code and a code needscurrency(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"or2026-10-14is 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, fromunknownTemplateValueKindtomissingNumberLocaleData. - 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/react0.6.0 draws templates, the three formats and plural forms, and has a new propnow— the clockdaysUntilcounts from. Pass the same moment on the server and in the browser, otherwise the two can count different days. - Flutter package:
hotlayout_flutter0.4.0 reads document contract 9.4.0 and data dictionary 1.2.0: sentences with named places, the formatsnumber,currency(amount, no-break space, ISO code —120 JOD) anddaysUntil, plural words by CLDR category. New parameternowonrenderDocument,renderScreenandScreenFlow; new fieldLocalizedStrings.sourceLocale. Changed: a binding whose data is a map, a list or an empty string shows its fallback text (ChoiceGroup.valueis not affected). A date is only a string of the contract's form — ISO 8601 with time and zone;date,dateTime,timeanddaysUntilshow anything else as it is (a date without a zone used to be read as local time).numberandcurrencydo 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 —
IconTokennow lists 332 (it had ten). They are accepted wherever an icon is:Icon.name, the leading icon of aListTile, 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-mirroredIconslists 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/react0.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 shipsTHIRD-PARTY-NOTICES.md. - Flutter package:
hotlayout_flutter0.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
Repeaterhas anavigateaction; 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-dataScopein 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/react0.4.0 gives the hostsource.recordwith 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'(arnever picksar-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-EGwithoutar). - Known limit:
zh-TWfalls tozh; see the localization page. - Older renderer: a 9.0.0 renderer compares the device locale with the document's tags literally. For
ar-EGit 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.styleand 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 leftprops; 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 inprops. - 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-tokensblock names the shape and the required core. - Changed:
style.backgroundis{ kind: 'color' | 'gradient' | 'image', … }; in 8.x it was a color token. - Added:
colorandtypographyare inherited down the tree; at the root of a screen they areonSurfaceandbodyM.Textreads both. The color is also read byIcon,ListTile,AppBar,TabandAccordion, 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 aListTileis a main part: a plain glyph with no disc.ButtonandBadgeread no inherited color; a text style is read only byText. - Added: a
Buttonor aBadgewith astyleof 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, defaultsm). Both renderers used to drawsmfrom 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.
ListTileandAppBarhave no built-in vertical padding;Accordionhas no built-in padding, radius, background or shadow — all of it isstyle(the builder writes the same values; migration writes them explicitly). Remove them from the renderer or they are drawn twice.Iconis a plain glyph of the sizeprops.size, with no disc. - Added: the
x-styleblock: 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. Thex-gradientGeometryblock is in both artifacts, andx-tokens.optionalInThemelists 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'andzeroScaleToken: 'none-is-zero'in the theme. Newx-stylekeys: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, andx-style.layout(lists, so that a word is not a request to invent a list:iconGlyphTypography,listTileIconSizes,scrollableRow,tightSlots,sizedBoxKeys).x-tokens.zeroScale:nonein 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 ownstyle.widthand the widgets inx-style.layout.column.naturalWidthkeep a natural width, and they follow thealignof 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;nonNumericLiteralis confirmed for every bounded numeric field, inpropstoo.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 isxsbysm. - Added (three more words before the release):
naturalWidthInTightSlot: 'natural-start-top'— aBadge, aButtonthat is not full width, anIcon, aCircularProgressand a verticalDivider/Spacerin aGridcell 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 itsjustifydoes nothing; a row that is a layer of aStackis 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.invalidWeightnow also covers a fractional weight:1.5is 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 ownstyle.widthand the widgets inx-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 astyle.widthtakes what is left as a share of weight 1. Data:x-style.layout.row. The list of rows covered byrowInUnboundedWidthis 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 aButtonis 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:
scrollableRowChildWidthismax(row-viewport, own-width)and names its stretching content as a list; a tight slot is aGridcell or a cell of aRepeaterthat hascolumns— aRepeaterwithoutcolumnsis not one. - Changed: a theme and a document are stricter at the door:
fontWeightis a multiple of 100 (multipleOf: 100); the key__proto__is refused inthemesandaction.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 aborderslot on a known color; the builder writes--product-role-<group>-<role>-border-widthfor 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;filloverridesstyle.widthandstyle.height; the corner radius clips the background and the content;opacityapplies to the whole box with its content and shadow. - Removed: the rule "the node's
styleis stronger than the widget'sprops". 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
stylewith one box model for every widget —margin, then the box (size, background, border, radius, shadow, opacity), thenpadding, then the content. Add no defaults of your own: a node withoutstylehas 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
propsandstyle. 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
formaton a text binding:date,dateTimeortime. Two policy keys:unknownBindingFormatandunparsableBoundValue, bothrender-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 noformat. 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,
isEmptyholds and every other operator does not. Check for absence before you switch on the operator.unknownVisibilityOpstays 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
Rowwithoutwrapand withoutscrollable, or aContainerwithdirection: '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,percentornone. The default isnone, 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-mirroredIconsblock in the schema artifact. It currently listschevron. - 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 withfill: truetakes the size of the stack instead of its own. The field is legal only inside aStack, 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.columnstogether withdirection: '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
presentationon a screen entry:pageormodal. No field meanspage. Feature name:modalScreens. - Renderer: draw a
modalscreen 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(coverorcontain), and an optional icon in the trailing slot ofListTile. 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
scrollableandwrapare 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 ascreenId. Policy keyunknownEntryPoint: '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 asvisibleIf. - Older renderer: draws the node with its base style; states are not distinguished.
8.8.0 — tabs#
- Added: widgets
TabsandTab. - Renderer: draw one panel at a time under a shared strip of titles. The title is on each
Tab.Tabis 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
columnschildren in a row (2–4) at equal widths, on any screen width. This is notRowwithwrap, which fits as many as the natural widths allow. - Older renderer: applies
unknownWidget: 'render-children'.
8.4.0 — lists from data#
- Added: widget
Repeaterand 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
stylewithcolor,typographyandbackground. Each refers to a token, never to a value. Feature name:nodeStyle. - Renderer: the node's
styleis stronger than the widget's ownprops. 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
bindingbranch ofImageSource:{ kind: 'binding', path, fallback }. Thefallbackis 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
kindin the image source.
8.1.0 — container direction#
- Added:
Container.direction:columnorrow. The default iscolumn. 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 keyoverlongText: '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 rolesbuttonVariant.secondaryandbuttonVariant.ghostnameonSurfaceasforeground; it wasprimary. The label was#5b55eaon#111827, 3.32:1, below the 4.5:1 that WCAG AA asks of text; it is now 16.1:1. The border ofsecondaryis stillprimary. The built-in themelightis 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
#rrggbbor#rrggbbaa, the alpha last. Policy keycolorAlpha: '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.gradientAngleSpaceis nowaxis-as-css-diagonal-css-to-corner(it wasaxis-aligned-only), plusdiagonalGradientGeometry: 'css-to-corner'; the formula is thex-gradientGeometryblock. - 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) andunorderedGradient: 'do-not-draw'. Thex-rulesblock: 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
#rrggbbaawith the alpha last. Draw the diagonal angles as CSSto <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
sourcetext 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
letterSpacingon a text style, in logical pixels, from −2 to 8. Optional colorslocked,inProgress,completed. Thex-optionalColorsblock with their fallback tokens. - Renderer: pass
letterSpacingthrough 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 toem. - 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 bybrightness.
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 prefixtask): completed tasks; its first record is the first task oftasksafter 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
idthat 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 prefixactivity), 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, foriconPath),account.lastStatus,account.lastMerchant(text),account.lastAt(a date). - Added — paths written by
setValueandtoggleValue: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, sample120) — an amount of money, shown with thecurrencyformat 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.000Zin 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
sampleValueofitem.imageand of theitemscollection 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/webpaddress, not onlydata:image/png. The backend sends real values.
1.1.1 — a real sample image#
Released 2026-10-09.
- Changed: the
sampleValueofitem.imageis 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.absentCollectionis nowempty(it wasnot-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.