| Engine | Unreal Engine 5.8 |
| Editor platform | Windows |
| Output platforms | Any platform UMG supports |
| Required plugins | None |
| Network | Never |
| For HTML conversion only | Node.js 22+ and any Chromium browser already installed (Chrome, Edge, Brave, Vivaldi) |
.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.
- Copy the
UMGForgefolder into your project’sPlugins/directory. - Open the project. If prompted to rebuild, accept.
- 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.
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.
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
| Option | Meaning |
|---|---|
| -o, --out | Where to write. Default: the input path with a .json extension. |
| --viewport WxH | The 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-bake | Turn 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-profile | Leave the temporary browser profile on disk, for debugging. |
| --strict | Exit 2 if the converter logged any warning. For CI. |
| -q, --quiet | Errors only. |
Exit codes: 0 ok, 1 failed, 2 warnings under --strict.
3. Python or Blueprint
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:
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.
| Field | Meaning |
|---|---|
| blueprint | Destination package path. The panel and the API can override it. |
| parentClass | Optional. Its BindWidget properties are what name matches against. |
| name | Unique, and a legal identifier. This is the binding mechanism. |
| type | VerticalBox, a full class path, or a Blueprint path (the _C is added for you). |
| properties | Reflected property names, matched case-insensitively. |
| slot | Applied to the slot - a different object whose class depends on the parent. |
| namedSlot | Which named slot to sit in, for a parent that takes children by name. |
| isVariable | Defaults true. A non-variable widget cannot be reached from C++ or the graph. |
Put these on elements in your HTML to control the conversion.
| Attribute | Effect |
|---|---|
| data-ue-name | The UMG widget name. This is what a C++ BindWidget matches on. |
| data-ue-class | Force a specific UMG class for this element. |
| data-ue-variable | "false" to keep the widget out of the generated class. |
| data-ue-blueprint | On the root element: the destination asset path. |
| data-ue-font-map | On the root element: a JSON map of family name to font asset path. |
| data-umg-bake | Capture 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-bake | Never capture this element. |
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.
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.
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:
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.
Render any imported widget to a PNG at any resolution and put it beside the page it came from. No PIE, no viewport:
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:
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
- Flex and grid layout, fill rules, padding, margin, gap, alignment
- CSS Grid with unequal tracks and explicit
grid-column/grid-rowplacement, including spans, as a real GridPanel with per-track fill; an evenly divided grid stays a UniformGridPanel - Absolute positioning as canvas anchors, not fixed offsets
- Colours including
oklch(),oklab(),color(srgb …), hex andrgb(), converted to linear - Rounded corners and outlines as RoundedBox brushes, including all four button states
- Linear gradients with every stop; radial and conic gradients capture to a texture
- Inline SVG, rasterised at 4x with
currentColorresolved - Font family, size, weight, style, letter spacing and line height
- Semantic form controls with their values
Not carried across
| What | Why, and what to do |
|---|---|
| Animations | CSS 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-shadow | Slate brushes have no drop shadow. It captures to a texture placed behind the element, or is reported by name. |
| Angled gradients | Snap 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 fill | One colour. A gradient on a bar flattens to its first stop; the track colour and radius do come across. |
| color-mix() and relative colour | Reported rather than guessed. |
| Per-side borders | A Slate brush has one outline, so four different borders use the first painted side. |
| JavaScript behaviour | Static markup in, widgets out. What your buttons do is yours to write in Blueprint. |
| Fixed grid track widths | UGridPanel 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 styling | A <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. |
| CommonUI | Plain UMG only. |
| Live URL capture | Local files only. |
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.
| Symptom | Cause |
|---|---|
| “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 box | A 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 unfilled | Add 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 failed | Read the errors above it. The asset is still saved, but it is half a screen - fix and import again. |
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.
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.
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.
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.
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.
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.
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.
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.
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.
Unreal Engine 5.8. The editor side is Windows; the widgets it produces are plain UMG and run on any platform UMG supports.
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.