Version 1.2.0 · Unreal Engine 5.8

UMGForge

HTML/CSS designs as native UMG Widget Blueprints
Requirements Quick Start Layout Format Attributes What Converts Troubleshooting FAQ
Docs · UMGForge for Unreal Engine 5.8
Product Converter Studio
Requirements
EngineUnreal Engine 5.8
Editor platformWindows
Output platformsAny platform UMG supports
Required pluginsNone
NetworkNever
For HTML conversion onlyNode.js 22+ and any Chromium browser already installed (Chrome, Edge, Brave, Vivaldi)
Importing a .json layout needs neither Node nor a browser. They are only used to turn an HTML page into a layout, and the panel names either one by hand if it is missing.
Install
  1. Copy the UMGForge folder into your project’s Plugins/ directory.
  2. Open the project. If prompted to rebuild, accept.
  3. Confirm it is enabled under Edit › Plugins › Editor.

The panel is at Tools › Widget From JSON, in the Window menu, and on the level editor toolbar. It can also be opened from the console with UMGForge.Open, optionally with a file path.

Quick Start

1. The panel

Open the panel, choose an .html design or a .json layout, and press Create Widget. An HTML design is converted the moment you choose it, so its warnings and its font list are in front of you before anything is written into your project.

No install needed to try it. The online converter runs the same converter core in your browser, so you can turn a design into a layout without downloading anything, and nothing is uploaded:

Three things need a browser driven from outside, so the online page cannot do them: capturing effects as textures, pulling the page's own font files, and re-measuring at other viewports to infer anchors. All three are in the panel above - baking is on by default, fonts are a checkbox, and viewports go in the aspect-ratio box - and all three are on the command line below.

2. Command line

node Plugins/UMGForge/Resources/Converter/cli/convert.js design.html -o design.json
OptionMeaning
-o, --outWhere to write. Default: the input path with a .json extension.
--viewport WxHThe viewport to measure at. Default 1920x1080.
--probe W1xH1,…Extra viewports to read anchors from.
--fonts <dir>Write the page’s real font files out for import.
--shot <png>Reference render at the conversion viewport.
--scale-box <mode>fit, fill, fitx or fity - wraps the layout in a ScaleBox.
--no-bakeTurn off capturing effects Slate cannot draw.
--blueprint <path>UE asset path for the widget, overriding data-ue-blueprint in the HTML.
--bake-scale <n>Capture baked effects at n times scale. Default 2, max 4.
--bake-max <n>Refuse a capture larger than n px per side. Default 2048.
--browser <exe>A specific Chromium, if the search misses.
--timeout <ms>Give up after this long. Default 30000.
--keep-profileLeave the temporary browser profile on disk, for debugging.
--strictExit 2 if the converter logged any warning. For CI.
-q, --quietErrors only.

Exit codes: 0 ok, 1 failed, 2 warnings under --strict.

3. Python or Blueprint

opts = unreal.UMGForgeOptions() opts.set_editor_property("override_asset_path", "/Game/UI/WBP_Screen") opts.set_editor_property("import_fonts", True) r = unreal.UMGForgeImporter.import_with_options(json_path, opts) print(r.success, r.widgets_created, r.properties_set) for m in r.messages: print(m.severity, m.text)

The result field is success, not b_success - Unreal strips the b. unreal.UMGForgeConverter.import_from_html(...) does the whole chain in one call. Both are Blueprint callable too, under the category UMGForge.

Headless, with no editor window:

UnrealEditor-Cmd.exe <uproject> -run=pythonscript -script=import.py -unattended -nullrhi
The Layout Format

HTML is the authoring format, JSON is the contract, UMG is the output. The JSON is diffable, reviewable in a pull request, and perfectly reasonable to write by hand.

{ "blueprint": "/Game/UI/WBP_Example", "parentClass": "/Script/MyGame.MyBaseWidget", "root": { "name": "Root", "type": "VerticalBox", "children": [ { "name": "TitleText", "type": "TextBlock", "properties": { "Text": "HELLO", "AutoWrapText": true }, "slot": { "Padding": {"Left":0,"Top":6,"Right":0,"Bottom":2} } } ] } }
FieldMeaning
blueprintDestination package path. The panel and the API can override it.
parentClassOptional. Its BindWidget properties are what name matches against.
nameUnique, and a legal identifier. This is the binding mechanism.
typeVerticalBox, a full class path, or a Blueprint path (the _C is added for you).
propertiesReflected property names, matched case-insensitively.
slotApplied to the slot - a different object whose class depends on the parent.
namedSlotWhich named slot to sit in, for a parent that takes children by name.
isVariableDefaults true. A non-variable widget cannot be reached from C++ or the graph.
Author Attributes

Put these on elements in your HTML to control the conversion.

