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.
If you ship to Roblox, 0.8.0 already worked. Under browser React, asChild failed on every styled
subtree until this release. That renderer leaves a host tag as written, where @rbxts/react
rewrites it to a class name.
0.7.x → 0.8.0
One shape change: every motion host that was a CanvasGroup is now a Frame. Placement, presence
timing, exit-before-unmount, dismissal boundaries, focus scoping and layering are untouched.
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 there.
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.
BackgroundTransparency fades one instance’s own background, not its children, so labels and icons inside a fading surface stay opaque. Fade them alongside it, 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. Two ways out, the second enabled by Dialog.Content accepting asChild as
of this release:
- Give the content a
Position-only config for the rise and put the fade on the overlay dim or your own panel elements. - Pass
asChildwith your owncanvasgroupandcreateCanvasGroupRevealRecipe(), which fades the surface as one layer. The outside-press boundary moves down to that element’s first host child, 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. Slot matched UI modifiers by JSX tag, so every modifier a
Tailwind-style transform emits counted as a second target candidate. Any subtree with a UICorner
sibling failed. Delete whatever you did to avoid it. Under a browser React renderer, see
0.8.0 → 0.8.1.
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
The largest breaking release so far. Every primitive is stripped down to behaviour: colours, fixed
sizes, font sizes, literal text and decorative children are gone. Any prop the underlying instance
accepts 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, so budget a pass over every surface.
1. Every primitive renders unstyled
Primitives set behaviour plus the minimum needed to neutralize Roblox’s instance defaults. A bare
textbutton renders an opaque grey box labelled “Button”, so parts still set
BackgroundTransparency = 1 and Text = "". Everything past that is yours.
Geometry the primitive computes from state stays with the primitive. Progress fill ratios, slider thumb travel, popper-driven position, scroll thumb size, and the full-screen sizing that makes an overlay cover the screen.
Since 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 renders, so a prop that instance does not
accept is a compile error rather than silently dropped. Dialog.Overlay is a textbutton. The
content and surface parts are all frames, so passing TextColor3 to one will not compile.
3. Motion no longer has defaults
No presence motion runs unless you ask for it. Timing is unchanged, and content stays mounted
until an exit transition finishes. With no transition, it is instant. Motion that follows a value
the primitive computes is unaffected: Progress.Indicator still settles its fill, and the slider
and switch thumbs still follow their values.
<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 render a frame, so the fade reaches the surface’s background and not the labels inside it. Fade those with it, or pass asChild with your own canvasgroup and the canvas-group variant.
The popper recipes took the resolved placement, so motion originated from the side the surface
landed on after any flip. Read it from usePopper to get that back. Passing no placement animates
from a fixed side.
Dialog.Content is the one part whose old default needs a change — see Fading a dialog for the two patterns.
Prefer a recipe over a hand-written config: each declares the correct target contract, which properties motion may own, so it cannot fight your layout. A hand-written PresenceMotionConfig 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 animate a dim by putting an element inside it.
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 it 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 rather than hardcoded queue markup. Map over
useToast().visibleToasts yourself:
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.Overlay. They previously 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 is re-parented under the element the props land on. It no longer raises “asChild requires a child element”. That is the shape a Tailwind-styleclassNametransform such as vela-rbxts emits. Fragments are looked through, and 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. It 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.