Migration To v0.13
Migration To v0.13
Section titled “Migration To v0.13”This is a historical migration guide for applications upgrading from pre-0.13 APIs. New applications should start with Getting Started and Modals. The old APIs in the “Before” examples are intentionally unavailable in 0.20.0.
v0.13 removes the legacy context-owned modal API and moves modals, popups, and overlays into normal render declarations. The main migration is to replace imperative ctx.modal(...) calls with Modal children.
Modals
Section titled “Modals”ctx.modal(...), ctx.modal_context(), ModalContext, and the legacy modal host are removed.
Before:
ctx.modal(self.open.clone(), |ctx| { ctx.mount::<SettingsModal>(SettingsModalProps { title: "Settings".into(), })});
Column::new().child(Button::new("Open").on_click({ let open = self.open.clone(); move |_| open.set(true)}))After:
Column::new() .child(Button::new("Open").on_click({ let open = self.open.clone(); move |_| open.set(true) })) .child( Modal::new(ctx.mount::<SettingsModal>(SettingsModalProps { title: "Settings".into(), })) .open(self.open.clone()) .target(Root), )Modal is a normal child declaration. It is layout-neutral, so it does not take space in its parent layout. When open, its content is layered over the selected target.
Targets
Section titled “Targets”Modal::target(...) accepts Parent, Root, or an ElementRef.
Modal::new(content).open(open.clone()).target(Parent);Modal::new(content).open(open.clone()).target(Root);Modal::new(content).open(open.clone()).target(panel_ref);Parentcovers the declaring parent bounds and is the default.Rootcovers the viewport. Use it for the old app-wide modal behavior.ElementRefcovers a specific element’s bounds.
If your old modal expected to cover the whole app, add .target(Root). If you want a modal that only covers a panel or card, use the default Parent target by rendering the modal as a child of that container.
Closing
Section titled “Closing”Modal close state now belongs to your signal.
Before:
let modal = ctx.modal_context().unwrap().clone();
Button::new("Close").on_click(move |_| modal.close())After:
let open = self.open.clone();
Button::new("Close").on_click(move |_| open.set(false))Signal-backed modals also close on Escape by default.
Render Timing
Section titled “Render Timing”The old API received a closure that built modal content through the modal registry. In v0.13, Modal::new(...) receives content from the regular render path.
If the modal content is expensive, keep the expensive work inside the mounted modal component or gate it yourself:
let modal = if self.open.get() { Modal::new(ctx.mount::<HeavyModal>(props)) .open(self.open.clone()) .target(Root)} else { Modal::new(Stack::new()).open(self.open.clone()).target(Root)};For most component-backed modals, the direct Modal::new(ctx.mount::<ModalComponent>(props)) migration is the right shape.
Scoped Modals
Section titled “Scoped Modals”To cover only a parent container, render the modal inside that container and use the default target:
Stack::new() .child(panel_content) .child( Modal::new(panel_modal_content) .open(panel_open.clone()) .target(Parent), )The explicit .target(Parent) is optional.
To cover a specific element, attach an element ref to the target and pass the same ref to the modal:
let panel_ref = ctx.element_ref();
Column::new() .ref_element(panel_ref.clone()) .child(panel_content) .child( Modal::new(panel_modal_content) .open(open.clone()) .target(panel_ref), )Popups And Overlays
Section titled “Popups And Overlays”Manual absolute-positioned dropdowns and popovers should migrate to Popup when they are anchored to an element.
Popup::new(anchor_ref, menu_content) .open(open.clone()) .placement(Placement::BottomStart) .offset(0.0, 8.0) .match_anchor_width(true)Popover is an alias for Popup. Use low-level Overlay only when you need custom anchored layer behavior that Popup does not expose.
Hit Testing
Section titled “Hit Testing”Transparent overlay wrappers should opt into the intended hit-test behavior instead of relying on host-specific event paths.
Stack::new().hit_test(HitTestBehavior::ContentOnly);Rect::new(200.0, 100.0).pointer_events_none();HitTestBehavior::ContentOnlylets transparent wrapper space pass through while keeping child content interactive..pointer_events_none()makes the node and descendants ignored by hit testing.
Tests should assert on user-visible modal, popup, or overlay content rather than legacy host tag names.
Before:
assert_eq!(tree.root().unwrap().tag_name(), "ModalHost");After:
let root = tree.root().unwrap();assert!(find_by_text(root, "Settings").is_some());The runtime may still introduce an internal overlay host for active root overlays, root modals, and popups. That host is implementation detail; application tests should prefer content, layout, and behavior assertions.