Latticereference

Migration

Upgrading between breaking Lattice UI releases, with the exact code changes each one requires.

Lattice UI is pre-1.0, so 0.x minors may break. This page carries the code changes each breaking release needs; Releases carries the summaries and the reasoning.

0.8.0 → 0.8.1

No work. A patch with no API change: asChild recognises UI modifier siblings under browser React as well as in Roblox, UIShadow joins the modifier set, and re-parented modifiers no longer collide keys with the element’s own children. npx lattice-ui upgrade and carry on.

The one thing worth knowing is why there were two spellings to get wrong, because it decides which renderers 0.8.0 was broken under. @rbxts/react rewrites a host tag to its Roblox class name before it builds the element, so in Roblox <uicorner /> reaches Slot as "UICorner". A browser React renderer — what the previews on this site run on — leaves the tag as written. 0.8.0 matched the first spelling only. If you ship to Roblox, 0.8.0 already worked; if you render Lattice primitives under browser React, asChild failed there on every styled subtree until this release.

0.7.x → 0.8.0

A small release with one shape change: every motion host that was a CanvasGroup is now a Frame. Nothing about placement, presence timing, exit-before-unmount, dismissal boundaries, focus scoping or layering moves.

1. The CanvasGroup-only props stop compiling

Dialog.Content, Popover.Content, Tooltip.Content, Menu.Content, ContextMenu.Content, Select.Content, Combobox.Content and Toast.Root forward props onto a Frame now, so GroupTransparency and GroupColor3 are compile errors on those parts. Both were the only reason to know the host was a CanvasGroup in the first place.

The reason to change it: a CanvasGroup flattens its whole subtree into one offscreen composited layer, so children could not carry their own transparency — and every menu and tooltip paid for that layer whether or not anything faded.

2. A GroupTransparency fade becomes a BackgroundTransparency fade

Swap the recipe for its Frame counterpart:

WasNow
createCanvasGroupRevealRecipe()createSurfaceRevealRecipe()
createCanvasGroupPopperEntranceRecipe(placement)createPopperEntranceRecipe(placement)

Both canvas-group recipes stay exported, for a canvasgroup you slot in yourself through asChild. createToastRevealRecipe already fades BackgroundTransparency to match its new host.

BackgroundTransparency fades one instance’s own background, not its children — so labels and icons inside a surface stay opaque while the surface fades. Fade them alongside it (TextTransparency, ImageTransparency), or bring the composited layer back deliberately:

Opt back into a one-layer fade
import { createCanvasGroupRevealRecipe } from "@lattice-ui/react-motion";
<Popover.Content asChild transition={createCanvasGroupRevealRecipe()}>
<canvasgroup>{/* … */}</canvasgroup>
</Popover.Content>

3. Dialog.Content is the one part with no drop-in replacement

Its motion host spans the whole layer, so animating BackgroundTransparency there paints a full-screen rectangle rather than fading the panel. Two ways out, and Dialog.Content accepts asChild as of this release specifically to make the second one possible:

  • Give the content a Position-only config for the rise and put the fade on the overlay dim or on your own panel elements.
  • Pass asChild with your own canvasgroup and createCanvasGroupRevealRecipe(), which fades the surface as one layer the way the primitive used to. The outside-press boundary moves down to that element’s first host child — the same panel a plain dialog renders directly under Content — and the dialog still owns Size and Visible on whatever element it renders.

4. asChild starts working on Vela-styled subtrees

Not work, a fix to stop working around. The Slot runtime matched UI modifiers by their JSX tag, but @rbxts/react rewrites a host tag to its Roblox class name before it builds the element — <uicorner /> arrives as "UICorner" — so every modifier missed the lookup and counted as a second target candidate. Any subtree carrying a UICorner, UIPadding or UIListLayout sibling, which is what a Tailwind-style className transform emits, failed with “expected exactly one child element besides any UI modifiers”. Delete whatever you did to avoid it.

In Roblox, that is the whole story. Under a browser React renderer the tag survives as written, and this release matched the class name only — see 0.8.0 → 0.8.1, which recognises both.

Upgrade checklist

  1. npx lattice-ui upgrade.
  2. Typecheck. Every error is a GroupTransparency or GroupColor3 on a part that is now a Frame.
  3. Swap each canvas-group recipe for its surface counterpart, and decide per surface whether the children should fade with it.
  4. Re-do the dialog’s reveal — position on the content, fade on the overlay, or a slotted canvasgroup.

0.6.x → 0.7.0

0.7.0 is the largest breaking release so far. It strips every primitive down to behavior: colors, fixed sizes, font sizes, literal text and decorative children are gone. In exchange, any prop the underlying instance accepts now forwards straight through, so styling no longer requires asChild.

1. Every primitive renders unstyled

