Overview
In UEFN, most UI is built from widgets — buttons, text blocks, images — but those widgets need a container that decides where they appear on screen. The canvas widget fills that role: it is a free-form layout panel where each child sits in its own canvas_slot that describes position, size, alignment, and draw order.
Use canvas when you need:
- Overlapping elements (health bars, crosshairs, objective icons) at precise screen positions.
- UI that changes layout at runtime (moving a marker, swapping a panel in/out).
- Layered widgets with explicit z-ordering.
The two types you'll work with constantly are:
| Type | Kind | Role |
|---|---|---|
canvas |
class (widget) |
The container; call AddWidget / RemoveWidget at runtime |
canvas_slot |
struct |
Describes one child's position, size, alignment, and z-order |
canvas_slot fields you set when building a slot:
Anchors— aanchorsstruct withMinimumandMaximumvector2values (0.0–1.0) that pin the slot relative to the canvas bounds.Offsets— amarginstruct (Left,Top,Right,Bottom) that shifts or sizes the slot in pixels.Alignment— avector2pivot inside the widget itself (0.5, 0.5 = center).ZOrder—intdraw order; higher = on top.SizeToContent—logic; whentruethe widget sizes itself; whenfalseOffsets.Right/Bottomset explicit width/height.Widget— the childwidgetto display.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: A capture-the-flag map needs an on-screen "Objective" banner that slides in when the round starts, then disappears after 4 seconds. We build the banner as a text_block inside a canvas, add it to the player's HUD via player_ui, then remove it after the delay.
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /Fortnite.com/Characters }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /Verse.org/Colors }
# Localized helper so we can pass a message to text_block
ObjectiveBannerText<localizes>(S : string) : message = "{S}"
objective_banner_device := class(creative_device):
# Called once when the game session begins
OnBegin<override>()<suspends> : void =
# Iterate every player currently in the session
Players := GetPlayspace().GetPlayers()
for (Player : Players):
ShowBannerForPlayer(Player)
# Build and display the canvas banner for one player, then remove it
ShowBannerForPlayer(Player : player)<suspends> : void =
# Grab the player_ui component so we can push widgets onto their HUD
if (PlayerUI := GetPlayerUI[Player]):
# 1. Create the text widget that will be our banner
BannerLabel := text_block:
DefaultText := ObjectiveBannerText("⚑ Capture the Flag!")
DefaultTextColor := MakeColorFromHex("FFDD00FF")
# 2. Build a canvas_slot that anchors the banner to the top-center
# Anchors.Minimum/Maximum both at (0.5, 0.0) = top-center point anchor
# Offsets.Left = -300 shifts left by half the widget width (600 px wide)
# Offsets.Top = 80 drops it 80 px from the top edge
# SizeToContent = false so Right/Bottom set explicit 600×80 px size
BannerSlot := canvas_slot:
Anchors := anchors:
Minimum := vector2{X := 0.5, Y := 0.0}
Maximum := vector2{X := 0.5, Y := 0.0}
Offsets := margin:
Left := -300.0
Top := 80.0
Right := 600.0
Bottom := 80.0
Alignment := vector2{X := 0.0, Y := 0.0}
ZOrder := 10
SizeToContent := false
Widget := BannerLabel
# 3. Create the canvas and add the slot during construction via Slots
BannerCanvas := canvas:
Slots := array{BannerSlot}
# 4. Push the canvas onto the player's HUD
PlayerUI.AddWidget(BannerCanvas)
# 5. Wait 4 seconds, then tear it down
Sleep(4.0)
PlayerUI.RemoveWidget(BannerCanvas)```
**Line-by-line breakdown:**
- **`GetPlayspace().GetPlayers()`** — fetches every player so we show the banner to everyone.
- **`text_block`** — a built-in widget; `DefaultText` requires a `message`, so we use the `<localizes>` helper.
- **`canvas_slot`** — a struct literal; every field is set inline. `Anchors.Minimum == Anchors.Maximum` creates a *point anchor* (no stretching).
- **`Offsets`** — because `SizeToContent` is `false`, `Right` and `Bottom` are the widget's **width** and **height** in pixels, not a right/bottom margin.
- **`canvas{ Slots := array{BannerSlot} }`** — the `Slots` field is only read at construction time; use `AddWidget` / `RemoveWidget` for runtime changes.
- **`PlayerUI.AddWidget(BannerCanvas)`** — pushes the whole canvas onto the HUD.
- **`Sleep(4.0)`** — suspends this coroutine for 4 seconds (the function is `<suspends>`).
- **`PlayerUI.RemoveWidget(BannerCanvas)`** — cleanly removes the canvas after the delay.
## Common patterns
### Pattern 1 — Dynamically swap a widget inside a running canvas
Swap an icon widget out for a different one at runtime using `RemoveWidget` then `AddWidget`.
```verse
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /Fortnite.com/Characters }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }
SwapIconText<localizes>(S : string) : message = "{S}"
icon_swap_device := class(creative_device):
OnBegin<override>()<suspends> : void =
Players := GetPlayspace().GetPlayers()
if (Player := Players[0], PlayerUI := GetPlayerUI[Player]):
# Build first icon
IconA := text_block:
DefaultText := SwapIconText("🔴 Red Team Leading")
SlotA := canvas_slot:
Anchors := anchors:
Minimum := vector2{X := 0.0, Y := 1.0}
Maximum := vector2{X := 0.0, Y := 1.0}
Offsets := margin:
Left := 40.0
Top := -60.0
Right := 400.0
Bottom := 50.0
Alignment := vector2{X := 0.0, Y := 1.0}
ZOrder := 5
SizeToContent := false
Widget := IconA
HudCanvas := canvas:
Slots := array{SlotA}
PlayerUI.AddWidget(HudCanvas)
# Simulate a score change after 5 seconds
Sleep(5.0)
# Remove old icon, add new one
HudCanvas.RemoveWidget(IconA)
IconB := text_block:
DefaultText := SwapIconText("🔵 Blue Team Leading")
SlotB := canvas_slot:
Anchors := anchors:
Minimum := vector2{X := 0.0, Y := 1.0}
Maximum := vector2{X := 0.0, Y := 1.0}
Offsets := margin:
Left := 40.0
Top := -60.0
Right := 400.0
Bottom := 50.0
Alignment := vector2{X := 0.0, Y := 1.0}
ZOrder := 5
SizeToContent := false
Widget := IconB
HudCanvas.AddWidget(SlotB)
Pattern 2 — Layered canvas with multiple slots at different z-orders
Stack a background panel behind a foreground label using ZOrder.
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /Fortnite.com/Characters }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }
LayerLabelText<localizes>(S : string) : message = "{S}"
layered_hud_device := class(creative_device):
OnBegin<override>()<suspends> : void =
Players := GetPlayspace().GetPlayers()
if (Player := Players[0], PlayerUI := GetPlayerUI[Player]):
# Background: a colored overlay widget at ZOrder 0
Background := color_block:
DefaultColor := MakeColorFromHex("000000AA") # semi-transparent black
BackSlot := canvas_slot:
Anchors := anchors:
Minimum := vector2{X := 0.5, Y := 0.5}
Maximum := vector2{X := 0.5, Y := 0.5}
Offsets := margin:
Left := -200.0
Top := -40.0
Right := 400.0
Bottom := 80.0
Alignment := vector2{X := 0.0, Y := 0.0}
ZOrder := 0
SizeToContent := false
Widget := Background
# Foreground: text on top at ZOrder 1
Label := text_block:
DefaultText := LayerLabelText("Zone Closing in 10s")
FrontSlot := canvas_slot:
Anchors := anchors:
Minimum := vector2{X := 0.5, Y := 0.5}
Maximum := vector2{X := 0.5, Y := 0.5}
Offsets := margin:
Left := -180.0
Top := -20.0
Right := 360.0
Bottom := 40.0
Alignment := vector2{X := 0.0, Y := 0.0}
ZOrder := 1
SizeToContent := false
Widget := Label
# Both slots initialized via Slots array at construction
LayeredCanvas := canvas:
Slots := array{BackSlot, FrontSlot}
PlayerUI.AddWidget(LayeredCanvas)
Sleep(10.0)
PlayerUI.RemoveWidget(LayeredCanvas)
Pattern 3 — SizeToContent: let the widget determine its own size
When you don't know the pixel dimensions ahead of time (e.g. dynamic text), set SizeToContent := true and use Offsets.Left/Top only for position.
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /Fortnite.com/Characters }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }
AutoSizeText<localizes>(S : string) : message = "{S}"
auto_size_hud_device := class(creative_device):
OnBegin<override>()<suspends> : void =
Players := GetPlayspace().GetPlayers()
if (Player := Players[0], PlayerUI := GetPlayerUI[Player]):
DynamicLabel := text_block:
DefaultText := AutoSizeText("Kills: 0")
# SizeToContent = true: Right/Bottom are IGNORED for sizing.
# Only Left/Top in Offsets matter — they position the top-left corner.
AutoSlot := canvas_slot:
Anchors := anchors:
Minimum := vector2{X := 1.0, Y := 0.0}
Maximum := vector2{X := 1.0, Y := 0.0}
Offsets := margin:
Left := -220.0
Top := 30.0
Right := 0.0
Bottom := 0.0
Alignment := vector2{X := 1.0, Y := 0.0}
ZOrder := 3
SizeToContent := true
Widget := DynamicLabel
KillCanvas := canvas:
Slots := array{AutoSlot}
PlayerUI.AddWidget(KillCanvas)
# Keep alive for the session
Sleep(Inf)
Gotchas
1. Slots is construction-only — use AddWidget/RemoveWidget at runtime
The Slots field on canvas is only read when the canvas is first created. If you try to mutate Slots directly after construction nothing will happen. Always call canvas.AddWidget(slot) or canvas.RemoveWidget(widget) to change children at runtime.
2. SizeToContent flips the meaning of Offsets.Right and Offsets.Bottom
SizeToContent := false→Right= width,Bottom= height in pixels.SizeToContent := true→RightandBottomare ignored; the widget measures itself. Mixing these up is the #1 source of invisible or zero-size widgets.
3. Point anchors vs. stretch anchors
- Point anchor:
Minimum == Maximum(e.g. bothvector2{X:=0.5, Y:=0.5}). The slot is positioned relative to that point;Offsets.Left/Topshift it. - Stretch anchor:
Minimum != Maximum(e.g. Min{0,0}Max{1,1}). The slot stretches to fill the region;Offsetsbecome inset margins from each edge. Accidentally using stretch anchors when you want a fixed-size widget will make it fill the screen.
4. message is not a string
text_block.DefaultText requires a message, not a string. Always declare a <localizes> helper:
MyLabel<localizes>(S : string) : message = "{S}"
There is no StringToMessage function.
5. GetPlayerUI is failable — always guard with if
GetPlayerUI[Player] uses the subscript (failable) form. Wrap it in if (PlayerUI := GetPlayerUI[Player]): or the compiler will reject it.
6. Sleep requires a <suspends> context
Any function that calls Sleep must be marked <suspends>. If you call ShowBannerForPlayer from OnBegin, that's fine because OnBegin is already <suspends>. But if you call it from a synchronous event handler you'll need to spawn it: spawn{ ShowBannerForPlayer(Player) }.