AttributeEffect
data-ue-nameThe UMG widget name. This is what a C++ BindWidget matches on.
data-ue-classForce a specific UMG class for this element.
data-ue-variable"false" to keep the widget out of the generated class.
data-ue-blueprintOn the root element: the destination asset path.
data-ue-font-mapOn the root element: a JSON map of family name to font asset path.
data-umg-bakeCapture this element as a texture whatever the CSS says.
data-umg-bake="full"Capture the element and its children, dropping them from the tree.
data-umg-no-bakeNever capture this element.
Fonts

Sizes need no setup. A partial struct write only touches the fields the layout names, so a first import is legible immediately at the right sizes. Getting the right face has two routes.

Capture the real files

Tick Capture the page’s font files in the panel, or pass --fonts <dir> on the command line. The actual TrueType is written out of the browser that rendered the page; ticking Import the font files the layout captured on the import side turns each family into one Font asset with a typeface per weight, with nothing to pick by hand.

This matters more than it sounds. A substituted face has different advance widths, so the text wraps at different words, the block is a line taller, and everything below it moves down.

Or map them yourself

The panel lists every family the layout asks for and gives you an asset picker per family, limited to UFont. Choices are remembered per project. A family left unmapped keeps the widget’s own font rather than resolving to null.

Gradients

UMG has no gradient widget. Slate has been able to draw one the whole time - it simply is not exposed - so the plugin ships a Gradient Panel: a real widget that draws the ramp with every stop and its corner radii, and holds its content on top. Nothing to bake, nothing to author, recolourable in the designer like any other property.

Slate’s gradient draw takes an orientation rather than an angle, so a 135° CSS gradient snaps to the nearer axis and the log says so.

An element that already owns a brush - a Border, Image or ProgressBar - cannot be swapped for the gradient panel, so those use a material instead. One ships with the plugin at /UMGForge/Materials/M_UMGForge_Gradient, and the panel offers it automatically whenever a layout needs it. Copy it into your own content and pick the copy to restyle; nothing hard-codes the path.

Staying Responsive

Only sizes the CSS declares become fixed. A computed width that came from filling a parent is deliberately ignored - that is measured geometry, and it must stay a Fill in UMG or the layout stops responding to resolution.

A layout is converted from one snapshot, and a single snapshot cannot tell a left-anchored panel from a centred one: at 1920 wide they sit in the same place. Name the other viewports you care about and the page is asked instead:

node cli/convert.js design.html -o design.json --probe 1440x1080,2560x1080

Every rewrite is checked against the design viewport and reverted if it would move anything there, so this can only improve the ratios you did not draw for. For a design that should not reflow at all, --scale-box fit wraps the layout in a UScaleBox - usually right for a full-screen menu and wrong for a HUD.

Checking the Result

Render any imported widget to a PNG at any resolution and put it beside the page it came from. No PIE, no viewport:

cls = unreal.load_object(None, "/Game/UI/WBP_Screen.WBP_Screen_C") unreal.UMGForgeFidelity.render_widget_to_file(cls, unreal.Vector2D(1920, 1080), "D:/out.png")
Use the resolution the design was converted at. The editor’s play viewport is whatever shape the docked panels leave behind, so a corner-anchored screen genuinely lays out differently there - and comparing that against the design scores correct behaviour as failure.
What Converts, and What Does Not

Widget types

VerticalBox, HorizontalBox, WrapBox, Overlay, CanvasPanel, ScrollBox, UniformGridPanel, GridPanel, Border, SizeBox, Button, CheckBox, TextBlock, RichTextBlock, Image, ProgressBar, EditableTextBox, MultiLineEditableTextBox, Spacer, Slider, ComboBoxString, ExpandableArea, plus the bundled Gradient Panel.

Which widget your name lands on

One element usually becomes several widgets. A panel with a background, a border, a backdrop blur and a declared size arrives as a stack:

MyPanel SizeBox the declared size, and what the parent positions MyPanel_BG Border background and border brush MyPanel_Blur BackgroundBlur backdrop-filter MyPanel_Content VerticalBox the element's own children

The name goes on the outermost widget, because that is the one a parent positions and the one you mean when you name an element. BindWidget MyPanel gets the SizeBox, so SetVisibility(Collapsed) takes the frame with it instead of leaving the border and background painting. Bind MyPanel_Content when you want the contents and not the chrome around them.

Carried across

Not carried across