Primitives now set behavior plus the minimum needed to neutralize Roblox’s own instance defaults — a bare textbutton renders an opaque grey box labelled “Button”, so parts still set BackgroundTransparency = 1, BorderSizePixel = 0, Text = "" and friends. Everything past that is yours.

Geometry the primitive computes from state is still owned by the primitive: progress fill ratios, slider thumb travel, popper-driven position, scroll thumb size, and the full-screen sizing that makes an overlay actually cover the screen.

Because props now forward, the fix is usually to move styling onto the part itself rather than reach for asChild:

0.6 — appearance came from the primitive
<Dialog.Overlay />
0.7 — pass the appearance you want
<Dialog.Overlay
BackgroundColor3={Color3.fromRGB(0, 0, 0)}
BackgroundTransparency={0.5}
/>

asChild is still there, and still the right tool when you need a different instance class than the part renders, or when the element comes from elsewhere.

2. Forwarded props are type-checked

Each part checks forwarded props against the instance it actually renders. Passing a prop that instance does not accept is now a compile error instead of being silently dropped.

That means you need to know what each part renders. Dialog.Overlay is a textbutton; the content and surface parts — Dialog.Content, Menu.Content, Tooltip.Content, Popover.Content and friends, Toast.Root, Toast.Viewport, Menu.Group — are all frames. Passing TextColor3 to a frame will not compile.

3. Motion no longer has defaults

No presence motion runs unless you ask for it. Presence timing is unchanged — content still stays mounted until an exit transition finishes — but with no transition the transition is instant.

Motion that follows a value the primitive computes is unaffected: Progress.Indicator still settles its fill by default, and the slider thumb, slider range and switch thumb still follow their values. That motion is behavior, not decoration, so it stayed.

0.6 — animated with no configuration
<Dialog.Content>{/* ... */}</Dialog.Content>
0.7 — pass the transition you want
import { createPopperEntranceRecipe } from "@lattice-ui/react-motion";
<Menu.Content transition={createPopperEntranceRecipe("bottom")}>
{/* ... */}
</Menu.Content>

The recipes that used to be applied for you are all still exported from @lattice-ui/react-motion, unchanged. Passing one back explicitly reproduces the old feel exactly:

PartRecipe that used to be its default
Dialog.ContentNone that still fits — see below
Menu.Content, ContextMenu.Content, Combobox.Content, Tooltip.Content, Popover.Content, Select.ContentcreatePopperEntranceRecipe(placement)
Accordion.Content, Tabs.ContentcreateSurfaceRevealRecipe()
Checkbox.Indicator, RadioGroup.IndicatorcreateIndicatorRevealRecipe(size)
Toast.RootcreateToastRevealRecipe()

Those hosts all render a frame now, so the fade reaches the surface’s own background and not the labels inside it. Fade those with it, or pass asChild with your own canvasgroup and the canvas-group variant of the recipe.

The popper recipes took the resolved placement, so the motion originated from the side the surface actually landed on after any flip. Read it from usePopper if you want that behavior back; passing no placement animates from a fixed side.

Dialog.Content is the one part whose old default needs a change to pass back. Its motion host is a frame rather than a canvasgroup, and a frame has no property that fades its descendants, so createCanvasGroupRevealRecipe() has nothing to write GroupTransparency to — unless you pass asChild with your own canvasgroup, which makes that element the motion host and the recipe fit again. Otherwise give the content a Position-only config for the rise and fade the overlay dim or your own panel elements — Fading a dialog has both patterns.

Prefer these over a hand-written config. Each one declares the correct target contract — which properties motion is allowed to own — so it cannot fight your layout. A PresenceMotionConfig written by hand needs reveal/exit as { values, intent }, not bare properties:

The config shape, if you do write one
transition={{
initial: { Position: UDim2.fromOffset(0, 8) },
reveal: { values: { Position: UDim2.fromOffset(0, 0) }, intent: { duration: 0.12 } },
exit: { values: { Position: UDim2.fromOffset(0, 8) }, intent: { duration: 0.096 } },
}}

See Presence and motion for which parts take a PresenceMotionConfig and which take a ResponseMotionConfig, and Motion recipes for the full catalog.

4. Props removed

PartRemovedDo this instead
Switch.RoottrackColorMode, trackOnColor, trackOffColor, disabledTrackColorStyle the track yourself; read state from Switch context
RadioGroup.ItemtransitionNothing — it only fed the removed color animation
ToggleGroup.ItemtransitionNothing — same

The SwitchTrackColorMode type is gone with them.

5. Contract changes per primitive

Select.Item and Combobox.Item no longer render textValue as their label. Supply the label as a child. textValue still drives Select.Value and combobox filtering, so keep passing it when the label is not a plain string.

