Reference · Updated 2552.07.26.14.59

SL Button Widget — Intended Design & Authoring

How the menu button is meant to be built and used. Written because the split between the C++ base class, the styled widget asset, the bound label widget, and the per-instance capti

How the menu button is meant to be built and used. Written because the split between the C++ base class, the styled widget asset, the bound label widget, and the per-instance caption is easy to trip over — most people’s first instinct (type text straight into the text block) fights the design and the text keeps vanishing.

Read alongside: Docs/UISystem.md (CommonUI plumbing + footguns), Docs/MenuSystemPlan.md §5.0a
(button asset prerequisite in the build order), Docs/CodexHandoff_WidgetTrees.md (step-by-step editor
spec for the trees).

#1. The three layers (and why there are three)

A menu button is not one thing. It’s a stack, each layer with one job:

LayerWhat it isJobYou place / edit it…
USLButtonBaseC++ class (Public/UI/SLButtonBase.h)Behavior: label sync, uppercase, text-style sync, focus/hover → description. No visuals, no style, no label of its own.Never directly. It’s Abstract.
WBP_SL_ButtonWidget BP asset (/Game/SystemLink/UI/Menus/), parented to USLButtonBaseThe thing menus place. Carries the Style (Style_ButtonMedium) and owns the label text block.Author its tree once.
A placed instanceA WBP_SL_Button dropped into WBP_SL_PauseMenu, etc.One concrete button on one screen.Set its caption per screen.

Why not collapse them? Because USLButtonBase’s CDO has Style = None and no internal label — drop the raw C++ class into a menu and you get an invisible, label-less widget (CommonUI even warns “must have a Style set”). The style + label live on WBP_SL_Button so every menu reuses one styled, labeled button and never re-solves that. See MenuSystemPlan.md §5.0a.


#2. How the label actually flows

Two named pieces, and they are not the same:

  • ButtonText — a CommonTextBlock inside WBP_SL_Button’s tree, named exactly ButtonText.
  • This is the widget that draws the caption. It’s bound in C++ via UPROPERTY(meta=(BindWidgetOptional)).

- ButtonDisplayText — an FText property on the button (SystemLinkUI category), set **per placed

instance**. This is the caption value.

The wiring that connects them lives in USLButtonBase:


void USLButtonBase::NativePreConstruct()

{

    Super::NativePreConstruct();

    SetButtonText(ButtonDisplayText);   // pushes the property → into the ButtonText block

}

SetButtonText applies bUseUpperCaseForButtonText and the current CommonTextStyle, then calls ButtonText->SetText(...). So the flow is always:


per-instance ButtonDisplayText  ──(NativePreConstruct)──►  ButtonText block  ──►  on screen

ButtonDisplayText is the single source of truth for the caption. That’s deliberate — it’s also what the runtime SetButtonText(...) path and the uppercase/text-style logic assume.


#3. The authoring workflow (do it this way)

A. Build WBP_SL_Button’s tree — once.

  1. Add a CommonTextBlock to the tree, named exactly ButtonText (case-sensitive).
  1. Center it (Horizontal + Vertical Alignment = Center).
  1. Do not type a caption into its Text field. Leave it empty. (Set Style on the button itself if not
  2. already — Style_ButtonMedium.)

B. Per screen, per button — set the caption on the button, not the block.

- Select the placed WBP_SL_Button (the root, i.e. the USLButtonBase), find **SystemLinkUI →

Button Display Text**, and type the caption there (Resume, Settings, …).

  • Optionally toggle Use Upper Case For Button Text and set Button Description Text (surfaces on
  • focus and hover — controller-reachable, footgun 6.19).

That’s it. The caption shows in the designer preview and at runtime, and it won’t get wiped.


#4. Gotchas (the ones that actually bite)

  • “My default text keeps disappearing when I type it into the text block.” Expected. NativePreConstruct
  • runs continuously in the designer and overwrites ButtonText with ButtonDisplayText (empty by default), so it stomps anything you typed into the block’s Text field. Fix: set Button Display Text on the button instead. The block is a render target, not where you author copy. (This doc exists because of exactly this confusion — 2026-07-08.)

  • Name the block ButtonText, nothing else. BindWidgetOptional binds by name with no warning on a
  • mismatch — a block named Text_Label, Label, Text, etc. binds to null, SetButtonText early-returns, and the button is silently blank forever. (Footgun bindwidgetoptional_silent_noop; same class of bug that bit GrenadeIndicator.) ⚠ Note: an earlier CurrentFocus.md “NEXT” step said add a Text_Label block — that was wrong; it must be ButtonText to match the C++ property.

  • BindWidgetOptional, not BindWidget. The bind is optional on purpose — a button with no label (icon-only)
  • is legal. The cost is you don’t get a compile error when the name’s wrong; you get silence. Match the name exactly and it’s a non-issue.

  • Never place the raw USLButtonBase (or CDO) in a menu. Style = None + no label = invisible widget +
  • CommonUI style warning. Menus place WBP_SL_Button, always.

  • Runtime caption changes go through SetButtonText(FText) (BlueprintCallable), not by poking the block —
  • it keeps uppercase + text-style in sync. Setting ButtonDisplayText then re-running construction also works.


#5. Quick reference

I want to…Do this
Set a button’s labelSet Button Display Text on the placed WBP_SL_Button
Change a label at runtimeCall SetButtonText (BlueprintCallable) on the button
Make labels all-capsCheck Use Upper Case For Button Text on the button
Add a focus/hover descriptionSet Button Description Text; bind OnDescriptionChanged from a shared description panel
Add a new styled button variantDuplicate WBP_SL_Button, change its Style (e.g. Style_ButtonLarge)
Name the label blockExactly ButtonText — case-sensitive

Files: Public/UI/SLButtonBase.h · Private/UI/SLButtonBase.cpp · asset /Game/SystemLink/UI/Menus/WBP_SL_Button. </content> </invoke>