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:
| Was | Now |
|---|---|
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:
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
asChildwith your owncanvasgroupandcreateCanvasGroupRevealRecipe(), 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 underContent— and the dialog still ownsSizeandVisibleon 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
npx lattice-ui upgrade.- Typecheck. Every error is a
GroupTransparencyorGroupColor3on a part that is now aFrame. - Swap each canvas-group recipe for its surface counterpart, and decide per surface whether the children should fade with it.
- 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.
Most of this release does not fail to compile — it renders differently. A dialog that looked finished on 0.6 comes up as an invisible overlay and an unstyled panel on 0.7, because the appearance it was relying on came from the primitive. Budget a pass over every surface, not just the ones the compiler flags.
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:
<Dialog.Overlay /><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.
<Dialog.Content>{/* ... */}</Dialog.Content>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:
| Part | Recipe that used to be its default |
|---|---|
Dialog.Content | None that still fits — see below |
Menu.Content, ContextMenu.Content, Combobox.Content, Tooltip.Content, Popover.Content, Select.Content | createPopperEntranceRecipe(placement) |
Accordion.Content, Tabs.Content | createSurfaceRevealRecipe() |
Checkbox.Indicator, RadioGroup.Indicator | createIndicatorRevealRecipe(size) |
Toast.Root | createToastRevealRecipe() |
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:
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.
Dialog.Overlay does not accept transition — an unstyled overlay has nothing to fade. It still owns presence timing, so to animate a dim, put an element inside it (or use asChild) and animate that.
4. Props removed
| Part | Removed | Do this instead |
|---|---|---|
Switch.Root | trackColorMode, trackOnColor, trackOffColor, disabledTrackColor | Style the track yourself; read state from Switch context |
RadioGroup.Item | transition | Nothing — it only fed the removed color animation |
ToggleGroup.Item | transition | Nothing — 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.
<Select.Item value="apple" textValue="Apple" /><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:
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:
<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 onTextFieldandTextarea,Toast.Title,Toast.Description,Select.Value,Combobox.Value,Avatar.ImageandDialog.Overlaypreviously dropped children unlessasChildwas set, so attaching aUICornerorUIPaddingsilently did nothing. Delete anyasChildyou added only to work around that. asChildaccepts UI modifiers as siblings. AUICorner,UIPaddingorUIListLayoutwritten next to the child — the shape a Tailwind-styleclassNametransform 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,useSelectItemContextanduseComboboxItemContextreturn{ highlighted, disabled }.highlightedreports 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:
onFocusChangefires when managed focus enters or leaves a node — including when the focused node unmounts, so a highlight cannot outlive its widget. Nodes that never becomeGuiService.SelectedObject(syncToRoblox: false, or a non-Selectableobject) previously had no way to know.onActivateroutes activation to the focused node. While aFocusScopeis active the navigation controller bindsReturn,KeypadEnter,SpaceandButtonAand runs the focused node’s activation. Opting out leaves the engine’s ownActivatedpath untouched.activateFocusedNode()drives the same path directly.
See Focus for the full API.
Upgrade checklist
npx lattice-ui upgradeto move every@lattice-ui/*package to 0.8.1.- Typecheck. Fix the forwarded-prop errors — those are props landing on an instance that does not accept them.
- Remove
trackColorMode/trackOnColor/trackOffColor/disabledTrackColorfromSwitch.Root, andtransitionfromRadioGroup.ItemandToggleGroup.Item. - Give
Select.ItemandCombobox.Itemchildren, render the toast queue insideToast.Viewport, and add layouts toMenu.GroupandContextMenu.Group. - Run each surface and restyle what went flat. Overlays, menu item highlights and switch tracks are the usual casualties.
- Add
transitionwherever 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.jsondependency entries before upgrading. Leaving both the old and new name listed makes two copies resolve side by side, which npm rejects. npx lattice-ui initrewrites the old names for you.- If you pin CLI registry keys,
coreis nowruntime. Other component keys are unchanged.
0.5.x → 0.6.0
Toast.Root’stransitionnow takes aPresenceMotionConfig(initial/reveal/exit) instead of aResponseMotionConfig.
0.4.x → 0.5.0
- Import focus and motion helpers from
@lattice-ui/focusand@lattice-ui/motionrather than@lattice-ui/core.