0.6
<Select.Item value="apple" textValue="Apple" />
0.7
<Select.Item value="apple" textValue="Apple">
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(200, 32)}
Text="Apple"
TextColor3={Color3.fromRGB(240, 244, 250)}
TextXAlignment={Enum.TextXAlignment.Left}
/>
</Select.Item>

Toast.Viewport renders its children instead of the toast queue markup it used to hardcode. Map over useToast().visibleToasts yourself — the same shape the asChild path already required:

0.7
const toast = useToast();
<Toast.Viewport>
<uilistlayout Padding={new UDim(0, 8)} SortOrder={Enum.SortOrder.LayoutOrder} />
{toast.visibleToasts.map((record) => (
<Toast.Root
key={record.id}
onExitComplete={() => toast.finalize(record.id)}
visible={!record.exiting}
>
<Toast.Title>
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(300, 20)}
Text={record.title ?? ""}
TextColor3={Color3.fromRGB(240, 244, 250)}
TextXAlignment={Enum.TextXAlignment.Left}
/>
</Toast.Title>
</Toast.Root>
))}
</Toast.Viewport>

Menu.Group and ContextMenu.Group no longer render a vertical UIListLayout or force AutomaticSize. They were the last primitives holding an opinion about how their children lay out. Supply the layout yourself, as Select.Group and Combobox.Group already required:

0.7
<Menu.Group>
<uilistlayout FillDirection={Enum.FillDirection.Vertical} />
{/* items */}
</Menu.Group>

6. Things that got easier

Not everything here is work. Three fixes remove workarounds you may currently be carrying:

  • 16 leaf parts now render children. Both separators, both text inputs, the labels, descriptions and messages on TextField and Textarea, Toast.Title, Toast.Description, Select.Value, Combobox.Value, Avatar.Image and Dialog.Overlay previously dropped children unless asChild was set, so attaching a UICorner or UIPadding silently did nothing. Delete any asChild you added only to work around that.
  • asChild accepts UI modifiers as siblings. A UICorner, UIPadding or UIListLayout written next to the child — the shape a Tailwind-style className transform such as vela-rbxts emits — is re-parented under the element the props land on, instead of raising “asChild requires a child element”. Fragments are looked through; two real candidates are still an error. Note that the 0.7.0 implementation matched modifiers by their JSX tag and therefore never fired in Roblox — that works from 0.8.0, and from 0.8.1 under a browser React renderer too. See styling with Vela.
  • Highlight state is readable. useMenuItemContext, useContextMenuItemContext, useSelectItemContext and useComboboxItemContext return { highlighted, disabled }. highlighted reports hover or managed focus, which is what you need to render the highlight the primitive used to draw.

7. Focus additions

Two new useFocusNode options matter if you render your own focus highlights:

  • onFocusChange fires when managed focus enters or leaves a node — including when the focused node unmounts, so a highlight cannot outlive its widget. Nodes that never become GuiService.SelectedObject (syncToRoblox: false, or a non-Selectable object) previously had no way to know.
  • onActivate routes activation to the focused node. While a FocusScope is active the navigation controller binds Return, KeypadEnter, Space and ButtonA and runs the focused node’s activation. Opting out leaves the engine’s own Activated path untouched. activateFocusedNode() drives the same path directly.

See Focus for the full API.

Upgrade checklist

  1. npx lattice-ui upgrade to move every @lattice-ui/* package to 0.8.1.
  2. Typecheck. Fix the forwarded-prop errors — those are props landing on an instance that does not accept them.
  3. Remove trackColorMode / trackOnColor / trackOffColor / disabledTrackColor from Switch.Root, and transition from RadioGroup.Item and ToggleGroup.Item.
  4. Give Select.Item and Combobox.Item children, render the toast queue inside Toast.Viewport, and add layouts to Menu.Group and ContextMenu.Group.
  5. Run each surface and restyle what went flat. Overlays, menu item highlights and switch tracks are the usual casualties.
  6. Add transition wherever you want motion back.

0.6.0 → 0.6.1

A rename only — no runtime behavior, exports or APIs changed.

  • Add the react- prefix to every import: @lattice-ui/<name>@lattice-ui/react-<name>.
  • @lattice-ui/core@lattice-ui/react-runtime.
  • Update package.json dependency entries before upgrading. Leaving both the old and new name listed makes two copies resolve side by side, which npm rejects.
  • npx lattice-ui init rewrites the old names for you.
  • If you pin CLI registry keys, core is now runtime. Other component keys are unchanged.

0.5.x → 0.6.0

  • Toast.Root’s transition now takes a PresenceMotionConfig (initial/reveal/exit) instead of a ResponseMotionConfig.

0.4.x → 0.5.0

  • Import focus and motion helpers from @lattice-ui/focus and @lattice-ui/motion rather than @lattice-ui/core.