UIWidget
A floating overlay that lives outside the screen system: always position-fixed relative to the
device, drawn above the active screen, and untouched by Router navigation. Use it for bottom
sheets, dialogs, toasts-with-actions, floating players. Create a widget once at module scope
and reuse it — visibility is imperative.
At a glance
const dialog = UIWidget([
UIText("Delete item?").style({ fontWeight: 700, fontSize: 18, color: "black" }),
UIButton([ UIText("Delete").style({ color: "white" }) ])
.style({ bgColor: "#FF4032", borderRadius: 8, height: 40, justifyContent: "center" })
.onClick(() => { doDelete(); dialog.hide() }),
])
.style({ left: 24, right: 24, top: "40%", p: 16, gap: 12, borderRadius: 16,
bgColor: "white", overlayColor: "rgba(0,0,0,0.5)" })
.onOverlayTap(() => dialog.hide())
.onBackPressed(() => dialog.hide())
// anywhere, on any screen:
dialog.show()Creating & visibility
UIWidget(): UIWidget
UIWidget(children: UINodeChild[]): UIWidget
widget.show(): void // mount and display (hidden by default)
widget.hide(): void // unmount
widget.isShow: boolean // getter — currently shown?Full container surface: .append / .insert / .remove / .setContent, .style,
.onLayout. Position it with top / left / right / bottom / width / height — the
coordinates are device-screen space (safe-area values like "safe-bottom" work).
a widget persists across Router.push / pop. If it belongs to one screen only, pair
it with the screen's lifecycle: .onOpen(() => sheet.show()), .onClose(() => sheet.hide()).
Modal behavior — overlayColor
widget.style({ overlayColor: "rgba(0, 0, 0, 0.5)" }) // Color | null
widget.onOverlayTap(cb: () => void): thisoverlayColor adds a full-screen scrim behind the widget that blocks all taps underneath —
this is what turns a widget into a modal (dialog / bottom sheet). "transparent" is invisible
but still intercepts; null (the default) removes the layer entirely. A tap on the scrim fires
onOverlayTap — usually () => widget.hide().
Touch & back button
widget.onTouchStart(ev => …) // ev.track({...}) for drag gestures (sheet dragging)
widget.onBackPressed(cb: () => void) // Android back while the widget is up — usually hide()Widgets are one of the three touch-receiving elements (with UIButton and UIScreen) — see
Pointer events.
Exit animations — animateTo with commit: false
widget.animateTo({ overlayColor, opacity, transform, …, duration?, delay?, commit?: boolean })
widget.animateFrom({ … }) // animate from the given values to the current styleanimateTo normally writes the target values into the widget's style when it starts. For an
exit animation that's wrong — the next show() would start from the faded-out values. Pass
commit: false to play the animation without persisting it, and hide() when done:
const close = () => {
widget.animateTo({ opacity: 0, commit: false, duration: 250 })
setTimeout(() => widget.hide(), 250) // style still has opacity 1 for the next show()
}Bottom sheet — the canonical widget
Drag-to-dismiss sheet: position driven by transform, scrim via overlayColor, drag captured
with claim: "pan-y".
let pos = 0, height = 0
const slideTo = (to: number, done?: () => void) => animate({
from: pos, to, duration: 300, easing: cubicBezier(0.2, 0, 0.2, 1),
onUpdate(v) { pos = v; sheet.style.transform = `translateY(${v}px)` },
onComplete() { done?.() },
})
const close = () => {
slideTo(height, () => sheet.hide()) // slide out, then unmount
sheet.animateTo({ overlayColor: "transparent", commit: false }) // fade the scrim, don't persist
}
const sheet = UIWidget([
UIColumn([]).style({ width: 50, height: 6, borderRadius: 3, bgColor: "#D9D9D9", mx: "auto", my: 12 }),
UIText("Title").style({ px: 16, fontWeight: 700, fontSize: 20, color: "black" }),
])
.style({ bgColor: "white", borderRadius: 20, bottom: 0, left: 0, right: 0, height: 400,
overlayColor: "rgba(0, 0, 0, 0.5)" })
.onTouchStart(ev => ev.track({
onMove({ deltaY }) { pos = Math.max(0, pos + deltaY); sheet.style.transform = `translateY(${pos}px)` },
onEnd() { pos > height * 0.3 ? close() : slideTo(0) },
claim: "pan-y", // win the vertical gesture from any scroller inside
}))
.onOverlayTap(close)
.onBackPressed(close)
.onLayout(l => { height = l.height })Pitfalls
// ✗ re-creating the widget per screen / per show
const openSheet = () => UIWidget([...]).show() // leaks a new widget every call
// ✓ create once at module scope, show()/hide() the same instance
// ✗ expecting it to appear on creation
const w = UIWidget([...]) // hidden until w.show()See also
- UIScreen & Router — the screen stack widgets float above
- Pointer events & gestures —
ev.track(),claim - Styling —
animate,animateTo, transforms