WhatWhy, and what to do
AnimationsCSS transitions and @keyframes do not become Widget Animations; build motion in UMG. Hover, pressed and disabled states do come across on buttons as real brush states. Every animation is settled before the page is measured, so what you import is the design at rest rather than whichever frame the browser was on: a finite animation is run to its end, so an element written opacity: 0 with a forwards fade arrives visible, and an endless one is cancelled back to its base style. The same file converts to the same widgets every time.
box-shadowSlate brushes have no drop shadow. It captures to a texture placed behind the element, or is reported by name.
Angled gradientsSnap to the nearest axis, and the log says so. A radial gradient draws as a linear ramp; when it covers more than a quarter of the viewport that is called out separately, because at that size it changes the brightness of the screen rather than just the shape of the ramp.
ProgressBar fillOne colour. A gradient on a bar flattens to its first stop; the track colour and radius do come across.
color-mix() and relative colourReported rather than guessed.
Per-side bordersA Slate brush has one outline, so four different borders use the first painted side.
JavaScript behaviourStatic markup in, widgets out. What your buttons do is yours to write in Blueprint.
Fixed grid track widthsUGridPanel has no fixed-width track: a fill coefficient is a share of the leftover space, and 0 means "size this track to its content". A 380px sidebar ends up as wide as what sits in it. Every non-fr track is named in the log with its declared size, so give that cell's content an explicit width when it has to be exact.
Form control stylingA <select> keeps its options, its font and its text colour, but its box is an FComboBoxStyle brush rather than a colour, so the background is reported rather than carried. A range input maps accent-color to SliderHandleColor when the page sets one; when it does not, the colour in the browser is the browser’s own default and the converter says so rather than inventing it.
CommonUIPlain UMG only.
Live URL captureLocal files only.
Re-importing

Bringing a design in again rebuilds the widget tree in place. The asset keeps its identity, its parent class and its graphs, so every reference to it from other Blueprints stays valid.

Manual edits to the imported layout are overwritten on re-import. Blueprint graphs and widget names survive, so bindings keep resolving - but if you rearrange the tree by hand, do it in the HTML instead.
Troubleshooting
SymptomCause
“Node.js was not found”The HTML converter runs on Node 22+. Install it from nodejs.org and reopen the editor, or set the Node path in the panel. Importing a .json layout needs none of this.
“no Chromium found”Pass --browser with a path to any Chrome, Edge, Brave or Vivaldi executable.
Every glyph is a magenta boxA RichTextBlock with tagged <span> runs and no Text Style Set. Make a DataTable with RichTextStyleRow as its row struct, give it a Default row, and pick it on import.
“‘X’ is not a property of Y”A hand-written layout naming a property the class does not have. The message names the class; check the spelling against it.
“‘X’ appears twice”Widget names must be unique - they are what BindWidget matches on.
A required binding is unfilledAdd data-ue-name="ThatName" to the element that should fill it. If a name is nearly right, the message names the likely typo.
The import says it failedRead the errors above it. The asset is still saved, but it is half a screen - fix and import again.
FAQ
Does it need a browser inside Unreal?

No. There is no embedded Chromium, no CEF and no WebBrowserWidget dependency. The conversion drives whichever browser is already on the machine as a separate process. That also means there is no Chromium version ceiling - color-mix(), :has(), container queries and position: sticky work because your browser supports them.

Does it convert animations?

No. CSS transitions and @keyframes do not become Widget Animations. Hover, pressed and disabled states do come across on buttons as real brush states. If one-click animation import is what you need, this is not the tool.

Will the imported screen still be responsive?

Yes - that is the main design decision. The CSS layout model is translated rather than measured, so flex: 1 becomes a slot that fills and percentages become anchors. Resize the panel in the designer and the screen reflows. Use --probe to read anchors from other aspect ratios.

Do I have to write the HTML a particular way?

No. It reads computed styles and never looks at class names, so Tailwind, shadcn/ui and anything generated by an AI tool work unmodified. There is no prompt template to follow. The data-ue-* attributes are optional and only used to name widgets or override a decision.

Can it run in CI, without an editor?

Yes. The converter is a Node script that needs no Unreal at all, and the importer runs in a -run=pythonscript commandlet. The whole chain from page to Widget Blueprint runs from a build script.

What happens to my Blueprint graphs when I re-import?

They survive. The widget tree is rebuilt in place, so the asset keeps its identity, its parent class and its graphs. Widget names stay stable, so bindings keep resolving. Only manual edits to the imported layout are overwritten.

How accurate is the conversion, really?

Depends entirely on which CSS a design leans on, which is why no fixed percentage is quoted. Three screens built to test it measured 2.9%, 1.6% and 4.0% differing pixels against the browser. More usefully, the plugin ships the renderer that produces those numbers, so you can measure your own designs rather than take a claim on trust.

Can I use it without HTML at all?

Yes. The JSON layout format is the contract, and it is meant to be hand-authorable. Point the importer at a .json file and neither Node nor a browser is involved.

Does anything ship into my packaged game?

One small Runtime module carrying the Gradient Panel, because a gradient has to be drawn at runtime. Everything else is editor-only. Imports are one-shot; the output is plain UMG.

Which engine versions are supported?

Unreal Engine 5.8. The editor side is Windows; the widgets it produces are plain UMG and run on any platform UMG supports.

A design of mine did not convert cleanly. What now?

Send the file. The converter only improves by meeting pages it has not seen, so a design that breaks it is the most useful thing you can send.