YAML UI Definition
Mateu can render a page defined entirely in a YAML file — no Java class required.
When the framework receives a request for a route that has no matching Java class, it looks for a YAML file in the classpath and uses it to build the component tree automatically.
How it works
Section titled “How it works”- A request arrives for route
my-app/my-page. - Mateu finds no Java view model bound to that route (a
@UImount or aroutes.yamlentry). - Mateu looks for
specs/ui/my-app/my-page.yamlin the classpath. - If found, it deserializes the file into a component tree and renders it.
- If not found, Mateu falls back to “Not found”.
File location
Section titled “File location”Place YAML files under src/main/resources/specs/ui/, mirroring the route structure:
| Route | YAML file |
|---|---|
hello | src/main/resources/specs/ui/hello.yaml |
demo/hello | src/main/resources/specs/ui/demo/hello.yaml |
admin/users/list | src/main/resources/specs/ui/admin/users/list.yaml |
File format
Section titled “File format”Every YAML file must have a root type field that names the component to render. All nested components also need a type.
type: VerticalLayoutspacing: truepadding: truecontent: - type: Text text: "Hello from YAML!" container: h1 - type: Text text: "This page was defined in a YAML file, without any Java class." - type: HorizontalLayout spacing: true content: - type: Button label: "Save" actionId: save buttonStyle: primary - type: Button label: "Cancel" actionId: cancel buttonStyle: secondary - type: FormLayout content: - type: FormField id: name label: "Name" dataType: string required: true - type: FormField id: email label: "Email" dataType: string stereotype: email - type: FormField id: birthDate label: "Birth date" dataType: datePage type — pageType:
Section titled “Page type — pageType:”A page has a coarse page type — landing, collection, detail, form, process or
dashboard (the Redwood page-template families). It sets the page’s default width (form, process
and landing pages are capped; collection, detail and dashboard pages defer to their content) and is
stamped on the rendered page as data-page-type. A Java view declares it with
@PageTemplate(PageType.X); a YAML page declares it with a top-level pageType: key, beside
layout: (or beside type: in a bare tree):
pageType: landinglayout: type: VerticalLayout content: - type: HeroSection title: Welcome- Declared, it wins over the type inferred from the content or from a bound
viewModel:— the same precedence as@PageTemplate(and, like the definition’slayout:, over a@PageTemplateon the view model it binds, since the definition is the more specific of the two). - Absent, nothing changes: the type is inferred as before.
- Matched without regard to case; an unknown value is ignored (and logged), so a typo never costs the page.
- It reaches the wire on every path — served by Java, .NET or Python, pre-rendered into a static
bundle, or expanded in the browser from a
specsOnlybundle — and every renderer (Vaadin, Redwood) reads it from there.
The New › Mateu › Page… templates (IntelliJ and VS Code) already write the right one: Welcome and
Hero Search → landing; Listing, Smart Search, To-do List, Calendar, Collection Detail and the
grid-like pages → collection; General/Item Overview, Foldout and Gantt → detail; Dashboard →
dashboard; Wizard step → process; Form → form. The blank layout leaves it to inference.
Page header — type: Form
Section titled “Page header — type: Form”A page’s header lives on its type: Form node: title, subtitle, avatar, header,
toolbar, buttons, footer, noHeader — and every other element a Java page gets from its
annotations and suppliers, now authorable as data:
| Key | Shape | Java twin |
|---|---|---|
overline | text above the title | @Overline / OverlineSupplier |
titlePlaceholder | what the header shows while the title is empty | @TitlePlaceholder |
timestamp | the “last updated” line under the subtitle | @Timestamp field |
badges | list of {label, color} chips | @BadgeInHeader / BadgeSupplier |
kpis | list of {label, value} key facts | @KPI |
peerNav | {prevLabel, prevRoute, nextLabel, nextRoute} | PeerNavigationSupplier |
recordSwitcher | {label, value, type, searchable, disabled, options | optionsSource, route | actionId} | RecordSwitcherSupplier |
banners | list of {theme, title, description, closeable, timeoutSeconds} | @Banner / BannerSupplier |
fabs | list of {icon, label, actionId, order, buttonStyle} | @Fab methods on the page |
type: Formtitle: ${state.name}overline: CustomerstitlePlaceholder: New customer…timestamp: Last updated ${state.updatedAt}badges: - {label: "${state.status}", color: success} # blank once interpolated → hiddenkpis: - {label: Balance, value: "${state.balance}"}peerNav: {prevLabel: Previous, prevRoute: "customers/${state.prevId}", nextLabel: Next, nextRoute: "customers/${state.nextId}"}recordSwitcher: label: Customer value: ${state.id} searchable: true optionsSource: {ref: customers, itemsPath: content, valuePath: id, labelPath: name} route: customers/${state._record} # the picked value is ${state._record}banners: - {theme: WARNING, title: Overdue, description: "Two invoices are overdue", closeable: true}fabs: - {icon: "vaadin:plus", label: New order, actionId: newOrder}content: [...]- Dynamic values use the idioms every YAML text already has:
${state.x}and${data.x}(the route’sdata:source feedsdata), interpolated by the renderer. A badge whose text interpolates to blank is hidden — the same switch a@BadgeInHeaderString field gives. - The record switcher’s entries are either listed (
options: [{value, label, description}]) or fetched by the browser from a REST source (optionsSource:, usually asources.yamlreference, read with itsitemsPath/valuePath/labelPath— like a select field’s options). A pick navigates toroute(with${state._record}= the picked value), or, without a route, dispatchesactionId(default_switchRecord) with the value in_record— a pageactions:entry or a view model method can take it from there. The source is fetched directly by the browser, so aproxy: truesource does not serve a switcher. - Banner themes are
INFO,SUCCESS,WARNING,DANGER(upper case). - FABs dispatch their
actionIdlike any button: an action the page declares inactions:, an action-catalogue entry, or a method of the bound view model. They stack byorder(ties keep the list order);buttonStyledefaults toprimary. - They reach the wire with the same field names a reflected Java page’s header has (
overline,titlePlaceholder,timestamp,badges,kpis,peerNav,switcher,banners,fabs), so every renderer draws them alike — served by Java, .NET or Python, pre-rendered into a static bundle, or expanded in the browser from aspecsOnlybundle. - Redwood draws every one of them, for a YAML page and a Java page alike: KPIs, subtitle,
switcher (listed or from a direct source;
routepicks navigate), banners and FABs, and on itsoj-sppage header the status badge (the first header badge — the rest follow the subtitle, and a badge whose label resolves blank is not drawn), the previous/next arrows (a side with no route is drawn disabled; both empty, no arrows), the timestamp (the header’s own “last updated” element when the value is a date —2026-10-12T10:42— otherwise the text ends the subtitle line), the overline above the title and the title placeholder (dimmed, only while the title resolves empty).
Available types
Section titled “Available types”All component types from the io.mateu.uidl.data package are supported.
Use the exact class name as the type value.
Layout components: VerticalLayout, HorizontalLayout, FormLayout, TabLayout, SplitLayout, MasterDetailLayout, AccordionLayout, BoardLayout, CarouselLayout, Grid, …
Content components: Text, Markdown, Image, Icon, Badge, KPI, Chart, ProgressBar, Anchor, Bpmn, Workflow, …
Input components: FormField, CustomField, FormSection, FormRow, …
Action components: Button, Dialog, ConfirmDialog, …
Navigation: RouteLink, Menu, Directory, Breadcrumbs, …
The complete list of types and their fields is in the JSON Schema:
https://raw.githubusercontent.com/miguelperezcolom/mateu/refs/heads/master/backend/shared/uidl/uidl-schema.json
IntelliSense setup
Section titled “IntelliSense setup”The project ships a JSON Schema at https://raw.githubusercontent.com/miguelperezcolom/mateu/refs/heads/master/backend/shared/uidl/uidl-schema.json.
Connecting your editor to it gives you autocompletion, field validation, and documentation tooltips while editing YAML files.
VS Code
Section titled “VS Code”Option A — workspace settings (recommended)
Install the YAML extension by Red Hat, then add this to .vscode/settings.json:
{ "yaml.schemas": { "./backend/shared/uidl/uidl-schema.json": "src/**/specs/ui/**/*.yaml" }}This activates IntelliSense for every *.yaml file under any specs/ui/ directory in the project.
Option B — per-file comment
Add this comment at the top of any individual YAML file:
# yaml-language-server: $schema=https://raw.githubusercontent.com/miguelperezcolom/mateu/refs/heads/master/backend/shared/uidl/uidl-schema.jsontype: VerticalLayout...Adjust the relative path so it points to uidl-schema.json from the YAML file’s location.
IntelliJ IDEA
Section titled “IntelliJ IDEA”- Open Settings (
⌘,/Ctrl+Alt+S). - Go to Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings.
- Click + to add a new mapping.
- Fill in:
- Name:
Mateu UIDL - Schema file or URL: click the folder icon and select
https://raw.githubusercontent.com/miguelperezcolom/mateu/refs/heads/master/backend/shared/uidl/uidl-schema.json - Schema version:
JSON Schema version 7
- Name:
- Under File path pattern, click + and enter:
specs/ui/**/*.yaml - Click OK.
IntelliJ will now provide autocompletion and validation for all YAML files under specs/ui/.
Tip: If IntelliJ shows a yellow bar saying “Schema is not applied”, make sure the file path pattern matches — use
specs/uiwithout a leading slash.
Partials
Section titled “Partials”A YAML file under specs/ui/partials/ is not a page but a partial — one component, or a list
of them, usable anywhere a component is:
- type: Partial ref: address-blockSee Partials.
layoutDelta: — keep inference alive
Section titled “layoutDelta: — keep inference alive”A page can carry a layoutDelta: instead of a layout:. The difference matters more than it
looks:
viewModel: com.acme.ContactlayoutDelta: order: [email, name] hidden: [internalNote] overrides: name: { label: "Full name", colspan: 2 }Vocabulary: the key that names the view model is
viewModel:(the canonical noun of the 5-noun model). The oldermodelView:is still accepted as a deprecated alias so existing definitions keep working; preferviewModel:in new files.
A layout: is a snapshot: it takes the screen out of the inference regime for good, because
explicit always wins. Add a field to the model afterwards and it does not appear; rename one and the
layout points at a ghost.
A layoutDelta: records what a human decided, anchored to field ids. Inference still runs on
every request and the delta is re-applied on top, so a field the model grows later lands in its
inferred place, and one it loses is an entry that simply matches nothing.
What a delta deliberately cannot express is arbitrary restructuring — “wrap these two in a card inside a tab”. Anchoring to ids is exactly what makes it survive a model change; a delta that could rebuild the tree freely would be a snapshot with a different name.
The delta is re-applied to the tree inference produced on every request — that is the whole
difference from a layout:, which is computed once and then stands in for the model forever.
Three parts, three rules
Section titled “Three parts, three rules”An inferred tree may spread the model’s fields over sections, tabs or rows, so:
hidden | removes that field wherever it sits |
overrides | apply to that field wherever it sits |
order | reorders the fields within each container that holds them, leaving non-field siblings in their slots |
A delta cannot move a field between containers, and cannot restructure the tree. That limit is not an omission — it is the reason a delta survives a model change at all.
The visual editor writes these
Section titled “The visual editor writes these”Drag a field in the editor and it saves a layoutDelta:, not a snapshot. Two consequences worth
knowing:
- Opening a screen and changing nothing writes
layoutDelta: {}— an empty delta, which costs the screen nothing. Using the editor must not, by itself, end a page’s inference. - Some edits cannot be a delta. Add a
Text, wrap two fields in a card, set a stereotype by hand — none of that is expressible as “these fields, in this order, with these tweaks”. The editor then falls back to writing alayout:and shows a snapshot badge instead of a delta one, so the moment a screen leaves inference is visible rather than silent.
An existing layout: page migrates by itself: open it in the editor, rearrange, and if the layout
is a flat run of the model’s own fields the next save comes back as a delta.
Hybrid: YAML layout + Java logic with @UISpec
Section titled “Hybrid: YAML layout + Java logic with @UISpec”A pure YAML file has no Java class, so it cannot run server-side logic. A pure Java ComponentTreeSupplier has no YAML file, so the layout is hard-coded.
@UISpec is the middle ground: a Java ViewModel class whose component tree is loaded from a YAML file. The Java class provides state (fields), actions (methods), rules, and validations; the YAML file defines the layout.
@UISpec("specs/ui/customer-form.yaml")public class CustomerFormViewModel {
public String name; public String email;
@Button public void save() { ... }}type: Routesroutes: - route: customer-form viewModel: com.example.CustomerFormViewModelThe YAML file at src/main/resources/specs/ui/customer-form.yaml defines the component tree rendered for this class. Mateu combines the two: layout from YAML, behaviour from Java.
See @UISpec for the full reference.
When to use YAML vs Java
Section titled “When to use YAML vs Java”| Situation | Recommendation |
|---|---|
| Static informational page (about, help, landing) | Pure YAML file |
| Page with server-side logic, computed fields, actions | @UISpec or ComponentTreeSupplier |
| Prototype or low-code scenario | Pure YAML file |
| Page that needs dependency injection or DB access | @UISpec or Java class |
| Designer-managed layout, developer-managed logic | @UISpec |
| Fully dynamic / programmatic layout | ComponentTreeSupplier |
YAML pages and Java pages can coexist freely in the same application.