# Mateu — full documentation
> Every page of https://mateu.io, in one file, generated from the docs at build time (Mateu 3.0-beta.7).
> The index with links is https://mateu.io/llms.txt; the curated AI references are https://mateu.io/mateu-ai-compact.md
> and https://mateu.io/mateu-ai-full.md; Claude Code users can install the skills with
> `/plugin marketplace add miguelperezcolom/mateu` and `/plugin install mateu@mateu`.
---
# What is Mateu?
URL: https://mateu.io/mateu-about/what-is-mateu/
> Mateu is a way to define business UIs agnostically: one model, authored as YAML, in a visual editor or in code (Java, C#, Python), rendered on web, mobile and desktop, served by any backend or by none.
Mateu is a way to define business user interfaces **once, independently of how they are written,
where they render and what serves them**.
You describe a screen — its fields, listings, actions, navigation — as a **model**. That model can
be authored three ways, and all three produce exactly the same thing:
- **as YAML** files in your repository,
- **in the visual editor** (IntelliJ or VS Code), which writes those YAML files for you,
- **in code**: Java (on Spring Boot MVC or WebFlux, Quarkus, Micronaut or Helidon MP), C# (ASP.NET
Core) or Python (FastAPI).
From that model Mateu derives the screens. The same model then:
- **renders anywhere**: in the browser with Vaadin or Oracle Redwood (including inside an existing
Oracle Visual Builder app), on phones through the Mateu mobile app, and on the desktop inside
IntelliJ;
- **is served by any backend, or by none**: a server in any of the languages above, or a static
bundle on a CDN that calls your REST APIs straight from the browser;
- **composes**: UIs owned by different teams and domains, on different stacks, can be federated
into one console.
A backend is an option, not the definition. Many Mateu UIs have one; some have none.
## One model, many paths
```mermaid
flowchart LR
subgraph Author
Y[YAML files]
V[Visual editor]
J[Java]
C[C#]
P[Python]
end
V -->|writes| Y
Y --> M((One model
wire JSON))
J --> M
C --> M
P --> M
subgraph Render
W1[Web: Vaadin]
W2[Web: Redwood / Visual Builder]
MO[Mobile: Mateu app]
D[Desktop: IntelliJ]
end
M --> W1
M --> W2
M --> MO
M --> D
subgraph Serve
B[A backend
Java · .NET · Python]
S[A static bundle
on a CDN]
F[A federated console
of several UIs]
end
M -.-> B
M -.-> S
M -.-> F
```
The model has a published JSON Schema, a versioned [wire specification](/reference/wire-specification/)
and conformance tests that hold the three server implementations to the same output. That is what
makes the paths interchangeable rather than similar. See [The model](/mateu-about/the-model/).
## The shortest example
The same small screen in YAML and in Java (C# and Python are side by side in
[Choose your path](/start-here/choose-your-path/)):
```yaml
# specs/ui/products.yaml — no code
type: Listing
title: Products
searchable: true
rowsSource:
ref: products
columns:
- {type: GridColumn, id: name, label: Name}
- {type: GridColumn, id: status, label: Status, dataType: status}
```
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
```java
// Java — a full CRUD over a record
@UI("/products")
public class Products extends AutoCrud {
@Override
public CrudStore store() {
return new ProductStore(); // your CrudStore
}
}
```
The Java class gives you a searchable list at `/products`, a detail view at `/products/:id`, an
edit form at `/products/:id/edit`, a creation form at `/products/new`, validation and navigation.
No frontend project, no per-screen REST controller.
## The core idea
Most business applications define the same concepts twice:
```text
backend model → API contract → frontend model → frontend validation → UI state
```
Mateu removes the duplicated half:
```text
model (YAML · Java · C# · Python) → Mateu wire model → browser · phone · desktop
```
The model says **what** a screen is. The renderer decides **how it looks**. The deployment decides
**where it runs**.
## What Mateu is not
- **Not a frontend framework** or a React alternative. You don't write components; you declare
screens.
- **Not a stateful server-side rendering framework** (like JSF or Wicket). When there is a server,
each request instantiates the view model, applies the action and returns the result — no
sessions, no sticky routing.
- **Not a low-code platform in someone else's cloud.** There is a visual editor and screens can be
plain YAML, but a Mateu UI is **files in your repository**, going through your pull requests,
your build and your release. See [Mateu vs visual builders](/mateu-about/comparison-low-code/).
- **Not a code generator you run once and abandon.** The model is interpreted at runtime (or
exported at build time for a static bundle); there is no generated frontend to maintain.
## When to reach for Mateu
- admin panels, backoffices and internal tools;
- enterprise workflow UIs and control planes;
- distributed systems where each service or domain owns its UI and a console composes them;
- teams that want screens over existing APIs without starting a frontend project;
- any application where a separate SPA costs more than it is worth.
## Next
- [Choose your path](/start-here/choose-your-path/) — authoring, rendering and deployment options
- [Concepts](/concepts/) — the model, routes, data sources, actions, federation
- [The Mateu Way](/the-mateu-way/) — every screen is one of six families
- [Why Mateu](/mateu-about/why-mateu/) — the problem it solves and what you gain
- [How Mateu works](/mateu-about/how-mateu-works/) — the request cycle when a server is involved
---
# Choose your path
URL: https://mateu.io/start-here/choose-your-path/
> Pick how you author a Mateu UI (YAML, the visual editor, or code in Java, C# or Python), where it renders and how it is served — the choices are independent.
import { Tabs, TabItem } from '@astrojs/starlight/components';
A Mateu UI is **one model**. You make three choices, and none of them constrains the others:
1. **How you author it**: YAML files, the visual editor (which writes those files), or code in Java,
C# or Python.
2. **Where it renders**: a web browser (Vaadin or Oracle Redwood / Visual Builder), the Mateu mobile
app, or IntelliJ on the desktop.
3. **How it is served**: by a backend (five Java frameworks, ASP.NET Core or FastAPI), or as a
static bundle on a CDN with no backend at all.
You can change your mind about (2) and (3) without touching what you wrote in (1).
## The same screen, three ways
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
A product listing with search and a status column, authored three ways. All three produce the same
wire model and render identically in every renderer.
```yaml
# specs/ui/products.yaml
type: Listing
title: Products
searchable: true
rowsSource:
ref: products # a named endpoint in sources.yaml
columns:
- {type: GridColumn, id: id, label: Id, identifier: true}
- {type: GridColumn, id: name, label: Name}
- {type: GridColumn, id: price, label: Price, stereotype: money}
- {type: GridColumn, id: status, label: Status, dataType: status}
```
```yaml
# specs/ui/routes.yaml
type: Routes
routes:
- route: products
layout: products.yaml
```
The rows come from a REST endpoint declared once in `sources.yaml` (with sample data until the API
exists). The [visual editor](/define/yaml/visual-editor/) writes these files for you. Served by a
backend, or bundled into static files.
```java
public record Product(@NotEmpty String id, @NotEmpty String name,
@Min(0) @Stereotype(FieldStereotype.money) double price,
@NotNull ProductStatus status)
implements Identifiable {}
@UI("/products")
@Title("Products")
public class Products extends AutoCrud {
@Override
public CrudStore store() {
return ProductStore.INSTANCE; // where the rows live: memory, JPA, a REST client…
}
}
```
Runs on Spring Boot (MVC or WebFlux), Quarkus, Micronaut or Helidon MP. Beyond the listing you
also get create, detail, edit and delete, derived from the record.
```csharp
public class Product
{
[Required] public string Id { get; set; } = "";
[Required] public string Name { get; set; } = "";
[Range(0, double.MaxValue), Money] public double Price { get; set; }
public ProductStatus Status { get; set; }
}
[UI("products"), Title("Products")]
public class Products : Crud
{
public override IEnumerable Fetch(string? search) => Store.Search(search);
public override Product? Get(string id) => Store.Get(id);
public override void Save(Product p) => Store.Save(p);
public override void Delete(string id) => Store.Delete(id);
}
```
Runs on ASP.NET Core (`dotnet add package Mateu.AspNetCore --prerelease`).
```python
class Product:
id: Annotated[str, Required()] = ""
name: Annotated[str, Required()] = ""
price: Annotated[float, Money()] = 0.0
status: ProductStatus = ProductStatus.Available
@ui("products")
@title("Products")
class Products(Crud[Product]):
def fetch(self, search): return store.search(search)
def get(self, id): return store.get(id)
def save(self, entity): store.save(entity)
def delete(self, id): store.delete(id)
```
Runs on FastAPI (`pip install "mateu-ui[all]"`).
The difference is not in what you can show but in **where the behaviour lives**. In YAML the screen
reads and writes REST endpoints directly (or through a proxy, so secrets stay on a server). In code
the screen owns its data access and can run any logic on the server. You can
[mix the two](/define/mixing/): layout in YAML, behaviour in code.
Whichever path you take, the UI is made of the same building blocks: a mount, an app shell and
menu, routes, pages, partials, actions, data sources and state, plus field types, translations,
environments, access rules and project settings. Read
[What a Mateu UI is made of](/concepts/building-blocks/) for the map, with the same product
catalogue as its running example. The [UX guide](/ux/) then shows what they look like on screen:
the app shell, the six page types and the component catalogue.
## Which path fits you
| You are… | Start with |
|---|---|
| Building screens over APIs that already exist, without writing code | [Quickstart: YAML](/define/yaml/quickstart/), then the [visual editor](/define/yaml/visual-editor/) |
| A Java team (Spring Boot, Quarkus, Micronaut, Helidon) | [Quickstart: Java](/java-user-manual/start-here/quickstart/) |
| A .NET team | [C#](/csharp-user-manual/) |
| A Python team | [Python](/python-user-manual/) |
| Designing in Figma first | [Figma design-to-code](/design-systems/figma/) |
| Composing UIs owned by several teams into one console | [UI federation](/mateu-about/ui-federation/) |
## Three front doors to a new project
- **IntelliJ IDEA**: File › New › Project… › **Mateu**. **VS Code**: command palette › **Mateu: New
Project…**. Both ask how you author (code, code + YAML, YAML served by a backend, YAML static) and
which runtime.
- **Maven archetype**: `mvn archetype:generate -DarchetypeGroupId=io.mateu
-DarchetypeArtifactId=mateu-archetype -DarchetypeVersion= -DgroupId=com.acme
-DartifactId=my-app` (add `-Dauthoring=yaml` or `-Dauthoring=static` for the YAML flavours). See
[Create your project](/java-create-your-project/).
- **A starter**: the repository's
[`starters/`](https://github.com/miguelperezcolom/mateu/tree/master/starters) folder has one
minimal project per runtime (Spring MVC, WebFlux, Quarkus, Micronaut, Helidon MP, .NET, Python,
YAML, static). CI builds and boots every one of them.
## Then: render and serve
- **Renderers**: [Vaadin](/design-systems/vaadin/) is the default web renderer;
[Redwood / Visual Builder](/design-systems/oracle-redwood/) also embeds Mateu screens inside an
existing Visual Builder app; the [Mateu mobile app](/native/react-native/) and
[IntelliJ](/native/) render natively. See [Renderers](/design-systems/).
- **Deployment**: a [backend](/deploy/), a [static bundle](/deploy/static-bundle/), and guides for
[Netlify](/deploy/netlify/), [Kubernetes](/deploy/kubernetes-hetzner-cloudfleet/) and the
[app stores](/deploy/mobile-app-stores/).
---
# Build UIs with AI
URL: https://mateu.io/build-uis-with-ai/
> Generate Mateu UIs with an AI assistant — the Claude Code plugin, AGENTS.md in every project, llms.txt / llms-full.txt, the JSON Schemas that validate generated YAML, and the MCP endpoint.
A Mateu UI is a **model**, not front-end code: a class with annotations, or a few YAML files. That
is the shape an AI assistant generates best — few tokens, a closed vocabulary, and a result that
either validates or does not. This page lists what Mateu gives an assistant so that what it writes
is right the first time.
| You use | Give it | It knows |
|---|---|---|
| Claude Code | the **`mateu` plugin** | the whole API through skills that load on demand |
| any assistant, in a Mateu project | the project's **`AGENTS.md`** (already there) | this project's layout, how to run it, the rules |
| any assistant / chat | **`llms.txt`**, **`llms-full.txt`**, the AI references | the docs, as plain text |
| an assistant writing YAML | the **JSON Schemas** | every key of every file kind, mechanically checked |
| an agent operating a running app | the **MCP endpoint** | the app's screens as tools |
## The Claude Code plugin
The Mateu repository is a Claude Code **plugin marketplace**. Install the plugin once:
```text
/plugin marketplace add miguelperezcolom/mateu
/plugin install mateu@mateu
```
(From a terminal: `claude plugin marketplace add miguelperezcolom/mateu` and
`claude plugin install mateu@mateu`. Pin a release with `miguelperezcolom/mateu#v3.0-beta.7`.)
The plugin carries **skills** — focused guides Claude loads only when the task needs them, so they
cost almost nothing until then. Ask for the work in your own words (*"add a CRUD for suppliers"*,
*"a listing of orders from our REST API, with sample data until it exists"*, *"start a Mateu app on
Quarkus"*) and the right one activates; you can also call one by name, e.g. `/mateu:mateu-yaml`.
| Skill | Use it for |
|---|---|
| `mateu` | Screens in **Java**: `@UI` classes, CRUDs (`AutoCrud`, capability listings), forms and actions, wizards, dashboards and the other archetypes, fluent component trees, app shells, the annotation catalogue and its gotchas. |
| `mateu-screen` | A new screen when you have not named the archetype: three business questions pick the page family, the family picks the class. |
| `mateu-yaml` | UIs as **data**: `type: UI` mounts, `routes.yaml`, page definitions, `sources.yaml` and the other `specs/ui` file kinds, the app shell, partials, the JSON Schemas — and **static bundles** with no backend. |
| `mateu-dotnet` | Screens in **C#** on ASP.NET Core: `[UI]` classes, `Crud`, the app shell, `AddMateu`/`MapMateu`. |
| `mateu-python` | Screens in **Python** on FastAPI: `@ui` classes, `Crud[...]`, the app shell, `add_mateu`. |
| `mateu-scaffold` | A new Java project or module and its Maven wiring (the annotation processors, the renderer) — and fixing "it compiles but nothing renders". |
| `mateu-run` | Build, run and screenshot a screen to see it render for real. |
| `mateu-federation` | Composing several apps or modules into one console (`@Menu`, `RemoteMenu`, micro-frontends). |
The skills are plain Markdown in
[`ai/claude-plugin/mateu/skills`](https://github.com/miguelperezcolom/mateu/tree/master/ai/claude-plugin/mateu/skills)
— read them, or copy one into another tool's rules.
## Projects come AI-ready
A project created by any of the [New Project front doors](/java-create-your-project/) — IntelliJ
**New Project › Mateu**, VS Code **Mateu: New Project…**, the `io.mateu:mateu-archetype` Maven
archetype — and every [starter](https://github.com/miguelperezcolom/mateu/tree/master/starters)
carries:
- **`AGENTS.md`** — what the project is, its layout, how to run it, where its UI is declared and the
rules an assistant must respect (a view model is per request; actions must be declared; Mateu does
not authenticate…). It is specific to the flavour: a YAML-only project's assistant is told to edit
`specs/ui` and not to add UI classes, a C# project's how its attributes work. Codex, Cursor, Copilot
and the other tools that read `AGENTS.md` pick it up as is.
- **`CLAUDE.md`** — one line, `@AGENTS.md`, so Claude Code reads the same text.
- **`.claude/settings.json`** (projects made by the generators) — it registers this marketplace and
enables the `mateu` plugin. When you open the project in Claude Code and trust the folder, the
marketplace is added for you; install the plugin once with `/plugin install mateu@mateu`.
## Docs as plain text: llms.txt and friends
| File | What it is |
|---|---|
| [`/llms.txt`](https://mateu.io/llms.txt) | The index of the documentation ([llmstxt.org](https://llmstxt.org) convention): what Mateu is and a link to every important page. |
| [`/llms-full.txt`](https://mateu.io/llms-full.txt) | **Every** page of this site in one text file, generated from the docs at build time. Large — for tools that ingest a whole site. |
| [`/mateu-ai-compact.md`](https://mateu.io/mateu-ai-compact.md) | A curated ~2 500-token reference: the annotations, patterns and return types used day to day. |
| [`/mateu-ai-full.md`](https://mateu.io/mateu-ai-full.md) | The curated complete reference (~9 000 tokens). |
How to load them in Claude Projects, Gemini, ChatGPT, Cursor and others:
[AI assistant reference](/ai-assistant-reference/).
## JSON Schemas: validate what the assistant writes
Every YAML file under `specs/ui` is described by a JSON Schema **generated from the records the
server reads**, so it cannot fall behind the code:
| Schema | Covers |
|---|---|
| [`specs-schema.json`](https://raw.githubusercontent.com/miguelperezcolom/mateu/master/backend/shared/uidl/specs-schema.json) | Every `specs/ui` file kind (`type: UI`, `Routes`, `AppShell`, sources, actions, types, translations, environments, `Project`, pages) — use this one. |
| [`uidl-schema.json`](https://raw.githubusercontent.com/miguelperezcolom/mateu/master/backend/shared/uidl/uidl-schema.json) | The component catalogue (a page's component tree). |
| [`routes-schema.json`](https://raw.githubusercontent.com/miguelperezcolom/mateu/master/backend/shared/uidl/routes-schema.json), [`sources-schema.json`](https://raw.githubusercontent.com/miguelperezcolom/mateu/master/backend/shared/uidl/sources-schema.json) | The route registry and the REST source catalogue alone. |
Point the editor at it (the IntelliJ plugin and the VS Code extension do so for you), or start each
file with the schema key, as the starters do:
```yaml nocheck
$schema: https://raw.githubusercontent.com/miguelperezcolom/mateu/master/backend/shared/uidl/specs-schema.json
```
(The loaders ignore the `$schema` key. In an editor without the Mateu extension the comment form
`# yaml-language-server: $schema=` works too.)
For an assistant the schema is a **check**, not a hint: validate each generated file against it
before running anything (the `mateu-yaml` skill ships a validator script). A definition the server
cannot parse shows up as *Not found* on screen, with the reason only in the server log (`WARN Could
not read the definition specs/ui/…`, or on the page itself with `-Dmateu.dev=true`) — and an
unknown key is ignored silently by both the schema and the loader, so check key spellings too. Generating whole UIs from a prompt, validated against these schemas with a repair
loop, is described in [Prompt-to-app](/reference/prompt-to-app/). Editor setup:
[YAML pages › IntelliSense](/define/yaml/pages/#intellisense-setup).
## Agents that operate the running app: MCP
The other direction: an agent **using** an app instead of writing it. Every Mateu backend can serve
its screens as MCP tools (`POST /mateu/mcp`, off by default — `mateu.mcp.enabled=true`), and a
zero-dependency sidecar covers any backend over the public wire. See
[Agent operability (MCP)](/reference/agent-operability/).
## Check the result for real
An assistant's "it compiles" is not "it renders". Ask it to run the app and load the screen — the
`mateu-run` skill does it, screenshot included — or post to the sync endpoint and look at the
answer:
```bash
curl -s -X POST http://localhost:8080/mateu/v3/sync/products \
-H 'Content-Type: application/json' \
-d '{"route":"/products","consumedRoute":"","actionId":"","componentState":{}}'
```
(`consumedRoute: ""` asks for the page itself; without it a route under an app shell answers with
the shell.)
---
# Concepts
URL: https://mateu.io/concepts/
> The ideas every Mateu UI shares, whatever it is authored in, rendered by or served from: its building blocks (mount, app shell, routes, pages, partials, actions, data sources, state and triggers, plus the cross-cutting ones), the model and its wire, and federation.
These concepts apply to every Mateu UI. It doesn't matter whether the UI is written in YAML, Java,
C# or Python, rendered in a browser, on a phone or in IntelliJ, or served by a backend or shipped as
a static bundle. The language-specific manuals only change the **syntax**. The vocabulary below stays
the same.
## Start here: what a Mateu UI is made of
**[What a Mateu UI is made of](/concepts/building-blocks/)** is the map. It names every building
block, shows how the blocks connect in one diagram, and describes each one with the same template:
what it is, what it's for, where it lives, how you author it, and how it behaves when served versus
static. The pages below follow the same order.
## The building blocks
| Block | In one line | Page |
|---|---|---|
| **Mount** | a UI at a base path (`type: UI` or `@UI`) | [Routes & mounts](/concepts/routes-and-mounts/) |
| **App shell & menu** | the chrome, plus menu leaves that route somewhere or run an action | [App shell and menus](/concepts/app-shell-and-menus/) |
| **Routes** | URL → layout + optional view model + parameters (`routes.yaml`) | [Routes & mounts](/concepts/routes-and-mounts/) |
| **Pages & components** | a layout definition (a component tree) plus an optional view model | [Pages and components](/concepts/pages-and-components/) |
| **Partials** | a named piece of layout, spliced in wherever it is used | [Partials](/define/yaml/partials/) |
| **Actions** | flows of steps, REST calls or server methods, run by id (`actions.yaml`) | [Actions](/concepts/action-catalogue/) |
| **Data sources** | named REST endpoints, with sample data (`sources.yaml`) | [Data sources](/concepts/data-sources/) |
| **State & triggers** | `state` / `data` / `appState` / `appData`, `${…}`, rules and triggers | [State, scopes and triggers](/concepts/state-and-triggers/) |
And the cross-cutting blocks, which apply wherever something references them:
| Block | Page |
|---|---|
| **Field types** (`types.yaml`) | [Field types](/java-ui-definition/field-types/) |
| **Translations** (`translations/.yaml`) | [Translations](/define/yaml/translations/) |
| **Environments** (`environments/.yaml`) | [Environments](/deploy/environments/) |
| **Access** (`access`, `eyesOnly`, `readOnlyUnless`, `disabledUnless`) | [Permissions](/define/yaml/permissions/) |
| **Project settings** (`project.yaml`) | [Project settings](/define/yaml/project-settings/) |
## Pages and patterns
Business screens fall into a few families: a listing, a record, a form, a guided process, a
dashboard and a landing page. Mateu has a starting point for each, and a catalogue of patterns on
top of them (wizards, master-detail, drawers, Gantt, planning boards…).
- [The UX guide](/ux/): the structure of an app, the six [page types](/ux/page-types/), the
components and how to extend them.
- [The Mateu Way](/the-mateu-way/): the six families and their starting points.
- [Page templates](/ux-patterns/page-templates/) and
[choosing one](/ux-patterns/choosing-a-page-template/).
- [UX patterns](/ux-patterns/): the full catalogue.
## The model and the wire
Every authoring path produces the same building blocks. Every renderer consumes them, and they
travel as JSON (the **wire**) between whatever produced them and whatever renders them.
- [The model](/mateu-about/the-model/): producers, consumers and the rules that keep them in step.
- [The wire](/reference/wire-specification/): the versioned contract, and how a declaration
becomes wire.
- [How Mateu works](/mateu-about/how-mateu-works/): the request cycle when a server is involved.
- [The authoring rule](/authoring-rule/).
## Federation
Several UIs, owned by different teams and built on different stacks, can be composed into one
console. A shell owns the chrome and the menu, and each domain owns its screens.
- [UI federation](/mateu-about/ui-federation/)
- [Shell and remote menus](/mateu-about/shell-and-remote-menus/)
- [Mateu in microservices](/mateu-about/microservices/) and the
[case study](/case-study-microservices/)
- [Embedding Mateu](/mateu-about/embed/) in another web application
---
# The action catalogue
URL: https://mateu.io/concepts/action-catalogue/
> Declare a client-runnable action once — a flow or a REST call — and run it by id from the shell menu or any page.
:::tip[Part of the map]
This page covers one of the [building blocks of a Mateu UI](/concepts/building-blocks/).
:::
An action that runs in the browser has always lived with its owner:
- a page definition's [`actions:`](/define/yaml/pages/) (a flow with `steps:`, or a `restAction`);
- the app shell's [`actions:`](/define/yaml/app-shell/#flows-on-the-shell), which a menu leaf runs;
- a Java `@Action` method (server logic).
That is right for an action exactly one screen uses. It stops being right the moment a second
owner needs it: **New order** in the menu *and* as a button on the customers page *and* on the
dashboard is three copies of the same flow, and they drift. The catalogue names it once:
```yaml
# specs/ui/actions.yaml
type: Actions
actions:
- id: newOrder
description: Start a new order
steps:
- type: MarkClean
- type: Navigate
route: orders/new
- id: refreshCustomers
description: Re-read the customers
restAction:
source:
ref: customers # a REST source catalogue entry, or an inline url
method: POST
successMessage: Refreshed
```
Every entry is **exactly the shape a page's `actions:` already has** — the same record, the same
schema — so nothing new has to be learned and an action can move from a page to the catalogue by
cut and paste.
## Declaring it
Two producers feed one table, exactly like the [REST source catalogue](/concepts/data-sources/):
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
- **Authored**: `specs/ui/actions.yaml`, plus **any file under `specs/ui` that declares `type: Actions`**
(whatever its name — `specs/ui/catalogs/orders-actions.yaml` works), merged in discovery order.
- **Code**: an `ActionCatalogSupplier` bean returning fluent `Action`s — for a catalogue that comes
from configuration or differs per environment:
```java
@Service
public class OrderActions implements ActionCatalogSupplier {
@Override
public List actionCatalog() {
return List.of(
Action.builder()
.id("newOrder")
.description("Start a new order")
.steps(List.of(new Step.MarkClean(), new Step.Navigate("orders/new")))
.build());
}
}
```
**The authored entry wins** and replaces a supplied one of the same id outright — a half-overridden
flow would be far harder to reason about than a replaced one.
Ids are **global**, not relative to a mount: the catalogue is one table per deployment.
## Using it
Name the id wherever an action id goes. From the shell menu, a `RuleLink`:
```yaml
menu:
- type: RuleLink
label: New order
rules:
- action: RunAction
actionId: newOrder
```
From a page, any `actionId` — a button, a tile, a flow's `RunAction` step:
```yaml
layout:
type: Button
label: New order
actionId: newOrder
```
From Java, the same: a `Button` whose `actionId` is a catalogue id works on a `ComponentTreeSupplier`
view.
## Resolution: owner first
When something names an action id, Mateu looks for it in this order:
1. **The owner's own action** — the page's `actions:` (or a Java action method) for a page; the
shell's `actions:` for a menu leaf. An owner action *without* steps still wins: it is the owner's
server action.
2. **The catalogue.**
3. **The server** — unchanged: an `@Action` method, or an app-level action of the app class.
So a page can **override** a catalogue entry locally by declaring an action with the same id, and
the catalogue never takes an id away from code that already handles it.
A catalogue flow can run another entry (`- type: RunAction, actionId: newOrder`): it resolves the
same way.
## How it runs
- **On the wire** the catalogue rides the app metadata (`AppDto.actionCatalogue`), each flow already
**lowered** to the commands it produces — the same lowering a page's actions get. A page also
carries the catalogue entries its own tree names, so even a renderer that does not look the
catalogue up runs them.
- **In the browser** the resolution happens at the single place a component dispatches an action, so
the Vaadin renderer and every shell get it at once; the Redwood/VB renderer, React Native and the
IntelliJ plugin resolve the same way.
- **In a static bundle** the catalogue ships **once** in `manifest.json` (`actions`), like the source
catalogue — a statically served menu or page runs it with no backend. The visual editor's
**▶ Play** and its bundle export ship it too. It is not part of the bundle's `structureHash`, for
the same reason the source catalogue is not.
## What it deliberately is not
**Only client-runnable actions belong here**: a flow (`steps`) or a REST call (`restAction`) — what
the browser can run on its own. An entry with neither is **ignored with a warning** at load:
```
Action catalogue: 'approveOrder' in specs/ui/actions.yaml is not client-runnable (it has neither
steps nor a restAction) and is ignored — server logic stays an @Action method
```
Server logic stays an `@Action` method on a class, where it can be tested, injected and secured. A
flow may still *call* the server — its `RunAction` step names a server action — but the catalogue is
not a place to name server code.
## In the IDE
**New › Mateu › Action Catalogue** (IntelliJ and VS Code) creates an empty `type: Actions` file. The
visual editor opens it in a structured editor — add, rename, describe and remove actions, edit each
flow's steps — and every action-id picker in the editor (menu leaves, buttons, flow steps) offers the
catalogue's ids, marked **catalog**. See [IDE tooling](/native/ide-tooling/).
The schema is generated: `actions-schema.json`, plus a `type: Actions` branch in the unified
`specs-schema.json`.
---
# App shell and menus
URL: https://mateu.io/concepts/app-shell-and-menus/
> The chrome around every page of a mount — title, variant, header widgets — and the menu, whose leaves either route somewhere or run an action.
The **app shell** is what surrounds every page of a [mount](/concepts/routes-and-mounts/): the
title and logo, the navigation variant, the header widgets and switches, and the **menu**. It is
one of the [building blocks](/concepts/building-blocks/).
An app shell is **not** a page. Its content area shows the current route's page, and when the URL
is the mount's root it shows the **home** route. The shell only draws the chrome around that slot.
## The menu
The menu is a list of entries, and every entry is one of these:
| Entry | What a click does |
|---|---|
| `RouteLink` | navigates to a route of the mount (or to a full URL) |
| `RuleLink` | runs client-side rules, typically one `RunAction` naming an [action](/concepts/action-catalogue/), instead of navigating |
| `Menu` | a group of entries, nestable. `presentation: {display: cards}` (or `@Menu(display = MenuDisplay.cards)`) opens it as a panel of cards |
| `RemoteMenu` | the menu of another Mateu service, fetched at runtime. This is how [federation](/mateu-about/shell-and-remote-menus/) composes several UIs into one console |
A menu leaf is therefore a **route or a rule**. A leaf that routes somewhere the caller may not
reach is not sent: a `RouteLink` inherits its route's [`access:`](/define/yaml/permissions/).
A group can open as a panel of **cards**: `presentation: {display: cards}` on the `Menu`, and a
`description` plus `presentation: {icon: …}` or `{image: …}` on each entry; an entry that is itself a
`Menu` becomes a card whose entries are its actions. See [Card menus](/ux-patterns/navigation/#card-menus-a-group-that-opens-as-cards).
```yaml
- type: Menu
label: Quick access
presentation: {display: cards}
submenu:
- type: RouteLink
label: Check-in
route: check-in
description: Sign the registration card and scan the ID
presentation: {icon: "vaadin:clipboard"}
```
## Authoring it
The product catalogue's shell has one link and one leaf that runs the catalogue's `newProduct`
flow:
```yaml
# specs/ui/app.yaml — bound to the mount root ("") in routes.yaml
type: AppShell
title: Catalogue
variant: MENU_ON_TOP
themeToggle: true
menu:
- type: RouteLink
label: Products
route: products
- type: RuleLink
label: New product
rules:
- action: RunAction
actionId: newProduct
widgets:
- type: Text
text: "v1.0"
```
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
In Java the mount class carries the shell: `@App` for the chrome and `@Menu` fields for the menu.
A field's type decides what kind of entry it becomes:
```java
@UI("")
@App(value = AppVariant.MENU_ON_TOP, themeToggle = true)
@Title("Catalogue")
public class Catalogue {
@Menu Products products; // a page
@Menu RouteLink archived = new RouteLink("/products/archived", "Archived");
}
```
For a menu that depends on the user, configuration or a database, implement `AppSupplier`, which
returns a fluent `AppShell`, or `MenuSupplier`, which returns just the menu. Both are evaluated per
request. C# uses `[App]` with `[MenuItem]` methods, or `IAppSupplier` / `IMenuSupplier`. Python uses
`@app` with `@menu_item`, or `AppSupplier` / `MenuSupplier`.
## The home page
The home page is the route the shell opens on. It resolves in this order: the shell's own
`homeRoute:`, then the mount's `home:`, then the first navigable menu item.
## Served vs static
- **Served**: the server builds the shell per request, so access rules, `AppSupplier`s and
translated labels are applied before anything reaches the browser.
- **Static**: the bundle ships the shell. The shell's flows (`actions:` with `steps`) and catalogue
actions are lowered to commands that run in the browser, so a `RuleLink` works with no backend.
Access rules can't be checked without a server, so they are cosmetic there.
- **Ports**: .NET and Python have no `type: AppShell` file. They declare the shell in code, and they
do carry the action catalogue.
## See also
- [Structure of an app](/ux/app-structure/): the shell variants, the app header, overlays and page
anatomy.
- [App shell as data](/define/yaml/app-shell/): every key of `type: AppShell`, flows and widgets.
- [Navigation and menus](/java-user-manual/build/navigation-and-menus/): the six kinds of `@Menu`
entry, and shells in code.
- [Application shell](/java-user-manual/build/application-shell/): variants and layout.
- [UI federation](/mateu-about/ui-federation/).
---
# What a Mateu UI is made of
URL: https://mateu.io/concepts/building-blocks/
> The building blocks of every Mateu UI — mount, app shell and menu, routes, pages, partials, actions, data sources, state and triggers, plus field types, translations, environments, access and project settings — and how they connect.
import { Tabs, TabItem } from '@astrojs/starlight/components';
A Mateu UI is a small set of **building blocks**. Each one has a name, a job and a home, and the
same blocks exist whichever way you author them: as YAML files under `specs/ui/` (by hand or with
the [visual editor](/define/yaml/visual-editor/)), or as code in Java, C# or Python. Whichever way
you write them, they become one model. That model renders in Vaadin or Redwood on the web, in the
React Native app and in IntelliJ. A backend in Java, .NET or Python can serve it, or it can ship as a
[static bundle](/deploy/static-bundle/) with no backend at all. Several UIs can also be
[federated](/mateu-about/ui-federation/) into one console.
This page is the map. Each section below describes one block with the same template, and links to the
page that covers it in depth.
## How the blocks connect
```mermaid
flowchart TD
Mount["Mount
type: UI · @UI"] --> Shell["App shell + menu
type: AppShell · @App / @Menu"]
Mount --> Routes["Routes
routes.yaml"]
Shell -- "menu leaf → route" --> Routes
Shell -- "menu leaf → rule" --> Actions
Routes -- "layout + optional viewModel" --> Page["Page
layout definition (+ view model)"]
Page --> Components["Components
Listing · Form · FormField · Button …"]
Page -. "type: Partial" .-> Partials["Partials
specs/ui/partials/"]
Partials --> Components
Components -- "actionId" --> Actions["Actions
page actions: · actions.yaml · @Action"]
Page -- "triggers:" --> Triggers["Triggers
OnLoad · OnValueChange · OnCustomEvent …"]
Triggers --> Actions
Actions -- "steps:" --> Steps["Navigate · RunAction · Emit
CloseOverlay · MarkDirty · MarkClean"]
Steps -- "Navigate" --> Routes
Actions -- "restAction" --> Sources["Data sources
sources.yaml · @RestSource"]
Components -- "rowsSource · optionsSource" --> Sources
Routes -- "data · appData" --> Sources
Routes -- "state · appState" --> State["State scopes
state · data · appState · appData"]
Sources --> State
Components -- "${state.x} · ${data.x}" --> State
subgraph cross["Cross-cutting"]
Types["Field types
types.yaml"]
I18n["Translations
translations/*.yaml"]
Envs["Environments
environments/*.yaml"]
Access["Access
access · eyesOnly · readOnlyUnless · disabledUnless"]
Project["Project settings
project.yaml"]
end
```
Read it top to bottom. The **mount** is the app at a base path. Its **app shell** draws the chrome
and the **menu**, and the **routes** map each URL of the mount to a **page**. A page is a **layout**
(a tree of **components**, possibly pulling in **partials**) and optionally a **view model** that
holds behaviour. Components name **actions**. An action is either a flow of **steps** that runs in the
browser, a REST call against a **data source**, or a method on a server. Data flows into four **state
scopes** that the components read with `${…}`, and **triggers** run actions with no click at all.
Across all of it, **field types**, **translations**, **environments**, **access** rules and the
**project settings** apply wherever they are referenced.
## The running example
Every example on this page, and on the concept pages it links to, builds the same small app: a
**product catalogue**, the one the [YAML starter](/define/yaml/quickstart/) generates. Its whole
`specs/ui/` folder:
```text
specs/ui/
├── app.ui.yaml ← the mount (type: UI)
├── app.yaml ← the app shell (type: AppShell)
├── routes.yaml ← the routes (type: Routes)
├── products.yaml ← a page (a Listing)
├── product.yaml ← a page (a Form for one product)
├── partials/
│ └── price-block.yaml ← a partial
├── actions.yaml ← the action catalogue (type: Actions)
├── sources.yaml ← the data sources (type: Sources)
├── types.yaml ← field types (type: Types)
├── translations/es.yaml ← translations (type: Translations)
├── environments/pre.yaml← an environment (type: Environment)
└── project.yaml ← project settings (type: Project)
```
Every file declares what it is with `type:`, so Mateu finds it by its content, not by its name. All
of them are validated by one generated schema, `specs-schema.json`, which also gives your editor
completion.
---
## Mount
| | |
|---|---|
| **What it is** | A UI application served at a base path. Every other block belongs to a mount. |
| **What it's for** | Giving the app an address (`/`, `/back-office`), naming its home page, and listing the route files that make it up. Several mounts can live in one project or be federated into one console. |
| **Where it lives** | A file with `type: UI` anywhere under `specs/ui/` (the starter calls it `app.ui.yaml`), or a class annotated `@UI`. |
| **Served vs static** | A server serves the mount at its base path. A static bundle exports one mount, with its routes, into `manifest.json`. |
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
```yaml
# specs/ui/app.ui.yaml
type: UI
basePath: /
home: products # the route the mount opens on
routes:
- routes.yaml
```
```java
@UI("") // the mount at the root; the class is its root view
@Title("Catalogue")
public class Catalogue { … }
```
C#: `[UI("…")]` on a class. Python: `@ui("…")`.
More: [Routes & mounts](/concepts/routes-and-mounts/).
## App shell and menu
| | |
|---|---|
| **What it is** | The chrome around every page: title, logo, variant (menu on top, hamburger, tabs, tiles), header widgets and the **menu**. |
| **What it's for** | Navigation. A menu leaf either **routes** somewhere (`RouteLink`) or **runs** something (`RuleLink`, which runs an action). Groups (`Menu`) nest leaves, and `RemoteMenu` pulls in the menu of another Mateu service. |
| **Where it lives** | A `type: AppShell` definition bound to the mount's root route `""`, or an `@App` class with `@Menu` fields (or an `AppSupplier` that composes it in code). |
| **Served vs static** | The server builds the shell per request, so menu items the caller may not open are dropped. A bundle ships it, and its flows run in the browser. |
```yaml
# specs/ui/app.yaml
type: AppShell
title: Catalogue
variant: MENU_ON_TOP
menu:
- type: RouteLink
label: Products
route: products
- type: RuleLink # runs an action instead of navigating
label: New product
rules:
- action: RunAction
actionId: newProduct # from actions.yaml
```
```java
@UI("")
@App(AppVariant.MENU_ON_TOP)
@Title("Catalogue")
public class Catalogue {
@Menu Products products; // a menu entry routing to the Products screen
}
```
C#: `[App]` with `[MenuItem]` methods, or `IAppSupplier`. Python: `@app` with `@menu_item`, or
`AppSupplier`.
More: [App shell and menus](/concepts/app-shell-and-menus/), and the shell's variants, header and
overlays in [structure of an app](/ux/app-structure/).
## Routes
| | |
|---|---|
| **What it is** | The route table of a mount. Each entry binds a URL, relative to the mount, to a **layout**, an optional **view model** and parameters. |
| **What it's for** | Deciding what each URL shows. One page can answer several routes (with `fixedParams` / `defaultParams`), one layout can serve several view models, and a route can have **no** view model at all. Entries can nest (`children`), be restricted (`access`) and seed state (`state`, `appState`, `data`, `appData`). |
| **Where it lives** | `routes.yaml` (`type: Routes`), listed by the mount. In code, a `RouteEntrySupplier`. An `@UI` class contributes its own route. |
| **Served vs static** | Route resolution runs on the server *and* in the browser with the same precedence: `fixed > client state > path > query > defaults`. The authored table travels in the bundle's `manifest.json`. |
```yaml
# specs/ui/routes.yaml
type: Routes
routes:
- route: "" # the mount root → the app shell
layout: app.yaml
- route: products
layout: products.yaml
- route: products/:id
layout: product.yaml
data: product # fetch the record from sources.yaml on entry
```
```java
@Service
public class CatalogueRoutes implements RouteEntrySupplier {
@Override public List routes() {
return List.of(RouteEntry.of("products", "com.acme.catalogue.Products"));
}
}
```
C#: `IRouteEntrySupplier`. Python: `RouteEntrySupplier`. All three also read `routes.yaml`.
More: [Routes & mounts](/concepts/routes-and-mounts/).
## Pages
| | |
|---|---|
| **What it is** | What a route renders: a **layout definition**, which is a tree of components rooted at one component (`Listing`, `Form`, `VerticalLayout`…), plus, optionally, a **view model** that supplies state, behaviour and validation. |
| **What it's for** | Being a screen. Most business screens belong to a handful of families (a listing, a record, a form, a guided process, a dashboard, a landing page), and Mateu has a starting point for each. |
| **Where it lives** | A YAML file under `specs/ui/`, named by a route's `layout:`. In code, a class: an annotated view model from which Mateu *derives* the layout, or a `ComponentTreeSupplier` that builds it. |
| **Served vs static** | A page with a view model needs a server. A page without one is pre-rendered into the bundle, or shipped raw and expanded in the browser. |
```yaml
# specs/ui/products.yaml
type: Listing
title: Products
searchable: true
rowsSource:
ref: products # a data source
rowRoute: products/${row.id} # a row opens the record page
columns:
- {type: GridColumn, id: name, label: Name}
- {type: GridColumn, id: price, label: Price, fieldType: Money}
- {type: GridColumn, id: status, fieldType: ProductStatus}
```
```java
@Title("Products") // routed as `products` (see Routes above)
public class Products extends AutoCrud { // listing, form and detail, derived
@Override public CrudStore store() { return ProductStore.INSTANCE; }
}
```
C#: `class Products : Crud`. Python: `class Products(Crud[Product])`.
More: [Pages and components](/concepts/pages-and-components/). For what pages look like (the six
[page types](/ux/page-types/), their anatomy and the component catalogue), see the
[UX guide](/ux/).
## Partials
| | |
|---|---|
| **What it is** | A named piece of layout (one component or a list of them) with no route, title or chrome. |
| **What it's for** | Reusing a piece of screen anywhere a component can go. A multi-component partial is **spliced** into its parent, so it works inside forms and grids. |
| **Where it lives** | `specs/ui/partials/.yaml`, used with `type: Partial` + `ref:`. In Java, `new Partial("…")`, or a `ref` naming a class. |
| **Served vs static** | Resolved before anything reaches the wire, so no renderer has to know about partials and a bundle stays self-contained. |
```yaml
# specs/ui/partials/price-block.yaml
content:
- {type: FormField, id: price, label: Price, fieldType: Money}
- {type: FormField, id: status, fieldType: ProductStatus}
```
```yaml
# used in specs/ui/product.yaml
- type: Partial
ref: price-block
```
```java
FormLayout.builder()
.content(List.of(
FormField.builder().id("name").label("Name").build(),
new Partial("price-block")))
.build();
```
Java, C# and Python all resolve file partials. Only Java resolves a `ref` that names a class.
More: [Partials](/define/yaml/partials/). For a reusable piece that is also *bound to a data source*,
see [business components](/java-ui-definition/component-catalogue/).
## Actions
| | |
|---|---|
| **What it is** | What runs when a button, menu leaf, row click, shortcut or trigger names an `actionId`. An action is either a **flow** of `steps`, a **REST call** (`restAction`), or a **server method**. |
| **What it's for** | Behaviour. A flow's steps are `Navigate`, `RunAction` (another action, which may be on a server), `Emit` (an event other components subscribe to), `CloseOverlay`, `MarkDirty` and `MarkClean`. A `restAction` reads or writes through a data source. |
| **Where it lives** | A page's `actions:`, the shell's `actions:`, or the **action catalogue** (`actions.yaml`, `type: Actions`), which declares an action once for every page. Resolution is owner first, then the catalogue, then the server. In Java, `@Action` / `@Button` methods, or an `ActionCatalogSupplier`. |
| **Served vs static** | Flows and REST calls run in the browser, so they also work in a bundle and in Play. A server method, or a `proxy: true` call, needs a server. |
```yaml
# specs/ui/actions.yaml
type: Actions
actions:
- id: newProduct
steps:
- type: MarkClean
- type: Navigate
route: products/new
- id: discontinue
access: {roles: [manager]}
restAction:
source: {ref: product-discontinue}
successMessage: Discontinued
```
```java
public class ProductPage {
String id;
@Button
public Message discontinue() { // a server action; its id is the method name
ProductStore.INSTANCE.discontinue(id);
return new Message("Discontinued");
}
}
```
C#: `[Button]` methods, `IActionCatalogSupplier`. Python: `@button()`, `ActionCatalogSupplier`.
More: [Actions](/concepts/action-catalogue/), and in Java [actions](/java-ui-definition/actions/).
## Data sources
| | |
|---|---|
| **What it is** | A named REST endpoint, declared once, which any surface references by name: listing rows, select options, a route's data, an action's call. |
| **What it's for** | Keeping the URL, the response mapping (`itemsPath`, `fields`, `totalPath`), `proxy` and secrets in one place, so a deployment can be re-pointed without touching a screen. `sample:` data lets a screen show realistic rows before the API exists. |
| **Where it lives** | `sources.yaml` (`type: Sources`). In Java, `@RestSource` on any registered class or a `RestSourceCatalogSupplier`. A surface refers to a source with `ref:` (YAML) or `source = "…"` (annotations). |
| **Served vs static** | The catalogue travels with the app (or once in the bundle's manifest). The browser calls endpoints directly, and `proxy: true` sends the call through the server, which injects `${secret.X}`. Sample data is used only in sample mode: always in the visual editor, and in a bundle or app only when you opt in. |
```yaml
# specs/ui/sources.yaml
sources:
- name: products
source: {url: /api/products}
sample:
- {id: P-001, name: Espresso machine, price: 249.0, status: AVAILABLE}
- name: product
source: {url: "/api/products/${state.id}"}
```
```java
@UI("")
@RestSource(name = "products", url = "/api/products")
@RestSource(name = "product", url = "/api/products/${state.id}")
public class Catalogue { … }
@UI("/rest-products")
@RestListing(source = "products") // rows fetched by the browser
public class RestProducts implements Listing { … }
```
C#: `[RestSource("products", "/api/products")]`, `IRestSourceCatalogSupplier`. Python:
`@rest_source(...)`, `RestSourceCatalogSupplier`.
More: [Data sources](/concepts/data-sources/).
## State and data scopes
| | |
|---|---|
| **What it is** | The four bags of values a screen reads and writes: `state` (the page's field values), `data` (extra page data that isn't a field), `appState` (app-wide values that persist across navigation) and `appData` (app-wide data, fetched once). |
| **What it's for** | Binding. A `FormField id: name` reads and writes `state.name`. Any label, title or URL can say `${state.name}`, `${data.total}`, `${appState.hotel}`. Rules and conditions read the same scopes. |
| **Where it lives** | Seeded by a route (`state`/`appState` as literals, `data`/`appData` as references to sources), filled by sources and actions, and carried by every request. In code, the view model's **fields** are its state, and an action returns `State`, `Data`, `AppState` or `AppData` to change the others. |
| **Served vs static** | Identical. The browser holds the scopes and sends them back with each request to a server, while in a bundle they never leave the browser. |
```yaml
# a route seeding scopes on entry
- route: products/archived
layout: products.yaml
state: {status: DISCONTINUED} # literal → state
appData: catalogue-totals # a source → appData, fetched once
```
```java
public class ProductPage {
String id; // state.id, filled from the :id path parameter
String name; // state.name
@Button
public Data refreshTotals() {
return new Data(Map.of("total", ProductStore.INSTANCE.count())); // → data.total
}
}
```
More: [State, scopes and triggers](/concepts/state-and-triggers/).
## Triggers
| | |
|---|---|
| **What it is** | An action that runs without a click: `OnLoadTrigger`, `OnValueChangeTrigger`, `OnCustomEventTrigger`, `OnSuccessTrigger`, `OnErrorTrigger` and `AutoSaveTrigger`. |
| **What it's for** | Loading on entry, polling, reacting to a field change, refreshing when another component emits an event, saving as the user types. |
| **Where it lives** | A page's `triggers:` (or a `Listing`'s). In Java, `@Trigger(type = …, actionId = …)`, `@SubscribeTo`, `@AutoSave` or a `TriggersSupplier`. |
| **Served vs static** | Triggers fire in the browser. Whether the action they run needs a server depends on the action. |
```yaml
triggers:
- type: OnCustomEventTrigger
eventName: product-saved
actionId: refresh
```
```java
@SubscribeTo(event = "product-saved", action = "refresh")
public class ProductsDashboard { … }
```
C#: `[SubscribeTo("event", "action")]`. Python: `@subscribe_to("event", "action")`.
More: [State, scopes and triggers](/concepts/state-and-triggers/#triggers).
---
The blocks below are **cross-cutting**. They don't sit at one place in the tree. Instead, they apply
wherever something references them.
## Field types
| | |
|---|---|
| **What it is** | A named domain concept (`ProductStatus`, `Money`, `Email`): label, data type, stereotype, options, alignment, status tones. |
| **What it's for** | Declaring a concept once instead of repeating `dataType: money, align: end` on every field and column. A field or column names one with `fieldType:`, and whatever the field declares itself still wins. |
| **Where it lives** | `types.yaml` or any `specs/ui` file declaring `type: Types`, or a `FieldTypeCatalogSupplier`. The Java analogue for annotated classes is a [semantic annotation](/java-ui-definition/annotations/semantic-annotations/). |
| **Served vs static** | Resolved on the authored tree before it becomes components, by the server, the browser expander and the ports alike. The wire never carries `fieldType`. |
```yaml
# specs/ui/types.yaml
type: Types
types:
- id: ProductStatus
label: Status
dataType: status
options:
- {value: AVAILABLE, label: Available}
- {value: DISCONTINUED, label: Discontinued}
tones: {AVAILABLE: success, DISCONTINUED: neutral}
- id: Money
dataType: money
align: end
```
More: [Field types](/java-ui-definition/field-types/).
## Translations
| | |
|---|---|
| **What it is** | One message catalogue per locale. Any label, title or text says `${i18n.key}`. |
| **What it's for** | A multilingual UI that uses one set of files, whether it is authored in YAML or in Java. |
| **Where it lives** | `specs/ui/translations/.yaml` (or `type: Translations` anywhere). In code, a `TranslationsSupplier` or your own `Translator`. C#: `ITranslationsSupplier`. Python: `TranslationsSupplier`. |
| **Served vs static** | A server resolves the keys per request, so every renderer gets finished text. A bundle ships the catalogue and the browser resolves it. |
```yaml
# specs/ui/translations/es.yaml
messages:
products: {title: Productos, new: Nuevo producto}
```
More: [Translations](/define/yaml/translations/).
## Environments
| | |
|---|---|
| **What it is** | Per-deployment overrides of data sources: `baseUrl`, `url`, `headers`, `proxy`. Secrets never go in these files. |
| **What it's for** | Pointing the same screens at `pre`, `pro` or a local stack without editing `sources.yaml`. |
| **Where it lives** | `specs/ui/environments/.yaml` (or `type: Environment`). This is deployment configuration with no code twin: select an environment with `MATEU_ENVIRONMENT` / `-Dmateu.environment`. |
| **Served vs static** | A server applies the overlay at startup. A bundle is built for one environment (`-Dmateu.bundle.environment`), or you re-point it by editing its manifest. |
```yaml
# specs/ui/environments/pre.yaml
sources:
products: {baseUrl: https://pre.api.example.com}
```
More: [Environments](/deploy/environments/).
## Access
| | |
|---|---|
| **What it is** | Who may see, reach or use something, by `roles`, `groups`, `scopes` or `permissions` (AND across dimensions, OR within one). |
| **What it's for** | `access:` on a route, menu item or action (refused → hidden, or 403). `eyesOnly`, `readOnlyUnless` and `disabledUnless` on any component. |
| **Where it lives** | Inline, on the block it restricts. In Java, `@EyesOnly`, `@ReadOnlyUnless` and `@DisabledUnless` (C# attributes of the same name, Python `eyes_only`/`read_only_unless`/`disabled_unless`). Identity always comes from what your framework authenticated. Mateu does not authenticate. |
| **Served vs static** | Enforced on the server, per request. A static bundle has no identity, so access rules are cosmetic there, and a `staticOnly` build refuses them. |
```yaml
- route: admin/prices
layout: prices.yaml
access: {roles: [manager]}
```
More: [Permissions](/define/yaml/permissions/) and [security](/java-user-manual/advanced/security/).
## Project settings
| | |
|---|---|
| **What it is** | Settings for the whole project. Today that is the renderer: `vaadin` or `redwood`. |
| **What it's for** | Choosing the renderer once. The visual editor's canvas, Play, the static bundle and the IDEs follow that choice. |
| **Where it lives** | `specs/ui/project.yaml` (`type: Project`), one per project. |
| **Served vs static** | A bundle ships the chosen renderer. A served app renders with the renderer jar on its classpath, and the server warns at startup if the two disagree. |
```yaml
# specs/ui/project.yaml
type: Project
renderer: vaadin
```
More: [Project settings](/define/yaml/project-settings/).
---
## Where to go next
- **Build it**: the [YAML quickstart](/define/yaml/quickstart/) generates this catalogue, and the
[Java quickstart](/java-user-manual/start-here/quickstart/) builds the code version.
- **See all three authoring paths side by side**: [Define the UI](/define/) and
[Mixing YAML and code](/define/mixing/).
- **See what the blocks look like**: the [UX guide](/ux/): structure of an app, page types,
components and extension points.
- **Understand the model behind the blocks**: [The model](/mateu-about/the-model/) and
[the wire](/reference/wire-specification/).
- **Serve or ship it**: [Serve & deploy](/deploy/).
---
# The REST source catalogue
URL: https://mateu.io/concepts/data-sources/
> Declare each endpoint your UI consumes once, and reference it by name from any screen.
:::tip[Part of the map]
This page covers one of the [building blocks of a Mateu UI](/concepts/building-blocks/).
:::
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
A screen can declare a REST endpoint inline, right where it is consumed:
```java
@RestOptions(url = "https://api.example.com/countries", valuePath = "code", labelPath = "name")
String country;
```
That is the right thing for an endpoint exactly one screen uses. It stops being the right thing the
moment a second screen needs the same one:
- Five screens consuming one endpoint are **five copies to edit** when the host changes.
- There is **nowhere to state what is true of the endpoint** rather than of one screen — its base
URL, its auth, whether it should be fetched through the server, which secrets it needs.
- A **statically deployed bundle cannot be re-pointed** at another environment without a rebuild,
because the URL is baked into every screen that uses it.
- Nothing says the five declarations are **one endpoint**, so anything derived from them has to guess
by comparing URL strings.
The catalogue fixes all four by naming each endpoint once.
## Declaring it
Two producers feed one table, exactly like the [route registry](/concepts/routes-and-mounts/): annotations are
the derived half, an authored `specs/ui/sources.yaml` is merged on top, and **the authored entry
wins**.
```yaml
# specs/ui/sources.yaml
sources:
- name: countries
description: ISO country codes
source:
url: https://restcountries.com/v3.1/all?fields=cca2,name
valuePath: cca2
labelPath: name.common
- name: orders
description: The orders a screen lists
source:
url: /api/orders?since=${state.since}
itemsPath: data
totalPath: meta.total
fields:
customerName: customer.name
```
or in Java, wherever it reads best — typically the `@UI` app class or a dedicated catalogue class:
```java
@UI("")
@RestSource(name = "countries", url = "https://restcountries.com/v3.1/all",
valuePath = "cca2", labelPath = "name.common")
@RestSource(name = "orders", url = "/api/orders?since=${state.since}", itemsPath = "data",
totalPath = "meta.total", fields = {"customerName=customer.name"})
public class Home {}
```
Source names are **global**, not relative to a mount: a source is an endpoint, not a screen, and two
mounts consuming the same endpoint should say so with the same name.
For a catalogue that comes from configuration or differs per environment, implement
`RestSourceCatalogSupplier` on a bean.
## Referencing it
Any of the four surfaces takes a `source` instead of a `url`:
```java
@RestOptions(source = "countries")
String country;
```
What the surface declares still **wins** over the entry, so it can point at a shared endpoint and
still map the response its own way:
```java
@RestOptions(source = "countries", labelPath = "name.official")
String country;
```
The reference travels to the renderer as a name, and the catalogue travels with the app metadata —
or, for a static deployment, once in the bundle's `manifest.json`. **That one table is what makes
re-pointing a CDN deployment an edit instead of a rebuild.**
A reference the catalogue does not carry is never resolved to something half-built: the fetch fails
visibly rather than quietly calling an endpoint nobody declared.
## `fields`: reading a nested value under a flat name
A listing reads each column by using the column id **directly as the dot path**, and a Java record
field cannot be called `customer.name`. So a nested response field could not reach a flat column at
all — and no surface can fix that on its own, because the surface is where the constraint lives.
Declaring it on the source fixes it once for every screen that consumes it:
```yaml
fields:
customerName: customer.name
roomNumber: room.number
```
## `provenance`: what you serve versus what you consume
Each entry says whether the endpoint is one **somebody already serves** or one **this project still
has to build**:
| value | meaning |
|---|---|
| `existing` | somebody else's. Documented, verifiable against their published OpenAPI, never generated. |
| `generate` | yours to build. It is what [`mateu-bundle:server`](/java-user-manual/build/derived-openapi/) generates a controller for. |
| `auto` (default) | inferred from the URL: relative or same-origin is yours, another origin is somebody else's. |
Declare it explicitly when you disagree with the inference:
```yaml
- name: invoices
provenance: generate
source:
url: https://api.acme-internal.example.com/invoices
```
Without the distinction the derived artifacts would be wrong in both directions: generating a Spring
controller for a third party's public API, and silently omitting an endpoint you owe.
## Proxy mode
`proxy: true` makes the fetch go through the Mateu server instead of the browser — no CORS, and
`${secret.X}` injected server-side. With a catalogue this gets **safer**, not just tidier: the
endpoint the proxy calls is resolved from a table the server holds, rather than from an annotation
scattered across fields, and never from the request. That is what keeps the proxy from becoming an
open relay.
## Per-environment overrides
`pre` and `pro` usually differ only in where the endpoints live. An
[environment](/deploy/environments/) file re-points named sources — `baseUrl`, `url`,
`headers`, `proxy` — for `MATEU_ENVIRONMENT=pre`, without editing `sources.yaml`, and the overlay
reaches the wire, the proxy and the bundle manifest alike.
## Sample data: designing without an API
A source can carry **sample data** — the response the endpoint would return — so a listing, a
select or a record shows realistic rows with no backend and no API at all:
```yaml
sources:
- name: orders
source: {url: /api/orders, itemsPath: data}
totalPath: meta.total
sample: # the RESPONSE, so itemsPath / totalPath / fields apply as usual
data:
- {id: 1, customer: Acme, status: OPEN, total: 120.5}
meta: {total: 1}
- name: customers
source: {url: /api/customers}
sampleFile: fixtures/customers.json # JSON or YAML, relative to specs/ui
```
An inline source may carry `sample:` too (`rowsSource: {url: …, sample: […]}`); the surface's own
sample wins over the catalogue entry's.
**When is the sample used?** Only in *sample mode*, and the rule is the same for every renderer and
on both legs:
| Where | Sample mode |
|---|---|
| The visual editor — canvas and Play | **always** |
| A bundle built with `-Dmateu.bundle.mock=true` | on (the manifest says `mockSources: true`) |
| A running app | **only** when it opts in: `-Dmateu.sources.mock=true` or `MATEU_SOURCES_MOCK=true` (the app metadata then says `mockSources: true`) |
| Anywhere else | **never** — the endpoint is called |
Outside sample mode the samples do not even travel: neither the app metadata nor a bundle manifest
carries them.
**What happens in sample mode.** A sampled source is never called and never proxied: a read answers
with a copy of the sample; a **write (POST/PUT/PATCH/DELETE) succeeds without persisting anything** —
the success message shows, nothing is stored, and a reload shows the sample as it was. A listing over
a sampled source searches, filters, sorts and pages the sample **in memory**, even when the source
declares a `totalPath` (a sample cannot honour `${state.page}`). Both legs agree: the browser's direct
fetch and the server's proxied `__restfetch__` short-circuit identically.
## What is not covered
The mapping reaches **any JSON whose pieces are reachable by a dot path** — no envelope convention is
imposed. What it does not do today:
- **Non-JSON responses** (XML, CSV, plain text).
- **Value transformation** — formatting a date, changing units, joining two fields.
- **Cursor or offset paging**: `${searchText}`, `${page}` and `${size}` interpolate, but there is no
arithmetic, so `offset = page × size` cannot be expressed.
- **A 200 carrying an error** in its body; only a non-2xx status is treated as failure.
### Why it stops there
What the catalogue gives you is **selection, not transformation**. A path says *where* a value is;
nothing says what to make of it. That is a deliberate stopping point rather than an oversight, and
the reason is worth knowing before you reach for a way past it.
**Whatever normalises a response has to work on BOTH legs.** A source is fetched in one of two
places: the browser (direct) or the server (`proxy: true` — which is what keeps an API key out of the
browser and sidesteps CORS). A mechanism that only exists on one leg means the same source behaves
differently depending on a flag the author set for an unrelated reason, and the two halves drift.
Every defect this area has produced has had that shape: the `proxy` flag read from the declared
source instead of the resolved one, and JSON body values escaped on one side and not the other.
Three ways past it, in the order they are worth trying:
1. **A mapping expression instead of a bare path.** `${…}` interpolation already exists on both legs
(`TemplateInterpolator` server-side, `interpolate` client-side), so extending `fields` from
`customer.name` to `${row.first} ${row.last}` costs no new machinery and stays symmetric. It
covers most of what is missing above: joining, formatting, defaults.
2. **A transform registered by the host app** — the shape `registerExternalAuthProvider` already has
for headers. Real code, no build step, but **client-side only**: a proxied source would be
unnormalised, so this suits an app that does not proxy.
3. **A web worker supplied by the developer.** What it buys over (2) is genuine isolation — third-
party code with no reach into the DOM or app state — and parsing off the main thread. It costs a
JS file to serve, version and allow through the CSP, and it is client-side only, so a proxied
source would need an equivalent server-side adapter or it stops agreeing with itself.
None of this is implemented. If your API is under your control, the cheapest fix is usually not here
at all: **have the endpoint serve the shape the screen needs**. `demo-starwars` is the worked example
— it uses only `itemsPath` and `totalPath`, no `fields` at all, because the service it reads
denormalises on the way out (`homeworldName` beside `homeworldId`). That is also why it is not
evidence that the mapping is sufficient: it never exercises the gap.
---
# Pages and components
URL: https://mateu.io/concepts/pages-and-components/
> A page is what a route renders: a layout definition — a tree of components — plus, optionally, a view model that supplies state, behaviour and validation.
A **page** is what a [route](/concepts/routes-and-mounts/) renders. It has up to two parts, and you
can author each one separately:
| Part | What it says | Authored as |
|---|---|---|
| **Layout** (the definition) | *what the screen looks like*: a tree of components | a YAML file under `specs/ui/`, the visual editor, or a component tree built in code |
| **View model** (optional) | *what the screen does*: state, actions, validation, data access | a class in Java, C# or Python |
A page can have only a layout. That page runs entirely in the browser, which is what a
[static bundle](/deploy/static-bundle/) ships. It can have only a view model, in which case Mateu
**derives** the layout from the class's fields and methods. Or it can have both: a YAML layout
bound to a class by the route, which is described in [Mixing YAML and code](/define/mixing/).
This page is one of the [building blocks](/concepts/building-blocks/). The running example is the
product catalogue.
## A page as data
A definition is one component at the root, with the components it contains nested under it:
```yaml
# specs/ui/product.yaml — the record page, route products/:id
type: Form
title: ${state.name}
subtitle: Product
toolbar:
- type: Button
label: Edit
buttonStyle: primary
actionable:
type: RouteLink
route: products/${state.id}/edit
content:
- type: FormLayout
content:
- {type: FormField, id: name, label: Name, readOnly: true}
- type: Partial
ref: price-block # price + status, shared with the edit page
buttons:
- type: Button
label: Discontinue
actionId: discontinue # from this page's actions:, or actions.yaml
actions:
- id: discontinue
confirmationRequired: true
restAction:
source: {ref: product-discontinue}
successMessage: Discontinued
```
The route supplies the record. `data: product` on the `products/:id` entry fetches it from the
[data source](/concepts/data-sources/) into the page state, so the fields show `state.name`,
`state.price` and the rest.
Beside the component itself, a definition can carry:
| Key | Meaning |
|---|---|
| `actions` | Actions this page owns: flows (`steps`) and REST calls (`restAction`). See [Actions](/concepts/action-catalogue/). |
| `triggers` | Actions that run without a click. See [triggers](/concepts/state-and-triggers/#triggers). |
| `viewModel` | The class this layout binds to. Omit it on a layout shared by several routes, and let each [route entry](/concepts/routes-and-mounts/#the-definition-is-layout-only) bind its own. |
| `layout` / `layoutDelta` | `layout:` wraps the component when you want the keys above beside it. `layoutDelta:` records human edits on top of a layout Mateu still infers from the view model. See [Pages in YAML](/define/yaml/pages/#layoutdelta--keep-inference-alive). |
## A page as code
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
In Java, C# and Python the view model *is* the page, and Mateu derives the layout from it:
```java
@Title("Product")
public class ProductPage {
String id; // from the :id path parameter
@NotEmpty String name;
@Stereotype(FieldStereotype.money) double price;
ProductStatus status;
@Button
public Message discontinue() {
ProductStore.INSTANCE.discontinue(id);
return new Message("Discontinued");
}
}
```
The fields become a form, the method becomes a button, and the bean validation becomes validation in
both the browser and the server. When you need full control, the same class can implement
`ComponentTreeSupplier` and return the component tree itself, using the same components a YAML file
names. Archetypes such as `AutoCrud`, `Wizard` and `Dashboard` give you whole page families from one
base class. See [The Mateu Way](/the-mateu-way/) and
[choosing a page template](/ux-patterns/choosing-a-page-template/).
C# and Python have the same shape: `[UI]` / `@ui` view models, `Crud` / `Crud[T]`, and the
[Language Rosetta](/reference/language-rosetta/) maps the annotations.
## Components
Every layout, whether it is written in YAML, built fluently in code or derived from a class, is made
of components from **one catalogue**: layouts (`VerticalLayout`, `FormLayout`, `TabLayout`…), fields
(`FormField`), listings (`Listing`, `GridColumn`), display components (`Text`, `Badge`,
`MetricCard`…), overlays (`Dialog`, `Drawer`) and navigation (`RouteLink`, `Menu`…). The `type:` in
YAML is the record's class name, and the catalogue's JSON Schema is generated from those records.
Because of that, the schema, the visual editor's palette and the renderers can't disagree.
- [Supported components](/java-ui-definition/supported-components/) and the
[component tree](/java-ui-definition/the-component-tree/).
- Reuse within the model: [partials](/define/yaml/partials/) (a named piece of layout) and
[business components](/java-ui-definition/component-catalogue/) (a named piece of layout bound to a
source, referenced with `ComponentRef`).
- New rendering: [custom components](/java-ui-definition/custom-components/), the per-renderer escape
hatch.
## Served vs static
| The page has… | With a server | As a static bundle |
|---|---|---|
| only a layout | rendered through the normal sync path | pre-rendered at build time, or shipped raw (`specsOnly`) and expanded in the browser |
| a view model | instantiated fresh on every request | needs a server. A `staticOnly` build refuses it |
## See also
- [Page types](/ux/page-types/) and [components by role](/ux/components/), in the UX guide.
- [Pages in YAML](/define/yaml/pages/): file format, IntelliSense and `layoutDelta`.
- [The visual editor](/define/yaml/visual-editor/), which writes these files.
- [What a Mateu UI is made of](/concepts/building-blocks/).
---
# Route registry (routes.yaml)
URL: https://mateu.io/concepts/routes-and-mounts/
> Bind a URL to a definition, a view model and its parameters — instead of letting the annotation and the file name decide.
**Status:** ✅ Implemented (Java server + static bundle)
:::tip[Part of the map]
This page covers one of the [building blocks of a Mateu UI](/concepts/building-blocks/).
:::
A **mount** is a UI application served at a base path — that is what `@UI("/back-office")` declares,
and the annotated class is the mount's root view. Everything inside the mount can be resolved
through its **route registry**: a `routes.yaml` sitting next to the definitions it routes to.
```
src/main/resources/specs/ui/
├── routes.yaml ← the registry
├── shared-list.yaml ← definitions
└── about.yaml
```
A fully data-driven mount needs no `@UI` class at all: it is declared by a
[`type: UI`](/define/yaml/app-shell/#the-mount-that-ties-it-together) file (base path +
the route files it serves), and its chrome is a [`type: AppShell`](/define/yaml/app-shell/)
definition bound to the root route. The route registry below is the same either way — it is what binds
URLs to definitions and view models, whether the mount is a class or a file.
## Why a registry and not just annotations
An annotation says "this class lives at this path". That is the one-to-one case, and it is what
`@UI` still does for a **mount**. The inner-route annotations (`@Route`/`@Routes`/`@HomeRoute`) were
removed, so every screen *inside* a mount is a registry entry — even a plain one-to-one route.
A registry entry binds **three independent things**, so each becomes reusable on its own:
| | |
|---|---|
| **definition** | the layout |
| **view model** | the behaviour and data |
| **route entry** | the URL, plus the parameters it seeds or pins |
That unlocks the cases an annotation cannot express:
- **One screen, several routes.** `orders/pending` and `orders/archived` over the same view model,
told apart by a pinned parameter — instead of two classes or an artificial `:param` in the path.
- **One definition, several view models.** A shared list layout serving books and films.
- **A route with no server class at all** — which is what a
[statically deployed](/deploy/static-bundle/) mount is.
## Which one do I use?
There is **one route table**. `@UI` and `routes.yaml` are two producers that feed it —
they are not two competing routing systems. `@UI` declares the **mount** (the app at its base path,
and the compile-time signal the annotation processor uses to generate the framework controllers);
`routes.yaml` declares the screens **inside** it.
> **An app at a base path → `@UI` (or a `type: UI` file). A screen inside it → `routes.yaml`.**
The registry also expresses what a one-class-one-path annotation never could:
| You want to… | Use | Why the annotation can't |
|---|---|---|
| Publish an app at a base path (`/back-office`) | `@UI` | — (this *is* the one-to-one case) |
| Serve a screen inside the app at a fixed path (`products`) | `routes.yaml` (`route` + `viewModel`) | The inner-route annotation was removed |
| One screen at several URLs, told apart by a pinned parameter (`orders/pending`, `orders/archived`) | `routes.yaml` (`fixedParams`) | An annotation carries one path and no pinned params |
| Seed a screen with overridable defaults (`?status=open&page=1`) | `routes.yaml` (`defaultParams`) | An annotation has nowhere to put seed values |
| One layout serving several view models (books and films over one list) | `routes.yaml` (`definition` + `viewModel`) | An annotation binds a class to a path, not a layout to many classes |
| A route with **no** view model — a bare layout | `routes.yaml` (`definition`, no `viewModel`) | An annotation needs a class to hang off |
| A [data-driven mount](/define/yaml/app-shell/#the-mount-that-ties-it-together) with no `@UI` class | `type: UI` + `routes.yaml` | There is no class to annotate |
| A [statically deployed](/deploy/static-bundle/) route (no backend) | `routes.yaml` | Only the authored table ships in the bundle; a class is useless without a server |
| Re-point or rename a route without touching Java | `routes.yaml` (authored wins) | Editing an annotation means recompiling |
### They also compose
Because the authored entry **replaces** the derived one outright, a `routes.yaml` entry for the
mount's root route (`""`) can even re-point what the `@UI` class answers — for instance to bind an
[app shell](/define/yaml/app-shell/) definition.
### Rules of thumb
- **One route file per mount.** For a class-declared `@UI("/shop")` mount, tag the file with
`basePath: /shop` and author the routes relative to it; a `specs/ui/routes.yaml` with no
`basePath` serves the root mount.
- **A shared `definition` must not declare `modelView:`** — see
[The definition is layout only](#the-definition-is-layout-only). Otherwise it can only ever serve
the class it names, defeating the "one layout, several view models" case.
## The file
```yaml
type: Routes
routes:
# The root route binds the app shell (a type: AppShell definition).
- route: ""
layout: app.yaml
# Two routes over ONE screen, told apart by a pinned parameter.
- route: orders/pending
viewModel: com.acme.Orders
fixedParams:
status: pending
- route: orders/archived
viewModel: com.acme.Orders
fixedParams:
status: archived
# Seeded, not pinned: the user may change these.
- route: orders
viewModel: com.acme.Orders
defaultParams:
status: open
page: 1
# One definition, two view models. The definition declares NO modelView (see below).
- route: catalog/books
layout: shared-list.yaml
viewModel: com.acme.Books
- route: catalog/films
layout: shared-list.yaml
viewModel: com.acme.Films
# No view model: a definition plus client-side data. Valid and complete — this is the
# statically served case, not a degraded one.
- route: about
layout: about.yaml
```
| Field | Meaning |
|---|---|
| `route` | Path **relative to the mount**, with `:name` segments for path parameters. `""` is the mount's root view, which typically binds the [app shell](/define/yaml/app-shell/). |
| `layout` | The layout file, relative to `specs/ui/` (a leading `/` addresses the classpath root). Optional — omit it and the view model supplies its own tree. This is the **canonical** key (part of the 5-noun vocabulary); **`definition`** is a deprecated alias kept for backward compatibility (`layout` wins if both are given). |
| `viewModel` | Fully qualified name of the server class. **Optional**: a statically deployed route has no server behind it. |
| `fixedParams` | Pinned. **Not overridable by the request.** |
| `defaultParams` | Seeded. The request may override them. |
| `children` | Sub-routes nested under this one, authored **relative** to it. Each fills this screen's slot — see [Nested routes](#nested-routes-a-sub-route-in-a-parents-slot). |
| `parent` | Set automatically when `children` is flattened: the absolute route of the screen whose slot a sub-route fills. You normally author `children`, not `parent`. |
| `state` | Literal values that seed the **component/route** state on entry — see [What a route carries](#what-a-route-carries). |
| `appState` | Literal values that seed the **app** state on entry (merged under the persisted `@AppContext`). |
| `data` | The route's **component** data, as a **reference** to a named [source](/concepts/data-sources/). Fetched when the route loads. |
| `appData` | The route's **app-scope** data, a reference to a named source, fetched **once** on app boot and shared across routes. |
### What a route carries
Beyond where it goes, a route can carry the four data scopes it will populate on entry — the same
four a menu leaf brings when it navigates here. They split by nature:
- **`state` / `appState` are literals** (there is an inbound channel for state): the route seeds them
at the *defaults* level, so anything the client sent — including the persisted `@AppContext` in
`appState` — still wins. `state` is component-scoped and replaced on navigation; `appState` is
app-scoped and persists.
- **`data` / `appData` are references** into the [REST source catalogue](/concepts/data-sources/)
— there is no literal data channel, so data is always *sourced*. `data: countries` is shorthand
for `{ref: countries}`. `data` is fetched at route load (it reuses the `@RestData` load path);
`appData` is fetched once at app scope.
```yaml
- route: reports
viewModel: com.acme.Reports
state: # literal, component scope
tab: summary
appState: # literal, app scope (merged under @AppContext)
theme: dark
data: report-rows # a source ref → the route's component data
appData: kpi-totals # a source ref → app-scope data, fetched once
```
### Routes are relative to the mount
An entry `orders/:id` under a mount at `/back-office` answers `/back-office/orders/42`. Two
federated domains can therefore each have their own `orders` screen without colliding: uniqueness
only has to hold *within* a mount, and between mount base paths (two `@UI` classes claiming the same
base path already fail at startup).
### The mount's home
A `type: UI` mount can name its home page with `home:` — a route of the mount, relative to it like
every other route (a leading `/` is tolerated):
```yaml
type: UI
basePath: /
home: dashboard
routes:
- routes.yaml
```
When the mount authors no `route: ""`, its root resolves to the `home` entry — same definition, view
model and pinned/default parameters — so `/` renders the dashboard. An authored `route: ""` always
wins (explicit beats derived); when that root is an [app shell](/define/yaml/app-shell/),
the shell's `homeRoute` defaults to `home` instead (its own `homeRoute:` still wins). A `home` that
names no route of the mount is warned about at startup and ignored. A static bundle ships the same
behaviour: the root alias travels in the route table, and a shell definition shipped raw carries
the home as its `homeRoute`.
### Nested routes (a sub-route in a parent's slot)
Some screens are a *shell with a slot* — a record master with tabs, or a mediator app — where a
sub-route does not replace the page but renders **inside** the parent. Author that with `children`:
each child's `route` is relative to its parent, and it fills the parent's slot instead of taking
over the screen.
```yaml
- route: use-cases/rra
viewModel: com.acme.RRA # the shell (its own tabs / slot)
children:
- route: orders # → use-cases/rra/orders, rendered in RRA's slot
viewModel: com.acme.OrdersPage
- route: orders/create
viewModel: com.acme.CreateOrderPage
- route: inventory/:id # a detail, still inside the slot
viewModel: com.acme.ProductDetailPage
```
On load, the loader flattens the tree to absolute routes, and each child carries its parent's route
as `parent`. Children nest to any depth. At runtime a request is resolved as a **route chain**: the
entry answering the path, preceded by its `parent`s. The outermost level not yet on screen renders
first — when it is an app (`@App`, a `@Menu`, an `AppSupplier`) it draws its chrome and renders the
rest of the path in its slot — so a reload, a pasted link or back/forward to a child lands on the
parent with the child inside it, never on the bare child.
| Key | Meaning |
|---|---|
| `defaultChild` | On a route with `children`: the child (relative, e.g. `orders`) that opens when the parent is reached on its own. Default: the first (visible) child. |
| `show` | On a child: a feature flag (`audit`, or `!legacy`) that must be on for it to be offered as a tab. Answered by `FeatureFlags` beans; unknown flags are on. A hidden child keeps answering its URL. |
## Recipe: a record master whose tabs are pages
The pattern of a cloud console's resource page: `/customers/7` is the customer, and its tabs —
`/customers/7/orders`, `/customers/7/addresses`… — are pages with a URL of their own. Each one gets
the customer's id, loads when opened, has its own actions and paging, and survives a reload, a pasted
link and back/forward. Runnable in `demo/demo-vb` (package `mastertabs`, renderer chosen with
`-Dmateu.renderer=mateu-vaadin|mateu-redwood`).
**1. The routes** — the master with its tabs as `children`:
```yaml
type: Routes
routes:
- route: customers
viewModel: com.acme.Customers
- route: customers/:customerId
viewModel: com.acme.CustomerMaster
defaultChild: orders # /customers/7 opens /customers/7/orders
children:
- route: orders
viewModel: com.acme.CustomerOrders
- route: addresses
viewModel: com.acme.CustomerAddresses
- route: audit
viewModel: com.acme.CustomerAudit
show: audit # a tab behind a feature flag
```
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
**2. The master** — an `@App(TABS)` with **no menu of its own**: its tabs are its children,
labelled with each child's `@Title`. `backLink = PARENT` draws «← Customers» (the title of the
nearest screen above) instead of breadcrumbs.
```java
@App(value = AppVariant.TABS, backLink = BackLink.PARENT)
public class CustomerMaster implements TitleSupplier {
String customerId; // from :customerId
@Override public String title() { return repo.name(customerId); }
}
```
**3. A tab** — any page or crud. It receives the master's path parameters on every request, by
name:
```java
@Title("Orders")
public class CustomerOrders extends AutoCrud {
String customerId; // from :customerId, on every request
@Override public CrudStore store() { … } // lists the orders of customerId
}
```
Inside a tab the parent's parameters are the listing's **scope**: a filter named like one of them is
shown as a fixed chip and never written into the query string. The crud's `/new` and `/{id}` are
relative to the tab (`/customers/7/orders/new`), and a new record's field named like a parameter
starts filled in (`customerId = 7`). The tab's own page does not repeat its label as a title.
**4. The way in** — a row of the listing opens the master:
```java
@Title("Customers")
@RowRoute("/customers/${row.id}")
public class Customers extends AutoCrud { … }
```
**One page instead of many** — when the tabs are small, keep them on one page: `@Tab(key = …)`
makes an in-page tab a URL (`/customer-overview/7/billing` opens Billing), and `@Subresource`
places sub-listings in the tabs, several stacked in one, with the record as their context:
```java
public class CustomerOverview {
String customerId;
@Tab(value = "Details", key = "details") String name;
@Subresource(tab = "orders", load = Subresource.Load.EAGER) // fetched with the page, counted on the tab
OrdersOfCustomer orders;
@Subresource(tab = "billing", order = 1, help = "Invoices issued to this customer") // lazy: fetched when opened
InvoicesOfCustomer invoices;
@Subresource(tab = "billing", order = 2)
PaymentsOfCustomer payments;
}
```
A tab bar with a single visible tab is not drawn (the tab keeps its key and URL), and a
sub-resource's title is dropped when it only repeats its tab or the page.
In Redwood a sub-listing is fetched when its tab is on screen (so `EAGER` only changes the count on
the tab) and is drawn as a read-only table with its first page: no paging, toolbar or row actions
yet. Vaadin draws the full listing.
## IntelliSense
The registry ships its own JSON Schema, generated from the `RouteEntry` record so it cannot drift
from what the loader accepts:
```
https://raw.githubusercontent.com/miguelperezcolom/mateu/refs/heads/master/backend/shared/uidl/routes-schema.json
```
Point your editor at it for completion, field validation and tooltips while editing `routes.yaml`.
In VS Code, add to `.vscode/settings.json`:
```json
{
"yaml.schemas": {
"https://raw.githubusercontent.com/miguelperezcolom/mateu/refs/heads/master/backend/shared/uidl/routes-schema.json": "**/specs/ui/routes.yaml"
}
}
```
Or per file, as the first line:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/miguelperezcolom/mateu/refs/heads/master/backend/shared/uidl/routes-schema.json
```
The definitions themselves have their own schema — see
[YAML UI Definition](/define/yaml/pages/#intellisense-setup).
## Precedence
Two producers feed one table: the annotation processors emit an entry for every `@UI` class
they index, and `routes.yaml` is merged on top. **The authored entry wins** — the same
*explicit beats derived* rule the layout and page inference already follow. An authored entry
replaces the derived one outright rather than being combined field by field.
Parameters resolve in this order, and it is the same on the server and in a static deployment:
```
fixed > client state > path > query > defaults
```
The two ends carry the meaning. **Defaults** only fill what nothing else supplied, so a route can
seed a screen without taking the choice away from the user. **Fixed** parameters are re-applied on
the server over everything, *including the component state the client sends back* — because route
resolution also runs in the browser, and a parameter pinned only there would be a suggestion rather
than a constraint: a query string or a doctored state could widen the scope the route was pinned to.
:::caution
A `fixedParam` is a routing constraint, not an authorisation check. It stops a request from
*silently* landing on a wider scope; it does not decide who may see that scope. Keep your
authorisation where it already lives.
:::
## The definition is layout only
When an entry names a `definition`, that file is loaded instead of the
[`specs/ui/.yaml` convention](/define/yaml/pages/) — which ties a
screen's layout to its URL and is exactly why one definition could not serve two routes.
A definition shared by several routes must **not** declare `modelView:`, or it can only ever serve
the class it names. Leave it out and each entry binds its own:
```yaml
# specs/ui/shared-list.yaml — layout, nothing else
layout:
type: VerticalLayout
content:
- type: Text
text: "A shared list"
```
A definition that *does* declare `modelView:` keeps it, and it wins over the entry's — so every YAML
page that works today is unaffected.
## Authoring routes in code
The authored half has two producers of its own: the YAML (`routes.yaml` and the `type: UI` mounts),
and **code** — a `RouteEntrySupplier` bean returning `List`. It is the programmatic twin
of the [REST source catalogue supplier](/concepts/data-sources/), for routes that
come from configuration, a database, or that differ per environment. A supplied entry expresses the
full model the YAML can — a route binding a definition, a view model and pinned parameters
independently, one definition serving several routes, and a route with **no view model at all**.
```java
@Service
public class TenantRoutes implements RouteEntrySupplier {
@Override public List routes() {
return List.of(
RouteEntry.of("orders", "com.acme.Orders"),
// The case an annotation cannot express: two routes over one screen, each pinning a scope.
RouteEntry.builder()
.route("orders/pending")
.viewModel("com.acme.Orders")
.fixedParams(Map.of("status", "pending"))
.build(),
// A viewModel-less route: a definition plus client-side data.
RouteEntry.builder().route("about").definition("about.yaml").build());
}
}
```
Build entries with `RouteEntry.builder()` (or `RouteEntry.of(route, viewModel)` for the plain case):
the positional constructors are kept for compatibility, but they break every time the record grows a
component, and an unset component is normalised exactly as YAML leaves it (empty params, no parent).
**Precedence: `routes.yaml` > code supplier > annotation-derived.** An entry in `routes.yaml` for the
same route still wins — the last-mile override that re-points a deployment without a rebuild — and a
supplied entry replaces an annotation-derived one for the same route outright. Like every server-side
supplier, it builds its entries from what the *server* holds, never from the request.
**Parity.** `RouteEntrySupplier` (Java, a bean), `IRouteEntrySupplier` (.NET) and `RouteEntrySupplier`
(Python) all feed the authored table with the same precedence; the ports discover implementers by
scanning the assemblies/modules rather than a bean container. (Python's `RouteEntry` carries no nested
`children`, so a Python supplier authors flat entries with `parent` set — the same shape the YAML
flattens to.) App shells and their menus are likewise code-authorable — see
[App shells in code](/java-user-manual/build/navigation-and-menus/#app-shells-and-menus-in-code).
## Static deployments
The authored table travels in the [static bundle](/deploy/static-bundle/)'s
`manifest.json`, and the renderer resolves routes from it — a statically deployed mount has no server
left to ask what a URL means. Only the authored half is shipped: the derived half is route→class,
and a class is what a bundle with no backend cannot use.
Routes that exist only in `routes.yaml` are exported too — **including entries with no view model**.
There is no client-side YAML renderer and none is needed: a definition that declares no `modelView`
renders as a bare layout through the ordinary sync path, so the exporter pre-renders it like any
other route and a static host serves it with no backend.
## Restricting a route
A route entry can say who may reach it — `access: {roles: [admin]}` — and anybody else gets 403 for
it and for every route nested under it, while a menu link to it is hidden. Same dimensions and
matching as `@EyesOnly`; see [Permissions in YAML](/define/yaml/permissions/). Such a route
is identity-dependent, so a static bundle keeps it backend-served (and `staticOnly` refuses it).
## Failure behaviour
- A **missing** `routes.yaml` is the normal case: the registry is empty and everything resolves as
it did before.
- A **malformed** `routes.yaml` is logged and ignored, leaving the annotation-derived routes intact.
Losing every route in an app because of a syntax error in an optional file would be worse than the
problem the file solves.
- An entry naming a **class that is not on the classpath** is logged and falls through to the
annotation-derived resolution, rather than failing the request.
---
# State, scopes and triggers
URL: https://mateu.io/concepts/state-and-triggers/
> The four scopes a screen reads and writes — state, data, appState, appData — how routes, sources and actions fill them, the ${…} expressions that read them, and the triggers that run actions without a click.
Every Mateu screen works on four bags of values. Components read and write them, `${…}` expressions
read them, and routes, data sources and actions fill them. They are one of the
[building blocks](/concepts/building-blocks/), and they behave the same whichever way the screen was
authored.
## The four scopes
| Scope | Holds | Lifetime | Typically filled by |
|---|---|---|---|
| `state` | the page's field values. A `FormField id: name` **is** `state.name` | the current page, replaced on navigation | the user typing, a route's `state:`, a source's response, an action |
| `data` | extra page values that are not fields: totals, lookups, display-only results | the current page | an action returning `Data`, a route's `data:` |
| `appState` | app-wide values that survive navigation, such as the active hotel, company or year | the app session | a route's `appState:`, an action returning `AppState`, an `@AppContext` selector |
| `appData` | app-wide data, fetched once and shared by every page | the app session | a route's `appData:`, an action returning `AppData` |
Each request carries the scopes from the browser to the server, and each response can change them.
In a static bundle there is no server, so the scopes never leave the browser. Otherwise the two
cases behave the same.
## How a route seeds them
A [route](/concepts/routes-and-mounts/#what-a-route-carries) can populate all four on entry. They
split by nature:
- **`state` and `appState` are literals.** They are seeded at the *defaults* level, so anything the
client already holds (including the persisted `@AppContext`) still wins.
- **`data` and `appData` are references** to a [data source](/concepts/data-sources/). There is no
literal data channel, so data always comes from a source. `data` is fetched when the route loads;
`appData` is fetched once for the whole app.
```yaml
# the product catalogue: the record page fetches its product, the archived listing
# pins a filter value, and the shell-wide totals are fetched once
- route: products/:id
layout: product.yaml
data: product # GET /api/products/${state.id}, merged into the page
- route: products/archived
layout: products.yaml
state: {status: DISCONTINUED}
appData: catalogue-totals
```
Path and query parameters land in `state` too, under the parameter's name (`:id` becomes
`state.id`). They follow the same precedence everywhere: `fixed > client state > path > query >
defaults`.
## How code fills them
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
In a view model, the class's **fields are the state**: Mateu hydrates them from the incoming state
before your method runs, and sends them back afterwards. An action changes the other scopes by
returning one of these:
```java
@Button
public Object refresh(HttpRequest rq) {
return List.of(
this, // state: the fields as they are now
new Data(Map.of("total", ProductStore.INSTANCE.count())), // → data.total
new AppData(Map.of("lastRefresh", Instant.now().toString())));
}
```
`HttpRequest.getAppState(...)` and `HttpRequest.appContext(field)` read the app scope on the server.
The Java data contexts are covered in [Data contexts](/java-user-manual/concepts/fluent-components/fluent-data-contexts/).
## Reading them: `${…}`
Any label, title, text, URL template or body template can read the scopes:
```yaml
title: ${state.name}
subtitle: "${i18n.products.title} (${data.total})"
rowRoute: products/${row.id}
source: {url: "/api/products?status=${state.status}"}
```
| In `${…}` | Where |
|---|---|
| `state`, `data`, `appState`, `appData` | everywhere |
| `i18n` | any text. Resolved per locale (see [Translations](/define/yaml/translations/)) |
| `row` | per-row templates such as a listing's `rowRoute` |
| `searchText`, `page`, `size` | a listing source's url and body |
| `secret` | **only** in a proxied source, resolved on the server. The browser never sees it |
The browser evaluates these expressions with Mateu's own sandboxed evaluator, not with `eval`, so it
is safe under a strict Content Security Policy. Rule and validation expressions can see only
`state`, `data`, `appState`, `appData` and `component`, never bare field names.
## Rules
A **rule** is reactive behaviour that runs in the browser: when a `filter` expression holds, it
sets a value, an attribute (`hidden`, `required`, `disabled`…), a style or a CSS class, or it runs an
action (`RunAction`, `RunJS`). In YAML, rules appear on a menu's `RuleLink`. In Java, a page returns
them from a `RuleSupplier`, and annotations such as `@Disabled` compile to rules for you. See
[Rules](/java-user-manual/concepts/fluent-components/fluent-rules/).
## Triggers
A **trigger** runs an [action](/concepts/action-catalogue/) without a click. Triggers fire in the
browser. Whether the action they name needs a server depends on the action.
| Trigger | Fires | Key fields |
|---|---|---|
| `OnLoadTrigger` | when the page or component loads, optionally repeatedly | `actionId`, `timeoutMillis`, `times`, `condition`, `background` |
| `OnValueChangeTrigger` | when a field changes | `actionId`, `propertyName`, `condition` |
| `OnCustomEventTrigger` | when an event is emitted (by an `Emit` step, `UICommand.dispatchEvent` or `closeModal(event)`) | `actionId`, `eventName`, `source` (`DOCUMENT` / `COMPONENT` / `SELF`), `from`, `condition` |
| `OnSuccessTrigger` / `OnErrorTrigger` | after another action succeeds or fails | `actionId`, `calledActionId`, `condition` |
| `AutoSaveTrigger` | when the user stops typing | `actionId`, `debounceMillis` |
A small summary page in the catalogue loads its totals on entry, and loads them again whenever any
page emits `product-saved`:
```yaml
# specs/ui/catalogue-summary.yaml
layout:
type: VerticalLayout
content:
- {type: Text, text: "${state.count} products"}
actions:
- id: refresh
restAction:
source: {ref: catalogue-totals}
resultPath: "" # merge the response into the state
triggers:
- type: OnLoadTrigger
actionId: refresh
- type: OnCustomEventTrigger
eventName: product-saved
actionId: refresh
```
and the page that saves emits that event as the last step of its flow:
```yaml
actions:
- id: save
validationRequired: true
restAction:
source: {ref: product-update}
successMessage: Saved
- id: saveAndBack
steps:
- type: RunAction
actionId: save
- type: Emit
event: product-saved
- type: Navigate
route: products
```
In Java the same triggers are annotations (`@Trigger(type = TriggerType.OnLoad, actionId = "load")`,
`@SubscribeTo(event = "product-saved", action = "refresh")`, `@AutoSave`) or a `TriggersSupplier`.
C# and Python have `[SubscribeTo]` / `@subscribe_to`. See
[Triggers](/java-user-manual/concepts/fluent-components/fluent-triggers/) and
[component communication](/ux-patterns/component-communication/).
## See also
- [What a Mateu UI is made of](/concepts/building-blocks/)
- [State, actions and fields](/java-user-manual/concepts/state-actions-and-fields/) in Java
- [App context](/ux-patterns/app-context/): an `appState` value picked in the header
---
# Define the UI
URL: https://mateu.io/define/
> Three ways to author a Mateu UI — YAML with the visual editor, code in Java, C# or Python, or both — producing the same model.
There are three ways to author a Mateu UI. They sit at the same level: none is the "real" one and
the others a shortcut, because all of them produce the same [model](/mateu-about/the-model/).
What you author is the same set of [building blocks](/concepts/building-blocks/) either way: a
mount, an app shell, routes, pages, partials, actions, data sources, state and triggers, field
types, translations, environments, access rules and project settings. Only the syntax changes.
| Path | You write | Good for |
|---|---|---|
| [**YAML + visual editor**](/define/yaml/quickstart/) | YAML files under `specs/ui/`, by hand or with the [visual editor](/define/yaml/visual-editor/) | screens over APIs that already exist; teams without a backend of their own; static deployments |
| **Code** — [Java](/java-user-manual/start-here/quickstart/), [C#](/csharp-user-manual/), [Python](/python-user-manual/) | annotated classes (and a fluent API for full control) | screens with server-side logic, validation and data access; teams that want the UI next to the domain code |
| [**Both**](/define/mixing/) | layout in YAML, behaviour in code | screens designed visually but backed by real logic |
## YAML + visual editor (no code)
A UI made only of data: a mount, a route registry, an app shell, pages and a catalogue of the REST
endpoints they use. Every file has a published JSON Schema, so your editor validates and completes
it; the visual editor in IntelliJ and VS Code edits the same files on a canvas.
- [Quickstart](/define/yaml/quickstart/) — a running app with no UI code.
- [Pages in YAML](/define/yaml/pages/), [App shell as data](/define/yaml/app-shell/),
[Partials](/define/yaml/partials/).
- [The visual editor](/define/yaml/visual-editor/) and [IDE tooling](/native/ide-tooling/).
- [Permissions](/define/yaml/permissions/), [Translations](/define/yaml/translations/),
[Project settings](/define/yaml/project-settings/).
- Shared with code: [routes](/concepts/routes-and-mounts/),
[data sources](/concepts/data-sources/), [actions](/concepts/action-catalogue/),
[state and triggers](/concepts/state-and-triggers/).
## Code: Java, C#, Python
Annotate a class and Mateu derives the screen from its fields and methods: a record becomes a form
and a listing, a method becomes a button, bean validation becomes client- and server-side
validation. When the defaults are not enough, a fluent component API gives full control.
- **Java** — the most complete implementation; runs on Spring Boot MVC and WebFlux, Quarkus,
Micronaut and Helidon MP. Start at the [Java quickstart](/java-user-manual/start-here/quickstart/).
[Kotlin](/kotlin-user-manual/) uses the same annotations.
- **C#** — ASP.NET Core. See [C#](/csharp-user-manual/).
- **Python** — FastAPI and Pydantic. See [Python](/python-user-manual/).
The [Language Rosetta](/reference/language-rosetta/) maps each idiom across the three, and the
[parity matrix](/reference/parity/) shows what each server supports today.
## Both
A route can bind a YAML layout to a code view model: the file decides what the screen looks like,
the class supplies state, actions and validation. See [Mixing YAML and code](/define/mixing/).
## With design tools and AI
- [From Figma](/design-systems/figma/) — design with the Mateu component library, generate the
implementation.
- [AI assistants](/ai-assistant-reference/) and [prompt-to-app](/reference/prompt-to-app/) — an AI
writes the model as data and the schemas check it mechanically.
---
# Mixing YAML and code
URL: https://mateu.io/define/mixing/
> Layout from a YAML definition, behaviour from a view model in Java, C# or Python — bound by a route, or by @UISpec in Java.
YAML and code are not two products. They meet in the same model, so one screen can take its
**layout** from a YAML file and its **behaviour** — state, actions, validation, data access — from a
class.
Use this when a screen is designed visually (or by someone who does not write the server code) but
needs logic that only a server can run.
## Bind them in the route
The [route registry](/concepts/routes-and-mounts/) binds a route to a layout and a view model
independently:
```yaml
# specs/ui/routes.yaml
type: Routes
routes:
- route: customer-form
layout: customer-form.yaml
viewModel: com.example.CustomerFormViewModel
```
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
```java
public class CustomerFormViewModel {
public String name;
public String email;
@Button
public void save() { /* … */ }
}
```
`customer-form.yaml` holds the component tree; its fields are bound to the view model's state by
id, and its buttons run the view model's methods. The .NET and Python servers read the same
`routes.yaml` (they accept `viewModel` and `view_model`).
Because layout and view model are bound by the route, not by each other:
- one definition can serve several view models — so a shared definition must **not** name a view
model itself, or it can only serve that one;
- one view model can answer several routes with different [pinned parameters](/concepts/routes-and-mounts/);
- a route can have **no** view model at all, which is the pure-YAML case.
## Or bind them in the class (Java)
In Java, `@UISpec` points a view model at its layout file directly:
```java
@UISpec("specs/ui/customer-form.yaml")
public class CustomerFormViewModel { /* … */ }
```
See [`@UISpec`](/java-ui-definition/annotations/uispec/).
## Keep inference alive with a delta
A YAML `layout:` replaces what Mateu would infer from the class. A `layoutDelta:` instead records
only what a person changed — order, hidden fields, overrides — and is re-applied on top of the
inferred layout on every request, so fields the class gains later still appear. The visual editor
writes deltas when it can. See [`layoutDelta:`](/define/yaml/pages/#layoutdelta--keep-inference-alive).
## What goes where
| Concern | YAML | Code |
|---|---|---|
| Arrangement, labels, sections, tabs | ✓ | ✓ |
| Reading and writing REST APIs | ✓ ([data sources](/concepts/data-sources/)) | ✓ |
| Logic that needs a server (database, domain rules, secrets) | via a proxied source | ✓ |
| Validation | client rules | client + server |
| Deployable without a backend | ✓ | — |
## Related
- [Pages in YAML](/define/yaml/pages/)
- [The model](/mateu-about/the-model/) — the precedence rules when both say something
- [The authoring rule](/authoring-rule/)
---
# App shell as data (type: AppShell)
URL: https://mateu.io/define/yaml/app-shell/
> Declare a mount's chrome — title, menu, variant, widgets — as a type: AppShell definition bound to a route, the data-driven counterpart of an @App class.
**Status:** ✅ Implemented (Java server; Spring MVC auto-configures the HTTP surface)
A **mount** is a UI application served at a base path. `@UI("/back-office")` declares one, and the
annotated class carries the chrome around it: title, logo, menu, variant.
A data-driven mount declares that same chrome **as data**, in its own file discriminated by
`type: AppShell`. The app shell is a **view like any other** — a definition bound to a route in the
mount's [route registry](/concepts/routes-and-mounts/), typically the root route `""`. There
is no longer an `app:` block inside `routes.yaml`: `routes:` says what is inside the mount, the app
shell says what surrounds it, and each is its own file.
```yaml
# specs/ui/app.yaml
type: AppShell
title: Back office
subtitle: Authored entirely in YAML
variant: MENU_ON_TOP
menu:
- type: RouteLink
label: Orders
route: orders
icon: vaadin:cart
- type: RouteLink
label: Users
route: users
icon: vaadin:users
```
The route registry binds that definition to the mount's root:
```yaml
# specs/ui/routes.yaml
type: Routes
routes:
- route: "" # the mount root → the app shell view
definition: app.yaml
- route: orders
definition: orders.yaml
- route: users
definition: users.yaml
```
With the shell, the routes and the page definitions they point at, the mount needs **no Mateu Java at
all** — see `demo/demo-app-definition` (port 8099), whose only Java file is the Spring Boot entry
point.
## What the definition accepts
| Key | Meaning |
|---|---|
| `type` | `AppShell` — the discriminator that makes this file an app shell |
| `title`, `subtitle`, `pageTitle` | header texts |
| `logo`, `favicon` | assets |
| `variant` | `AppVariant` — `AUTO`, `MENU_ON_TOP`, `HAMBURGER_MENU`, `HAMBURGER_SECTIONS`, `MENU_ON_LEFT`, `RAIL`, `TABS`, `TILES` (see [shell variants](/ux/app-structure/#the-app-shell-and-its-variants)) |
| `layout` | `AppLayout` — `SINGLE_SLOT` (default) or `SPLIT` |
| `drawerClosed` | start with the drawer collapsed |
| `accentColor` | the brand accent, a CSS colour (`"#D2232A"`): the accent strip (under page headers, on top of listings, at the foot of the hero), the hero background and the console name — the twin of `@App(accentColor)` |
| `backLink` | `PARENT` draws a single «← Parent» link instead of breadcrumbs (a record master whose tabs are pages) — the twin of `@App(backLink)` |
| `style`, `cssClasses` | styling hooks |
| `menu` | a list of navigation items |
| `widgets` | a list of components rendered in the header |
| `homeRoute` | what the mount loads at its root |
| `actions` | the shell's **flows**: actions with `steps:`, run by a menu leaf — see [Flows on the shell](#flows-on-the-shell) |
| `themeToggle`, `commandCenter`, `chromeless`, `accessKeys` | the header switches — the twins of the same `@App(...)` attributes. Absent: the `@App` class decides (no class: off). `chromeless` implies the command center |
| `contextSelectors` | app context pickers on the header — the twin of `@AppContext` fields: see [Header extras](#header-extras-pickers-actions-inbox-and-search) |
| `headerActions` | header buttons and dropdowns — the twin of `AppActionsSupplier` |
| `notifications` | the notification inbox fed by REST sources — the twin of `NotificationsSupplier` |
| `globalSearch` | the palette's entity search fed by a REST source — the twin of `GlobalSearchSupplier` |
| `mobileQr` | the "Open on mobile" header button (experimental) — the twin of `@App(mobileQr)`; see [Mobile app](/ux-patterns/mobile-app/). Absent: the `@App` class decides (no class: off) |
`menu:` and `widgets:` deserialize through the **same `type:` discriminator as page layouts**, so
every navigation type (`RouteLink`, `Menu`, `RemoteMenu`, …) and every component of the catalog is
available with no extra wiring, and the same [JSON Schema IntelliSense](/define/yaml/pages/#intellisense-setup)
applies while you edit.
## Card menus
A `Menu` group with `presentation: {display: cards}` opens as a panel of **cards** instead of a
plain dropdown. Each entry is a card: its `label` is the title, `description` the text and
`presentation: {icon: …}` or `presentation: {image: …}` the visual. An entry that is itself a `Menu`
is not a destination — its entries become the card's actions:
```yaml
menu:
- type: Menu
label: Quick access
presentation: {display: cards}
submenu:
- type: RouteLink
label: Check-in
route: check-in
description: Sign the registration card and scan the ID
presentation: {icon: "vaadin:clipboard"}
- type: RouteLink
label: Floor plan
route: floor-plan
description: Rooms coloured by housekeeping status
presentation: {image: /img/floor.png}
- type: Menu
label: Billing
description: Folios, charges and payments
presentation: {icon: "vaadin:invoice"}
submenu: # the card's actions
- {type: RouteLink, label: Folio, route: billing}
- {type: RouteLink, label: New booking, route: bookings/new}
```
The routes of the entries are mount-relative as written: a group adds nothing to them. It works
served and in a `specsOnly` static bundle alike. Where it shows (top bars on Vaadin and Redwood, the
React Native drawer, IntelliJ) and the Java/.NET/Python equivalents:
[Card menus](/ux-patterns/navigation/#card-menus-a-group-that-opens-as-cards).
## Flows on the shell
A menu entry does not have to navigate somewhere: it can **run a flow**. The shell declares its
flows under `actions:` — exactly the shape a [page definition's `actions:`](/define/yaml/pages/)
already has: an `id` plus a list of `steps` (`Navigate`, `Emit`, `CloseOverlay`, `RunAction`,
`MarkClean`, `MarkDirty`). A menu leaf runs one with the existing **rule leaf**: a `RuleLink` whose
single rule is `RunAction` naming the action's id.
```yaml
type: AppShell
title: App
homeRoute: home
actions:
- id: newOrder
steps:
- type: MarkClean
- type: Navigate
route: orders/new # relative to the mount, like every routes.yaml route
menu:
- type: RouteLink
label: Home
route: home
- type: RuleLink
label: New order
rules:
- action: RunAction
actionId: newOrder
```
The server lowers each flow to the wire commands it produces and ships them on the App
(`AppDto.actions`); clicking **New order** applies them **in the browser, with no server round-trip**.
That is what makes it work with no backend at all: in a [static bundle](/deploy/static-ui/)
and in the visual editor's **▶ Play** the browser expander emits the same lowered actions. A
`Navigate` to a route of the app stays inside the shell (the same in-app navigation a menu link does);
a full URL (`https://…`) leaves the page.
A `RunAction` rule naming an id the shell does **not** declare is looked up in the
[action catalogue](/concepts/action-catalogue/) — named flows and REST calls declared once
in `specs/ui/actions.yaml` and runnable from the menu and any page. An id the shell declares wins over
the catalogue's, even one without steps. Anything else keeps its old meaning: an app-level action
dispatched to the server (an `@Action` on the app class).
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
Built in code, the shell carries flows the same way:
```java
AppShell.builder()
.title("App")
.menuItem(new RuleLink("New order",
List.of(Rule.builder().action(RuleAction.RunAction).actionId("newOrder").build())))
.action(Action.builder()
.id("newOrder")
.steps(List.of(new Step.MarkClean(), new Step.Navigate("orders/new")))
.build())
.themeToggle(true)
.build();
```
## Header extras: pickers, actions, inbox and search
What an `@App` class gets from `@AppContext` fields, `AppActionsSupplier`, `NotificationsSupplier`
and `GlobalSearchSupplier` is authorable on the shell too. Dynamic data comes from the
[REST source catalogue](/concepts/data-sources/) (`sources.yaml`), by reference:
```yaml
type: AppShell
title: Front office
actions:
- id: newBooking
steps:
- {type: Navigate, route: bookings/new}
contextSelectors:
- fieldName: hotel # the app-state key every request carries
label: Hotel
options:
- {value: H1, label: Palma}
- {value: H2, label: Ibiza}
- fieldName: company
optionsSource: {ref: companies} # itemsPath / valuePath / labelPath of the source
headerActions:
- {label: New booking, icon: "vaadin:plus", actionId: newBooking} # a shell flow / catalogue action
- {label: Reports, route: reports} # navigates in the app
- label: More
children:
- {label: Help, route: help}
notifications:
source: {ref: inbox} # GET the inbox
markRead: {ref: inbox-read} # optional; ${state.ids}, ${state.id}, ${state.all}
globalSearch:
source: {ref: search} # ${state.searchText} in its url/body
```
- **`contextSelectors`** — `fieldName`, `label` (default: the field name, humanized) and either
static `options` or an `optionsSource`. The picked value is kept by the browser and sent in the
app state of every request, exactly like an `@AppContext` picker (a Java server reads it with
`HttpRequest.appContext(fieldName)`). A source is called with `${state.searchText}` (what the user
typed in the picker), so a large list can be searched server-side.
- **`headerActions`** — `label`, `icon`, and either `route` (in-app navigation, relative to the
mount like the menu's routes) or `actionId`: one of the shell's `actions:` flows or an
[action-catalogue](/concepts/action-catalogue/) entry runs **in the browser**, like a menu
`RuleLink`; any other id is an app-level action dispatched to the server (an `@App` class
method). `children` makes a dropdown.
- **`notifications`** — `source` lists the inbox; each item (at the source's `itemsPath`, else the
response itself) is read as `id`, `title`, `text`, `route`, `unread` (absent = unread) and `when`,
through the catalogue entry's `fields:` map when the endpoint names them differently.
`markRead` (optional) is called when entries are read, with `${state.ids}` (comma-joined),
`${state.id}` and `${state.all}`; without it entries are marked read on screen only.
- **`globalSearch`** — `source` is called with `${state.searchText}`; each hit is read as
`label`, `description`, `route`, `category` (field map honoured). Hits without a route are
dropped.
- **Where the call runs**: a direct source is fetched by the **browser** — served and in a
[static bundle](/deploy/static-ui/) alike. A `proxy: true` source is called by the **server**,
through the reserved app-level action `_appsource`, which only calls a source the shell itself
declares for that role (the browser names the role, never a url) and injects `${secret.X}`
server-side. A static bundle has no server, so the bundle's static-safety check reports a proxied
shell source — and a header action naming neither a route nor a shell/catalogue action — as
needing one.
- A shell built in code (`AppShell.builder().contextSelector(…).headerAction(…).notifications(…)
.globalSearch(…)`) carries the same. When the mount also has an `@App` class, its pickers and
supplier actions are added after the shell's; its `NotificationsSupplier` /
`GlobalSearchSupplier` keep answering when the shell declares no source of its own.
- **Renderers**: Vaadin and Redwood draw all four. On Redwood the pickers take static options or a
source (direct or proxied), the header actions navigate on `route`, the bell lists
`notifications.source` (opening an entry, or *Mark all read*, marks it read on screen at once and
then through `markRead`) and the Ask palette searches `globalSearch.source` — a direct source
fetched by the browser, a proxied one through `_appsource`, exactly as on Vaadin. Without a
source, the inbox and search of an `@App` class (`_notifications-list` / `_globalsearch`) answer
as before.
- **Ports**: the .NET and Python servers have no `type: AppShell`, so these keys are Java-only
(their app classes keep `[AppContext]` / `IAppActionsSupplier` / … and `@app_context` / …).
## Header widgets
`widgets:` is a list of components drawn in the shell header — a version tag, a status badge, a
button. Each one is an ordinary component of the catalog (same `type:` discriminator as page
layouts) and travels as a child of the App in the `widgets` slot; on `MENU_ON_TOP` they sit in the
top band, with the menu in the band below next to the title.
```yaml
widgets:
- type: Text
text: "v1.0"
```
The server and the browser expander (static bundles, the visual editor's Play) render them alike.
### `homeRoute` matters
A YAML mount has no class to carry `@HomeRoute`, so the home defaults to the **mount's `home:`**
(see [below](#the-mount-that-ties-it-together)) and, when the mount declares none, to the **first
navigable menu item**. Declare `homeRoute:` on the shell to point somewhere else — the shell's own
value always wins. Do not leave a mount with none of them: the shell would load its own root route,
which is the shell again.
:::note[App ≠ its Home Screen]
An **App is a shell** — a menu, title, subtitle, logo and widgets — **plus a reference to a Home
(a Route)**. The App is **not** a Screen. Its content area *mounts the current Route's Screen* (the
Home when the URL is the base path); the App itself only renders the chrome around that content
slot. So the Home is a normal Screen at a route like any other — the App just points at it as the
default. Keeping the two distinct is why the home defaulting is uniform whether the mount is a class
or a YAML file (part of the 5-noun vocabulary — see the [route registry](/concepts/routes-and-mounts/)).
**How it resolves:** when the home route points at a **distinct routed Screen** (a `homeRoute:` or a
first menu item that has its own `@UI`/definition), the App's home fragment is typed with *that
Screen's* class — the content slot loads the home Screen, and the App renders only the chrome. When
the home has no separate Screen (a single-screen app, or a bare menu link with no backing route), the
App remains its own content, unchanged.
:::
## The mount that ties it together
The app shell, the routes and the pages are files of one **mount**, declared by a `type: UI` file —
the data-driven `@UI`. It carries the base path, optionally names its home page, and lists the route
files that make up the mount:
```yaml
# specs/ui/back-office.ui.yaml
type: UI
basePath: /
home: dashboard # optional: a route of this mount (relative, as in routes.yaml)
routes:
- routes.yaml
```
`home:` says which page the mount opens on:
- **With an app shell** bound to `""`, the shell's `homeRoute` defaults to it (a `homeRoute:` on the
shell still wins).
- **Without a shell**, requesting the mount root renders the `home` route — the same definition,
view model and parameters as if that route were requested — unless a `route: ""` is authored,
which always wins (explicit beats derived).
- A `home` naming a route the mount does not have is logged as a warning at startup and ignored.
It only applies to `type: UI` mounts; a class-declared `@UI` mount is unaffected. Java, .NET and
Python resolve the root the same way (the ports have no `type: AppShell` definitions, so the shell
default is Java-only).
Mounts are found by scanning the classpath under `specs/ui/**` for `type: UI` files (by content, not
by filename), so several UIs can coexist. When there is **no** `type: UI` file at all, the
conventional single mount is used: `specs/ui/routes.yaml` loaded at base path `""`.
## Serving it
For **Spring MVC**, `mateu-mvc` auto-configures the HTTP surface (the SPA at the mount's base path and
sync under it) when the classpath declares at least one `type: UI` mount — the work the annotation
processor normally does per `@UI` class, which cannot happen when there is no class. Adding an `@UI`
class later changes nothing: the `type: UI` mounts keep being served next to it, on every Java adapter —
only a base path the class itself is mounted at is left to the class.
Your application is then a Spring Boot entry point plus resources:
```text
src/main/
├── java/…/AppDefinitionDemoApplication.java ← @SpringBootApplication, nothing else
└── resources/specs/ui/
├── back-office.ui.yaml ← the mount (type: UI): base path + route files
├── app.yaml ← the app shell (type: AppShell)
├── routes.yaml ← the route registry (type: Routes)
├── orders.yaml ← page definitions
└── users.yaml
```
## The visual editor edits this
Open any of these files in the [visual editor](/define/yaml/visual-editor/) and it picks the
mode from the file's `type:`: `app.yaml` opens the **app** form (title, chrome and a
link/group/separator/action menu tree — a group's **Display** is *List* or *Cards*, and the entries
of a cards group get a description, an image and, for a card with actions, an icon —, an
**Actions** panel for the shell's flows and a **Widgets** section), `routes.yaml` opens the **routes** table, and the `type: UI` file
opens the **mount** form. Each file is authored on its own — editing the shell never rewrites the
routes.
## Limits
Not carried by the definition yet — these are read reflectively off an `@App` class and still need
one: SSE / MCP / upload URLs and app-level FABs. (The theme toggle, command center, chromeless and
access-keys switches, the context pickers, header actions, inbox and palette search are authorable
on the shell; page-level FABs are authorable on a page's `type: Form` — see
[YAML pages](/define/yaml/pages/#page-header--type-form).)
The .NET and Python servers have no `type: AppShell` definitions, so shell flows do not apply there
(they do carry the [action catalogue](/concepts/action-catalogue/), which a menu leaf of
their app class can run).
Before shell flows landed, a `RuleLink` in a YAML menu did not parse on the server (the schema
advertised it, the YAML mapper did not know it), and the whole shell answered "Not found." — it is
registered now.
A broken or unparseable definition never takes the application down: a route pointing at a file that
is not a `type: AppShell` definition simply resolves to no shell, exactly like the route registry
ignoring a malformed entry.
## Next
- [Route registry](/concepts/routes-and-mounts/) — the `routes:` file that binds the shell to a route
- [Authoring with the visual editor](/define/yaml/visual-editor/) — building all of this by hand
- [YAML UI definition](/define/yaml/pages/) — the page definitions it points at
- [The model](/mateu-about/the-model/) — how this fits with the annotated half
---
# YAML UI Definition
URL: https://mateu.io/define/yaml/pages/
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
1. A request arrives for route `my-app/my-page`.
2. Mateu finds no Java view model bound to that route (a `@UI` mount or a `routes.yaml` entry).
3. Mateu looks for `specs/ui/my-app/my-page.yaml` in the classpath.
4. If found, it deserializes the file into a component tree and renders it.
5. If not found, Mateu falls back to "Not found".
## 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` |
:::note
This convention ties a page's layout to its URL, so one definition cannot serve two routes. To name
the file explicitly — and to bind parameters or reuse one layout across several routes — declare the
route in a [`routes.yaml` registry](/concepts/routes-and-mounts/) instead. The convention
stays as the default when no entry mentions the route.
:::
## File format
Every YAML file must have a root `type` field that names the component to render. All nested components also need a `type`.
```yaml
type: VerticalLayout
spacing: true
padding: true
content:
- 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: date
```
## 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):
```yaml
pageType: landing
layout:
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's `layout:`, over a `@PageTemplate`
on 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 `specsOnly` bundle — 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`
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. The keys in the table below are
**experimental** (`@Experimental("YAML page header (3.0 beta)")` on the `Form` record): their names
and shapes may still change before they are declared stable.
| 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` / `HeaderBadgesSupplier` |
| `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 |
```yaml
type: Form
title: ${state.name}
overline: Customers
titlePlaceholder: New customer…
timestamp: Last updated ${state.updatedAt}
badges:
- {label: "${state.status}", color: success} # blank once interpolated → hidden
kpis:
- {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's `data:` source feeds `data`), interpolated by the renderer. A badge whose text
interpolates to blank is hidden — the same switch a `@BadgeInHeader` String 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 a `sources.yaml` reference,
read with its `itemsPath`/`valuePath`/`labelPath` — like a select field's options). A pick
**navigates** to `route` (with `${state._record}` = the picked value), or, without a route,
dispatches `actionId` (default `_switchRecord`) with the value in `_record` — a page `actions:`
entry or a view model method can take it from there. The source is fetched directly by the
browser, so a `proxy: true` source does not serve a switcher.
- **Banner themes** are `INFO`, `SUCCESS`, `WARNING`, `DANGER` (upper case).
- **FABs** dispatch their `actionId` like any button: an action the page declares in `actions:`,
an [action-catalogue](/concepts/action-catalogue/) entry, or a method of the bound view model.
They stack by `order` (ties keep the list order); `buttonStyle` defaults to `primary`.
- 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 a `specsOnly` bundle.
- **Redwood** draws every one of them, for a YAML page and a Java page alike: KPIs, subtitle,
switcher (listed or from a direct source; `route` picks navigate), banners and FABs, and on its
`oj-sp` page 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
All component types from the [`io.mateu.uidl.data`](/java-ui-definition/supported-components/) 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
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
**Option A — workspace settings (recommended)**
Install the [YAML extension by Red Hat](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml), then add this to `.vscode/settings.json`:
```json
{
"yaml.schemas": {
"https://raw.githubusercontent.com/miguelperezcolom/mateu/master/backend/shared/uidl/specs-schema.json": "src/**/specs/ui/**/*.yaml"
}
}
```
This activates IntelliSense for every `*.yaml` file under any `specs/ui/` directory in the project.
`specs-schema.json` is the unified schema — a `oneOf` of every file kind (pages, `type: Routes`,
`type: UI` mounts, `sources.yaml`, `project.yaml`…), so a routes or sources file validates too; the
component catalogue `uidl-schema.json` alone would flag those as invalid.
**Option B — per file**
Start the file with a `$schema` key (the loaders ignore it; this is what the starters do), or with the
YAML language server's comment:
```yaml nocheck
$schema: https://raw.githubusercontent.com/miguelperezcolom/mateu/master/backend/shared/uidl/specs-schema.json
type: VerticalLayout
...
```
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/miguelperezcolom/mateu/master/backend/shared/uidl/specs-schema.json
```
`specs-schema.json` fits every file kind — a bare component tree, a page with a `layout:` envelope,
routes, sources, project settings. `uidl-schema.json` covers a bare component tree only and rejects a
`layout:` page.
Neither the schema nor the loader rejects an **unknown key** (a misspelt `titel:` is ignored), so a
typo shows as a missing feature, not an error. A file the loader cannot read at all makes its route
answer *Not found*; the reason is in the server log (`WARN Could not read the definition …`) and,
with `-Dmateu.dev=true`, on the page.
---
### IntelliJ IDEA
1. Open **Settings** (`⌘,` / `Ctrl+Alt+S`).
2. Go to **Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings**.
3. Click **+** to add a new mapping.
4. Fill in:
- **Name:** `Mateu UIDL`
- **Schema file or URL:** `https://raw.githubusercontent.com/miguelperezcolom/mateu/refs/heads/master/backend/shared/uidl/specs-schema.json` (the unified schema of every `specs/ui` file kind)
- **Schema version:** `JSON Schema version 7`
5. Under **File path pattern**, click **+** and enter: `specs/ui/**/*.yaml`
6. 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/ui` without a leading slash.
---
## 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:
```yaml
- type: Partial
ref: address-block
```
See [Partials](/define/yaml/partials/).
## `layoutDelta:` — keep inference alive
A page can carry a **`layoutDelta:`** instead of a `layout:`. The difference matters more than it
looks:
```yaml
viewModel: com.acme.Contact
layoutDelta:
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](/concepts/routes-and-mounts/)). The older **`modelView:`** is still accepted
> as a deprecated alias so existing definitions keep working; prefer `viewModel:` 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
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 (the rows of one form layout — how its columns are drawn — count as one run, so a two-column form reorders as the editor shows it) |
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
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 a `layout:` 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.
:::note
`layoutDelta:` is served by the **Java**, **.NET** and **Python** servers (same three rules).
:::
## 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.
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
`@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.
```java
@UISpec("specs/ui/customer-form.yaml")
public class CustomerFormViewModel {
public String name;
public String email;
@Button
public void save() { ... }
}
```
```yaml
# src/main/resources/specs/ui/routes.yaml
type: Routes
routes:
- route: customer-form
viewModel: com.example.CustomerFormViewModel
```
The 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`](/java-ui-definition/annotations/uispec/) for the full reference.
## 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.
---
# Partials
URL: https://mateu.io/define/yaml/partials/
A **partial** is a page-shaped thing with no page. It holds one component, or a list of them, and
nothing else — no route, no title, no chrome — and it can be used anywhere a component can.
Where a page answers *"what does this URL show"*, a partial answers *"what does this **piece** look
like, wherever it appears"*.
## Defining one
Exactly as you would define a page, in a YAML file under `specs/ui/partials/`:
```yaml
# src/main/resources/specs/ui/partials/address-block.yaml
content:
- type: FormField
id: street
label: Street
dataType: string
- type: FormField
id: city
label: City
dataType: string
```
When the partial is a single component, the file *is* that component — no envelope:
```yaml
# src/main/resources/specs/ui/partials/legal-notice.yaml
type: Text
text: "Prices include VAT."
```
## Using one
```yaml
type: FormLayout
content:
- type: FormField
id: name
label: Name
- type: Partial
ref: address-block
```
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
or from Java:
```java
FormLayout.builder()
.content(List.of(
FormField.builder().id("name").label("Name").build(),
new Partial("address-block")))
.build();
```
Partials compose: a partial may use another. A cycle is reported in the log and rendered as
nothing, rather than taken as an invitation to recurse.
## It splices, it does not wrap
A partial standing for **several** components is spliced into its parent's content:
| Authored | Rendered |
|---|---|
| `FormLayout[ name, Partial(address-block) ]` | `FormLayout[ name, street, city ]` |
This is the property that makes "usable anywhere a component is" true rather than aspirational. A
wrapper would put two fields into one grid cell — so the places reuse matters most, forms and grids,
would be exactly the places partials could not go.
The one exception is a slot that holds a single component by construction, such as
`Container.content`. There is nowhere to splice into, so a multi-component partial is stacked in a
bare `VerticalLayout`. Give such a partial its own root when you care about the container.
## Binding
There is **no parameter-passing mechanism**, and that is deliberate. A partial is inlined *before*
binding, so its fields bind against the page that used it, by the same convention as everywhere
else: a `FormField id="street"` inside a partial binds to the hosting ModelView's `street`
property, and a `Button actionId="save"` to its `save()` method.
The consequence to know: a partial is a piece of **layout vocabulary**, not a component with its
own private state. Two pages using `address-block` each bind it to their own `street` and `city`.
## Where partials live at runtime: nowhere
Partials are resolved and inlined **server-side, before anything is mapped to DTOs**. They never
appear on the wire.
That is not an implementation detail — it is why the feature works everywhere at once:
- No renderer has to learn the concept. Vaadin, Redwood, React Native and both IDE plugins support
partials without a line of renderer code, and a renderer added next year does too.
- A [static bundle](/deploy/static-bundle/) stays self-contained. A partial node
on the wire would be a reference the bundle has no server to follow.
## Partials in Java
`ref` may also name a class:
```yaml
- type: Partial
ref: com.acme.ui.AddressBlock
```
A `ComponentTreeSupplier` contributes the tree it returns — resolved per request, which is the
reason to author a partial in Java rather than in a file. A plain `Component` contributes itself.
Partials can also be registered in code, and a registration wins over a file of the same name:
```java
PartialRegistry.instance().register("legal-notice", List.of(new Text("…")));
```
## When a ref resolves to nothing
The partial renders as no content, and the miss is logged. A page is not worth taking down over
one piece of it: the alternative turns a typo in one ref into a 500 on every request to every page
that mentions it.
## Why "partial" and not "fragment"
The obvious name was taken. `UIFragmentDto` is the wire's name for an *increment of UI* returned by
an action — a different layer, and one this concept never reaches, but one word meaning two things
in the same codebase is a tax paid by every reader forever.
## Availability
| Port | Partials |
|---|---|
| Java | ✅ |
| .NET | ✅ |
| Python | ✅ |
All three servers resolve `type: Partial` while building the tree, from
`/partials/[.yaml` (or an explicit `.yaml` path), splice multi-component partials into
the parent's content, stack them where there is no list to splice into, drop a missing ref, and
break a cycle. Registration in code is available in all three.
One difference: **a `ref` that names a class works only on the Java server**. The ports carry the
declarative form only — the same split as `@Overline` vs `OverlineSupplier`.
---
# Permissions in YAML
URL: https://mateu.io/define/yaml/permissions/
> Restrict routes, menu items, actions and components of a YAML-authored app by role, group, scope or permission — enforced on the server.
A YAML-only app restricts what each user sees and does with the same rules the annotations
`@EyesOnly`, `@ReadOnlyUnless` and `@DisabledUnless` express in Java. Same four dimensions, same
matching, **same evaluator**: the YAML keys are read by the very `Authorizer` that reads the
annotations. There is one rule, written two ways.
```yaml
# specs/ui/routes.yaml
routes:
- route: admin/users
layout: users.yaml
access: {roles: [admin]} # anybody else gets 403, here and on every nested route
# specs/ui/app.yaml (type: AppShell)
menu:
- {type: RouteLink, label: Users, route: admin/users} # hidden: it inherits the route's access
- {type: RouteLink, label: Audit, route: audit, access: {roles: [auditor]}}
# specs/ui/users.yaml (a page definition)
actions:
- id: delete
access: {roles: [manager]} # not advertised, buttons naming it disabled, 403 if called
restAction: {source: {ref: users-delete}}
layout:
type: VerticalLayout
content:
- {type: FormField, id: salary, label: Salary, readOnlyUnless: {roles: [hr]}}
- {type: FormField, id: notes, label: Notes, eyesOnly: {groups: [staff]}}
- {type: Button, label: Archive, actionId: archive, disabledUnless: {roles: [manager]}}
```
## The restriction
Every key takes the same object:
```yaml
access:
roles: [admin, manager] # Keycloak realm/client roles, or a top-level `roles` claim
groups: [staff] # the `groups` claim
scopes: [orders:write] # `scope` (space-delimited) or `scp`
permissions: [orders.delete] # the `permissions` claim
```
- **AND across dimensions, OR within one.** The example needs (admin *or* manager) *and* staff
*and* orders:write *and* orders.delete.
- A restriction with **no dimension** lets everybody through.
- **No token, or a malformed one, is denied.** Restricted content needs a Bearer token.
- Shorthand: `access: admin` or `access: [admin, hr]` lists **roles**.
The caller's identity is resolved exactly as for the annotations: the principal your framework
authenticated — Mateu does not authenticate, and never reads a Bearer token itself (see
[Security](/java-user-manual/advanced/security/#how-authorization-works)).
## Where each key goes, and what it does
| Annotation (Java) | YAML key | Where | Unauthorized caller |
|---|---|---|---|
| `@EyesOnly` on a `@UI` class | `access:` | a route entry in `routes.yaml` | **403** for the route and every route nested under it (its `children`, a crud's `/new`, `/:id/edit`) |
| `@EyesOnly` on a menu field | `access:` | an app-shell menu item | the item is not sent. A `RouteLink` with no `access:` of its own **inherits its route's**, so a link to a page you cannot open is not offered. A `Menu` group left empty disappears too |
| `@EyesOnly` / `@DisabledUnless` on an action method | `access:` | an entry of a definition's `actions:` | the action is **not advertised**, every `Button` naming it is **disabled**, and a call that reaches the server anyway (dispatched, or proxied through `__restfetch__`) answers **403** |
| `@EyesOnly` on a field | `eyesOnly:` | any component of a definition | the component (and everything inside it) is removed |
| `@EyesOnly` on a `@UI` class | `eyesOnly:` / `access:` | the ROOT of a definition (its `layout:`, or a bare layout) | nothing of the page is left for the caller: **403**, before any state is hydrated or any action of its view model runs |
| `@ReadOnlyUnless` | `readOnlyUnless:` | any component | `readOnly: true` on it and on every `FormField` inside it |
| `@DisabledUnless` | `disabledUnless:` | any component | `disabled: true` (a `Button`, a `RouteLink`, a `Menu`…); a `FormField`, which has no disabled state, becomes **read-only** |
A field that ends up hidden or read-only for the caller is also **locked on the way in**: its value
is dropped from the state the client sends, so the server keeps its own value. A read-only field
whose value the server accepted from the browser would be a suggestion, not a rule — the same
reason the `Hydrater` ignores such fields for the annotations.
### Why `access` here and `eyesOnly` there
The keys name what the restriction *does*:
- `access:` is about **reaching or invoking** something — a route you navigate to, a menu item you
click, an action you run. Refused, it is simply not there for you (and 403 if you insist).
- `eyesOnly:`, `readOnlyUnless:` and `disabledUnless:` are **presentation states of a component**
on a screen you *can* open. They keep the annotations' names on purpose, so a Java developer
reads a YAML definition the same way they read a class.
A component also accepts `access:` as a synonym of `eyesOnly:`; the schema advertises `eyesOnly`.
## Enforced on the server
Nothing about a restriction is decided in the browser. For every request the server resolves the
caller's identity, rewrites the definition (or the app shell) for it **before** mapping it to the
wire, and strips the keys: what reaches the client is what this caller may see. A restriction the
browser enforced would be a suggestion — the same reason [pinned route
parameters](/concepts/routes-and-mounts/#precedence) are re-applied on the server.
Hiding is UX, not authorization. As with the annotations, the REST endpoints your screens call must
still check who is calling.
## Static bundles and Play
A statically deployed bundle has **no server and no identity** to check a restriction against, so
there the keys are cosmetic:
- `staticOnly` (`-Dmateu.bundle.static=true`) **fails the build** when a route declares `access:`
or its definition declares any of the keys (`StaticSafetyCheck`) — exactly as it does for
`@EyesOnly`. A screen that depends on who asks cannot be one file for everyone.
- A non-static (hybrid) bundle keeps those routes **backend-served**: they are not pre-rendered nor
shipped as raw definitions, and the backend answers them per user.
- The browser-side expander (specs-only bundles, the visual editor's **▶ Play**) has no identity
either: it renders the definition **as authored, unrestricted**, and warns once in the console.
Play shows you everything; check restrictions against a running backend.
## See also
- [Security](/java-user-manual/advanced/security/) — the annotations and the token.
- [Route registry](/concepts/routes-and-mounts/) — where `access:` on a route lives.
- [App shell as data](/define/yaml/app-shell/) — the menu `access:` filters.
- [Translations in YAML](/define/yaml/translations/) and [Environments](/deploy/environments/) — the other two pieces a YAML-only app needs.
---
# Project settings (project.yaml)
URL: https://mateu.io/define/yaml/project-settings/
> Choose the renderer once for the whole project — the visual editor, Play, the static bundle and the IDEs follow it.
A project picks its renderer **once**, in a small descriptor next to its other `specs/ui` files:
```yaml
# src/main/resources/specs/ui/project.yaml
type: Project
renderer: redwood # vaadin | redwood
```
| Key | Values | Default |
|---|---|---|
| `type` | `Project` (required — it is how the file is recognised) | — |
| `renderer` | `vaadin` (served by `io.mateu:mateu-vaadin`) or `redwood` (served by `io.mateu:mateu-redwood`) | `vaadin` |
There is one per project. It conventionally lives at `specs/ui/project.yaml`, and any file under
`specs/ui/` with `type: Project` is found too. A project without one uses the defaults, so it
renders with Vaadin. The file is covered by the generated authoring schema (`specs-schema.json`,
the `type: Project` branch), so editors complete and validate it.
## Who reads it
Before this file existed, three unrelated places each decided the renderer. A served app's Maven
dependency decided it on the server. The visual editor had a switch for each session, and Play and
the static bundle always used Vaadin. They now read the project's choice:
| Where | What it does with `renderer` |
|---|---|
| **Visual editor** | The canvas, the board, Play and Export always use the project's renderer — there is no per-page or per-session switch. Opening `project.yaml` itself shows a small settings form. See [the visual editor](/define/yaml/visual-editor#the-projects-renderer). |
| **Play** | Plays the mount in the project's renderer. With Redwood, the whole mount runs in the real Redwood app, including the menu, routes and Play's back/forward. |
| **Static bundle** | `mateu-bundle:bundle` ships the project's renderer, and the editor's **Export** writes a manifest that renderer can read. See [static UI](/deploy/static-ui#choosing-the-renderer). |
| **IDEs** | IntelliJ (**Settings → Tools → Mateu**) and VS Code (the `mateu.renderer` setting, or **Mateu: Project Settings…**) have a project-level renderer selector. The selector reads and writes this file, and the file is the source of truth. See [IDE tooling](/native/ide-tooling#the-projects-renderer-projectyaml). |
## A served app: the dependency decides, the server warns
The renderer a **served** app shows is still decided by the renderer jar on its classpath. With
`io.mateu:mateu-vaadin` it serves Vaadin, and with `io.mateu:mateu-redwood` it serves Redwood. `project.yaml`
does not switch it. If the two disagree, the app your users get is not the one you designed in the
editor. The server therefore logs one warning at startup:
```text
WARN ProjectRendererCheck -- The project descriptor (specs/ui/project.yaml) says renderer: redwood,
but this server serves vaadin (io.mateu:mateu-vaadin is on the classpath). … Depend on
io.mateu:mateu-redwood instead of io.mateu:mateu-vaadin, or set renderer: vaadin in the descriptor.
```
The warning is never a failure. A team may serve Vaadin while it tries Redwood out in the editor.
Nothing is logged when there is no descriptor, or when the server is headless (no renderer jar on
the classpath, for an API behind a separately deployed frontend). The C# and Python servers do not
serve a renderer bundle, so the check does not apply to them.
To switch a served app, change the dependency **and** the descriptor together:
```xml
io.mateu
mateu-redwood
${mateu.version}
```
## See also
- [Route registry](/concepts/routes-and-mounts) and [REST source catalogue](/concepts/data-sources), the other `specs/ui` kinds.
- [Authoring with the visual editor](/define/yaml/visual-editor).
- [Static UI](/deploy/static-ui).
---
# Quickstart: YAML only
URL: https://mateu.io/define/yaml/quickstart/
> A Mateu app with no UI code — the UI is YAML under specs/ui, served by Spring Boot or bundled into static files for a CDN.
A Mateu app whose UI is **data**: a few YAML files under `src/main/resources/specs/ui/`. You write
**no code** — the only Java is the Spring Boot main class the generator gives you, and the static
flavour has none at all.
You need **Java 21** and **Maven 3.9+**.
## 1. Create the project
Pick one front door ([all of them](/java-create-your-project/) generate the same project):
- **IntelliJ IDEA**: File › New › Project… › **Mateu**, authoring **YAML only, served by a backend**.
- **VS Code**: command palette › **Mateu: New Project…** › **YAML only, served by a backend**.
- **Maven**:
```bash
mvn archetype:generate -DarchetypeGroupId=io.mateu -DarchetypeArtifactId=mateu-archetype \
-DarchetypeVersion= -DgroupId=com.acme -DartifactId=my-app -Dauthoring=yaml
```
:::caution[The archetype needs a JDK 21–25]
Maven's archetype plugin runs a Groovy script that cannot read newer class files: on JDK 26+ it
fails with *"BUG! exception in phase 'semantic analysis' … Unsupported class file major version
70"* and leaves a half-generated folder (`variants/`, `generator/`). Delete that folder and rerun
with `JAVA_HOME` pointing at a JDK 21–25. The IDE wizards have no such limit.
:::
It is the [`starters/yaml`](https://github.com/miguelperezcolom/mateu/tree/master/starters/yaml)
project of the repository, which CI builds and boots on every change.
## 2. Run it
```bash
cd my-app
mvn spring-boot:run
```
Open http://localhost:8080: an app shell with a menu (**Home**, **Products**) opening on a home
page, whose **Browse products** button leads to a listing — search, a status filter, sorting and
paging — over sample data.
## What you got
```
my-app/
├── pom.xml Spring Boot + mateu-mvc + the renderer
├── AGENTS.md, CLAUDE.md guidance for AI assistants
└── src/main/
├── java/…/Application.java boots the server — nothing else
└── resources/specs/ui/
├── app.ui.yaml the mount: this app at "/", its routes, its home (`home: home`)
├── routes.yaml each route bound to a definition
├── app.yaml the app shell: title and menu
├── home.yaml a page: the home, with a call to action to the products
├── products.yaml a page: the listing
├── sources.yaml the REST endpoints, named once, with sample data
└── project.yaml project settings (the renderer)
```
The home page is a hero whose button navigates — its `actionable` is a `RouteLink`, so there is
nothing behind it to run:
```yaml
# home.yaml
type: VerticalLayout
fullWidth: true
horizontalAlignment: STRETCH
content:
- type: HeroSection
title: My app
subtitle: Every screen here is declared as data, in src/main/resources/specs/ui.
centered: true
content:
- type: Button
label: Browse products
buttonStyle: primary
actionable:
type: RouteLink
route: products
```
The listing reads its rows from a **named source**:
```yaml
# products.yaml
type: Listing
title: Products
searchable: true
rowsSource:
ref: products
columns:
- {type: GridColumn, id: id, label: Id, identifier: true}
- {type: GridColumn, id: name, label: Name}
- {type: GridColumn, id: status, label: Status, dataType: status}
```
```yaml
# sources.yaml
sources:
- name: products
source:
url: /api/products
sample:
- {id: P-001, name: Espresso machine, price: 249.0, status: AVAILABLE}
```
Until your API exists the source answers with its `sample:` — the generated main class turns
[sample mode](/concepts/data-sources/#sample-data-designing-without-an-api) on.
Point `url` at your API and run with `-Dmateu.sources.mock=false` to go live.
## 3. Add a screen
A screen is a definition file plus a route and, if it belongs in the menu, a menu entry:
1. Create `specs/ui/orders.yaml` — in IntelliJ **New › Mateu › Page…** or in VS Code
**Mateu: New File…** start it from a page template, then edit it in the **visual editor**.
2. Add the route in `routes.yaml` (**Add Route…** in both IDEs):
```yaml
- route: orders
layout: orders.yaml
```
3. Add it to the menu in `app.yaml`:
```yaml
- type: RouteLink
label: Orders
route: orders
```
Every file carries a `$schema` line, so both IDEs validate and complete it.
## No backend at all: the static flavour
Choose **YAML only, static (no backend)** instead (`-Dauthoring=static`) and the same `specs/ui`
is bundled at build time into plain files:
```bash
mvn package # → target/mateu-bundle/: index.html, manifest.json, assets/
npx serve -s target/mateu-bundle # preview it at http://localhost:3000
```
Deploy `target/mateu-bundle/` to any static host with an SPA fallback. The browser calls the APIs
named in `sources.yaml` itself, so they must allow your site's origin (CORS); `.mvn/maven.config`
ships the sample data (`-Dmateu.bundle.mock=true`) until they exist. The build fails if a screen
still needs a server. See [A 100 % static UI](/deploy/static-ui/).
## Next
- [Route registry](/concepts/routes-and-mounts/) — routes, parameters, nested routes.
- [REST source catalogue](/concepts/data-sources/) — endpoints, field mapping,
writes, sample data.
- [The YAML app shell](/define/yaml/app-shell/) — menus, home route, header actions.
- Need behaviour on the server for a screen? Bind a Java view model to its route
(`viewModel:` in `routes.yaml`) — the **Code + YAML** flavour starts that way.
---
# Translations in YAML
URL: https://mateu.io/define/yaml/translations/
> Make a YAML-authored app multilingual with per-locale message files and ${i18n.key} expressions — shared with Java apps.
A YAML app keeps its texts in **message catalogues**, one per locale, and its labels, titles and
texts reference them with `${i18n.}`. The same files serve a Java app: the default
`Translator` reads them too.
```yaml
# specs/ui/translations/es.yaml
messages:
orders:
title: Pedidos
new: Nuevo pedido
common:
save: Guardar
```
```yaml
# specs/ui/orders.yaml
layout:
type: VerticalLayout
content:
- {type: Text, text: "${i18n.orders.title}"}
- {type: Button, label: "${i18n.orders.new}", actionId: create}
```
## The files
Two spellings, the same thing:
- **By convention**: `specs/ui/translations/.yaml`. The file name is the locale; `type:`
and `locale:` may be omitted.
- **Anywhere under `specs/ui/`**: a file with `type: Translations` and a `locale:`.
```yaml
type: Translations
locale: en-GB
messages:
orders: {title: Orders, new: New order}
```
`messages` is a free tree: nested maps are flattened with dots, so `orders: {title: …}` is the key
`orders.title`. Locales are BCP 47 tags (`es`, `en-GB`); matching ignores case and accepts `_`.
The catalogue is checked against the generated `specs-schema.json` like every other `specs/ui`
file (the `type: Translations` branch).
## Where `${i18n.…}` works
`i18n` is one more scope of the `${…}` expressions the labels already understand (`state`,
`data`…): use it in any label, title, text or other string a definition or an app shell
carries — menu labels, button labels, field labels, section titles, page titles. An expression can
sit inside a longer text: `"${i18n.orders.title} (${state.count})"` — the `i18n` part is resolved
first and the rest is left to the client.
## Which language
The request's locale is what the app's `Translator.locale(HttpRequest)` answers — by default the
first tag of the `Accept-Language` header. It is also what the client gets as `AppDto.locale`, so
the renderer's own chrome follows the same language.
For each key the catalogue is searched in this order:
1. the exact locale (`es-ES`);
2. its language (`es`);
3. the **fallback locale** — `-Dmateu.i18n.fallback` or `MATEU_I18N_FALLBACK`, default `en`;
4. the **key itself** (`orders.title`), with **one warning** in the log per locale and key —
visible without flooding the log.
## Resolved on the server
With a backend, `${i18n.…}` is resolved **on the server, per request**: the wire carries finished
text, so every renderer — web, Redwood, React Native, IntelliJ — shows it with no work of its own.
A definition that mentions `${i18n.…}` is re-derived for each request's locale; one that does not
is shared as before.
## Translations in code
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
A `TranslationsSupplier` bean is the code producer of the same catalogue, exactly as a
`RestSourceCatalogSupplier` is for `sources.yaml`:
```java
@Component
class Messages implements TranslationsSupplier {
public List translations() {
return List.of(new Translations("es", Map.of("orders", Map.of("title", "Pedidos"))));
}
}
```
The two halves merge key by key, and **the YAML files win**: a key a file also declares takes the
file's text.
### Java labels use the same files
The default `Translator` (`DefaultTranslator`, used when the app provides none) consults the
catalogue first:
- `${i18n.key}` inside any text it translates — `@Label("${i18n.orders.title}")` works;
- a text that **is** a key of the catalogue — `@Label("orders.title")` is translated;
- then the classic `messages` `ResourceBundle`, as before.
## Static bundles and Play
With no server nobody resolves the expressions on the way out, so the browser does:
- The catalogue travels in the bundle's `manifest.json` (`translations`: locale → key → text).
- Pre-rendered screens **keep** their `${i18n.…}` expressions on purpose — a bundle serves every
visitor — and raw definitions (`specsOnly`) are expanded with them too.
- The browser resolves them for `AppDto.locale` when the app sets one, else for the browser's
language (`navigator.language`), with the same fallback chain.
- In the visual editor, **▶ Play** reads the project's translation files and offers a locale
switcher, so you can click through the app in every language you have.
## See also
- [Internationalization](/java-user-manual/advanced/i18n/) — `Translator` and the code side.
- [Permissions in YAML](/define/yaml/permissions/) and [Environments](/deploy/environments/).
---
# Authoring a UI with the visual editor
URL: https://mateu.io/define/yaml/visual-editor/
> Build a data-driven Mateu UI by hand — mounts, app shells, routes, pages and partials — in the cross-IDE visual editor, wiring the files to each other as you go.
The **visual editor** is a web-based authoring tool for a **data-driven mount**: a whole Mateu UI
defined as YAML files, with no `@UI` Java class. It runs as the same web bundle in three hosts — a
standalone browser, an **IntelliJ** tab (embedded via JCEF), and a **VSCode** custom editor — so the
experience is identical wherever you author. Behaviour and data stay in Java view models; the editor
edits the **shape** of the UI: its files and how they reference each other.
:::note[Reference pickers]
When the editor can see the whole mount (the IntelliJ and VSCode hosts enumerate `specs/ui/**`
automatically), every cross-file reference is a **picker** — chosen from a list of what exists, while
still accepting a typed value: the route a menu links to, the page a route serves, the partial a page
inlines, and a page field or action bound to its data source's members. Each step below also shows the
exact string, so you can always author by hand.
:::
## The four kinds of file
A mount is a folder of YAML files under `src/main/resources/specs/ui/`. Each file is discriminated by
an explicit `type:` at its root, and the editor opens it in the matching mode:
| File | `type:` | What it declares | Editor mode |
| --- | --- | --- | --- |
| **Mount** | `UI` | A base path and the route files it serves — the data-driven `@UI`. | `mount` (form) |
| **App shell** | `AppShell` | The chrome around the mount: title, variant, logo, **menu**. | `app` (form) |
| **Routes** | `Routes` | Pure routing: each URL bound to a **definition** and an optional **view model**. | `routes` (table) |
| **Page** | any component (e.g. `VerticalLayout`) | A component tree — the layout of one screen. | `page` (WYSIWYG canvas) |
| **Partial** | a bare `content:` list | A reusable, rootless fragment inlined wherever a page names it. | `page` (canvas, "partial" chip) |
| **Project** | `Project` | The project's settings — the renderer, chosen once ([project settings](/define/yaml/project-settings)). | `project` (form) |
The editor picks the mode automatically from the file's `type:` — there are no tabs to switch. A
`type: UI` file opens the mount form; `type: AppShell` opens the app form; a routes file opens the
route table; anything else is a page (or partial) on the canvas.
## The reference graph, and why it matters
The four kinds of file are not independent — they **point at each other**, and that graph is the
whole authoring model:
```
app ──references routes──► routes
▲ (menu: each link │
│ names a route) ├──references pages──► page ──► partial (Partial ref)
└──root route: AppShell───────┘ (definition) │
└──► data source (view model / REST source)
```
- **App → routes.** Each menu item links to a **route** by name.
- **Routes → pages.** Each route's `definition` names a **page** file; its optional `viewModel` names
the **data source** behind that page. The mount's root route (`""`) names the **app shell**.
- **Pages → partials and data sources.** A page inlines a **partial** by `ref`, and its fields bind
(by convention: `FormField id` ↔ property) to the **data source** the route paired it with.
Because these references form a closed graph, the ideal editor lets you **pick** each one from what
already exists in the project: add a menu link and choose the route from a dropdown; add a route and
choose its page from the pages you have; drop a `Partial` and pick which partial; bind a field and
pick it from the data source's contract. That "pick, don't type" experience is what
[project awareness](#project-awareness-the-roadmap) delivers.
## Set up
The editor needs a running Mateu backend — any Mateu app exposes the reserved `__preview__` (live
render) and `__contract__` (data-source shape) actions the editor calls. The reference mount is
`demo/demo-app-definition` on port **8099**; run it with `mvn -o spring-boot:run` from that module.
Then open the editor in your host of choice:
- **IntelliJ** — open the project, open a `specs/ui/*.yaml` file, and switch to the **Visual Editor**
tab. Set the backend URL for the run.
- **VSCode** — open the folder, open a `specs/ui/*.yaml` file, and choose **Reopen Editor With… →
Mateu Visual Editor**. Configure `mateu.baseUrl` (default `http://localhost:8594`).
- **Browser (dev)** — from `frontend/web/monorepo/apps/visual-editor`, run
`MATEU_BACKEND=http://localhost:8099 npx vite` and open `http://localhost:5199`.
## The authoring journey
Below is the end-to-end flow for the reference mount. The files all live in `specs/ui/`.
### 1. The mount — `back-office.ui.yaml`
The mount is the entry point: a base path and the list of route files it serves.
```yaml
type: UI
basePath: /
routes:
- routes.yaml
```
In the **mount** editor you set the base path and manage the list of route files (add, reorder,
remove). On a route collision across several files, the last file wins.
### 2. The app shell — `app.yaml`
The app shell is the chrome around every screen: title, variant, and the navigation **menu**.
```yaml
type: AppShell
title: Back office
subtitle: Authored entirely in YAML
variant: MENU_ON_TOP
menu:
- type: RouteLink
label: Orders
route: orders # ← references a route
icon: vaadin:cart
- type: RouteLink
label: Users
route: users # ← references a route
icon: vaadin:users
```
The **app** editor is a form: title/subtitle/logo/home-route text, the `variant`/`layout` dropdowns,
the header switches (theme toggle, command center, chromeless, access keys), and a menu tree where
you add links, groups, separators and **actions**.
- **Actions** — the shell's [flows](/define/yaml/app-shell/#flows-on-the-shell): add an
action, give it an id and build its steps (Navigate, Emit, Close overlay, Run action, Mark
clean/dirty) with the same flow editor a page's actions use. They are written to the shell's
`actions:` and round-trip losslessly.
- **Action menu items** — a menu entry that runs an action instead of navigating (a `RuleLink` with a
`RunAction` rule). Its action id is picked from the actions the shell declares; you can still type
any other id, which then runs an `@Action` on the server.
- **Widgets** — the components drawn in the shell header (`widgets:`): add, remove and reorder them.
Widgets the editor does not know how to edit are kept untouched.
Each `RouteLink` names a **route** in the `route` field. *Target:* choose the route from a dropdown of
the routes declared in this mount. *Today:* type the route name (e.g. `orders`) — it must match a
`route:` in the routes file.
### 3. The routes — `routes.yaml`
The route registry is pure routing: each URL is bound to a page **definition** and, optionally, a
**view model** (its data source). The root route names the app shell.
```yaml
type: Routes
routes:
- route: "" # the mount root → the app shell view
definition: app.yaml # ← references the app shell
- route: orders
definition: orders.yaml # ← references a page
- route: users
definition: users.yaml # ← references a page
- route: tasks
viewModel: io.mateu.demo.appdefinition.TaskCrud # ← references a data source (no page: the model brings its own UI)
- route: dashboard
viewModel: io.mateu.demo.appdefinition.SalesDashboard
```
The **routes** editor is a table — one row per route, with columns Route / Definition / View model /
Fixed params / Default params, plus add and delete.
- **Definition** names a **page** file (or the app shell for the root). *Target:* pick from the pages
in the mount. *Today:* type the file name (e.g. `orders.yaml`).
- **View model** names the **data source** — a Java view model FQN. *Target:* pick from the project's
view models (and, later, REST sources). *Today:* type the fully-qualified class name.
- A route may bind a definition, a view model, or **both** — a shared page layout served for several
view models, or a view model with no layout of its own. Fixed and default params pin values into the
screen (`k=v, k2=v2`).
### 4. A page — `orders.yaml`
A page is a component tree — the layout of one screen, typed at its root. It carries **no** data
source; that lives on the route.
```yaml
type: VerticalLayout
content:
- type: Text
text: Orders
size: xl
- type: Text
text: This page is a bare, typed component tree — no Java class behind it.
```
The **page** editor is the WYSIWYG canvas: a searchable **palette** of ~120 components on the left,
the live render in the middle (rendered by the real Mateu renderer via `__preview__`), and a **typed
properties** panel on the right. Drag a component from the palette onto the canvas, click to select,
and edit its props on the right.
Two references start on a page:
- **Partials.** Drop a `Partial` component and set its `ref` to a partial's name. *Target:* pick from
the partials in the mount. *Today:* type the `ref` (e.g. `address-block`). The backend inlines it,
so the canvas shows the resolved content.
- **Data sources.** A page's `FormField`s bind by convention (`id` ↔ property) to the **view model**
the route paired the page with (or a page-level `modelView:`, which wins). With that binding
resolved, the properties panel offers the model's fields for a `FormField`'s `id` and its actions
for a `Button`'s `actionId`, both fetched from the model's `__contract__` — so you pick a member
instead of matching names by hand.
#### Selecting components
The canvas, **Layers** and **Properties** always show the same selection.
- **Click anything painted on the canvas** and the innermost component under the pointer is
selected: a hero, a button inside the hero, a dashboard panel, the text inside that panel, a
metric card, a card's title, a tab, a foldout panel. **Layers** opens the groups above it,
highlights its row and scrolls it into view. **Properties** shows it.
- **The canvas is a design surface.** A click on a button, a link, a checkbox or an input selects
it. It does not run the action, follow the link, toggle the box or focus the field. One control
still works: a tab header shows its tab, so the other tab's contents can be seen and selected.
- **Click a row in Layers** to outline the component on the canvas and scroll it into view. A
component inside a tab that is not showing brings its tab forward. A component inside a folded
foldout panel unfolds it.
- **`Esc`** selects the parent. On the page root, `Esc` clears the selection. A click on the canvas
around the page selects the page root.
- **The outline stays on its component.** It moves with the component after you edit a property,
undo, change the canvas width, scroll, switch between the Vaadin and Redwood canvases, or when the
file changes on disk. If the component no longer exists, the selection is cleared.
Grid columns and listing filters are selected from **Layers**. Their `id` is the field they bind,
so the canvas cannot mark them. A layout whose children fill it completely, such as a dashboard
grid, has no spot of its own to click. Click one of its children, then press `Esc`.
#### Images
A property that holds an image has an image picker in **Properties**: a hero's `image`, an
`Image`'s `src`, an avatar, an offer card's picture, the app shell's `logo` and `favicon`. The rule
is the property name (`image`, `src`, `avatar`, `logo`, `favicon`, or any name ending in `Image` or
`ImageUrl`) or an image media type in the schema. Only text properties count, so a form's `avatar`
component is not one.
- **Type anything.** A URL, a path or a data URI is kept as you write it.
- **▦ opens the project's images** as a grid of thumbnails, with a filter when there are many. Click
one to set the property to the URL the app serves it at: `src/main/resources/static/img/hero.jpg`
becomes `/img/hero.jpg`.
- **Add image to project…** opens a file chooser, copies the file into the project and selects it.
The file goes to `images/` under the project's existing web folder. With none, it goes to
`src/main/resources/static/images/` (Java), `wwwroot/images/` (.NET) or `static/images/`
(Python). The folder is created if needed, and an existing file is never overwritten.
The images come from the folders the app serves: `src/main/resources/static`, `public` and
`META-INF/resources`, a `public/` folder, `wwwroot` and `static`, in the module of the file you are
editing. Build output (`target`, `build`, `node_modules`, `dist`…) is skipped. The list follows the
project: an image added, changed or deleted shows up in the picker without reopening the editor.
#### Icons
A property that holds an icon name — `icon`, a button's `iconOnLeft`/`iconOnRight`, a tile's
`actionIcon` — and an app-shell menu entry's icon stay **free text**, with a **☆** button next to
them that opens a searchable grid of the icons the **project's renderer** paints, each previewed.
Picking one fills the text.
- **Vaadin**: the whole Vaadin icon set (`vaadin:cart`).
- **Redwood**: first the neutral names Redwood translates to its own icons (`vaadin:cart` becomes
`oj-ux-ico-cart`, so the page keeps working on Vaadin too), then every `oj-ux-ico-*` icon of
Redwood's icon font. The font loads from Oracle's CDN, as the Redwood canvas does; offline, the
picker still lists the translated names. A name Redwood does not translate shows its generic icon,
which is what the app shows too.
#### The page's settings
Select the page itself (the root, `Esc` until nothing is above it) and **Properties** starts with a
**Page** section. `pageType` is the page family — `landing`, `collection`, `detail`, `form`,
`process` or `dashboard`, the YAML twin of `@PageTemplate`. Leave it on **inferred** and the page
type comes from the page's content; pick one and the editor writes `pageType:` on the definition.
The canvas, the Redwood canvas and **Play** show a project image even when no backend serves it.
The IDE serves the file to the editor, and the page keeps the URL you picked. In a browser with no
IDE, the field is plain text.
### 5. A partial — `specs/ui/partials/address-block.yaml`
A partial is a reusable fragment: a rootless `content:` list, inlined wherever a page's `Partial ref`
names it.
```yaml
content:
- type: FormField
id: street
label: Street
- type: FormField
id: city
label: City
```
The editor opens a partial on the canvas exactly like a page (it wraps the list in a temporary root
for editing) and marks it with a **"partial"** chip; on save it unwraps back to the bare `content:`
list so the authored shape is preserved. Partials compose — a partial may use another.
## Preview and save
The canvas renders continuously: every edit re-runs `__preview__` and repaints, so what you see is the
real UI, not an approximation. Saving writes the file through the host (IntelliJ/VSCode integrate
their own undo and save; the standalone browser keeps a localStorage draft). For a page bound to a
view model, the editor saves the smallest thing that expresses your change — a **layout delta** when
your edits are a re-ordering/relabelling of the model's fields, a full **snapshot** only when the tree
holds something a delta cannot.
## The project's renderer
The canvas paints with the **project's renderer**, the one `specs/ui/project.yaml` names
(`renderer: vaadin | redwood`; Vaadin when the project has no descriptor, see
[project settings](/define/yaml/project-settings)). The toolbar names it, and there is no
per-page or per-session switch: the canvas, the board, Play and Export all use the project's
renderer, so what you see is what ships. To change the project's
renderer, open `project.yaml`. The editor shows it as a small settings form, and choosing a renderer
there rewrites only its `renderer:` line, so comments are kept. You can also use the IDE settings
([IDE tooling](/native/ide-tooling#the-projects-renderer-projectyaml)).
## The Redwood canvas
With `renderer: redwood`, the canvas paints
the page with the real Redwood renderer: the
Oracle Visual Builder app that the `io.mateu:mateu-redwood` jar ships, running inside the canvas. You see
what a Redwood app shows, at the canvas's width, and the page is still editable:
- **Click** a component in the Redwood canvas to select it. **Layers**, **Properties**, the
breadcrumb, `⌘Z`, `Delete` and the arrow keys work as they do on the Vaadin canvas.
- **Every edit** repaints the page, like the Vaadin canvas does.
- The canvas is inert. A click selects a button and does not run it, and a field takes no typing.
- Selection and hover are outlined inside the Redwood canvas.
The page comes from the same place as on the Vaadin canvas: your backend's `__preview__` when you
have one, otherwise the expansion in the browser. A YAML-only project with no backend running
therefore previews in Redwood too. The browser expansion resolves what the server would: a
`type: Partial` or `type: ComponentRef` is inlined from the mount's files and a `${i18n.key}` is
translated from its catalogues (your browser's language, then `en`, then the first declared) — on
the canvas a partial is one selectable node, the reference, since the partial's own file is edited
as itself. The canvas sends the result to the Redwood app, and the app
renders it without asking a server for anything. Its renderer marks each element it paints with the
component it came from, and that is how a click selects the right node. The marks exist only in the
editor; an app in production never carries them.
**Requirements.** Redwood loads Oracle JET, the Spectra components and the Visual Builder runtime from
**Oracle's CDN** (`static.oracle.com`) every time it starts. They are never bundled, see the
renderer's `NOTICE.md`. The Redwood canvas therefore needs that CDN. When a script cannot load, because
you are offline or a proxy or content security policy blocks it, or when the app has not started
after 45 seconds, the canvas shows a notice explaining the problem. **Try again** retries, and you
can switch back to Vaadin and keep working offline. The canvas retries by itself when the browser
comes back online.
**Where it is served from.** The editor's build bundles the Redwood app under `redwood/` next to the
editor, so every host can serve it without a backend:
| Host | The Redwood canvas |
|---|---|
| Browser (`npx vite` in `apps/visual-editor`) | the dev server serves `redwood/` from the `io.mateu:mateu-redwood` resources (`MATEU_REDWOOD_STATIC` points it elsewhere) |
| IntelliJ | the plugin's loopback server serves it from the bundled editor |
| VS Code | the extension's loopback server serves it from the bundled editor, and the webview frames it from there |
If the build does not include the Redwood app, the hosts ask the configured backend for it instead.
A backend that depends on `io.mateu:mateu-redwood` serves it at its root (`/_index.html`, `/_redwood/`).
**What it does not do yet.** You can drag a component from the palette onto the Redwood canvas, but
it lands at the end of the page. Use the Vaadin canvas or **Layers** to place it exactly. Layout
containers that Redwood does not paint as a box of their own, such as a `FormLayout` shown as the
page's form or the dashboard grid of a dashboard page, are selected from **Layers** or with `Esc`
(parent). A click on the page's empty background selects the page root. Tabs are not switched
inside the Redwood canvas, so a component in another tab is selected from **Layers**. The board's
miniatures stay Vaadin.
## Tidy
While you build a page, its structure drifts. Fields get dropped loose instead of into a form layout,
buttons end up one per line, a move leaves a wrapper around a single component, a layout stays
empty. The **Tidy** tab in the bottom dock lists what it finds, with a count on the tab:
| Rule | What it does |
|---|---|
| Put loose fields in a form layout | 2+ `FormField`s in a row, outside a `FormLayout`, go into one |
| Put stacked buttons in a row | 2+ `Button`s in a row go into a `HorizontalLayout` |
| Remove wrappers around a single component | a layout holding one component is replaced by it |
| Merge a layout into its same-direction parent | a `VerticalLayout` directly in a `VerticalLayout` gives up its children |
| Remove empty layouts | layouts with nothing in them |
| Label fields that have none, from their id | `birthYear` is labelled “Birth year” |
| Fields sharing an id | only reported: two fields with one id edit the same value |
The rules are fixed and no AI is involved, so the same page always tidies the same way. They only
touch layouts that carry nothing but their children: a layout with its own spacing, padding, style
or id is your decision and stays. Click a finding to select it, untick the rules you do not want and
**Apply**. The whole tidy is one edit, so `⌘Z` undoes it.
## Design notes, and the view model they become
Any component can carry a **note**: a sentence about what it should do, such as "must be unique
among customers" or "only managers may change it". Select the component and write it in the yellow
**Note** box at the top of **Properties**. In the file it is the `note:` key:
```yaml
- type: FormField
id: email
label: Email
note: Must be unique among customers.
```
A note is never rendered. The schema documents it, and both the server and the browser render
ignore it. On the canvas the selected component shows its note as a sticky, and **Layers** marks
noted components with ✎.
Notes are what turn a drawn page into a working one. A page made of only a definition has a layout
but no behaviour, and the notes say what that behaviour should be. In the **AI** panel, **Copy
view-model prompt** writes a prompt for your coding agent. It asks for the Java class that the layout
binds to: fields for the `FormField` ids and `@Action` methods for the buttons. Each note goes in as
a requirement for its own component, followed by the layout itself and the line that adds the class
to `routes.yaml`. When the page already has a view model, the prompt asks the agent to complete that
class instead. Share links carry the notes too, and the agent guide asks agents to leave notes where
a person asked for behaviour.
## Checking a page at other widths
The width selector in the toolbar shows the page at the size of a **desktop** (1280 px), a
**tablet** (768 px) or a **phone** (390 px), framed on the canvas. **Fill** takes the whole pane.
Mateu screens adapt to the room they are given: form columns collapse, zones stack and listings
narrow, so it is worth a look before a screen ships. Editing works the same at any width. Components
that adapt to their container, such as form layouts and zones, show their real behaviour. Anything
keyed to the browser window's media queries does not, because the editor's window stays wide. Play
mode starts at the same width and has its own selector.
## The board, and playing the app
Next to **Edit** in the toolbar, **Board** shows the whole mount at once: one card per route, each
a real miniature of its screen, and an arrow wherever one screen takes you to another. The arrows
are read from the files, not drawn by hand, so the board cannot go out of date:
| Arrow | Where it comes from |
|---|---|
| menu | the app shell's `RouteLink`s, nested menus included |
| button / link | a `RouteLink` in a page, such as a button's `actionable` |
| row click | a listing's `rowRoute` (`people/${row.id}` lands on `people/:id`) |
| after save | a REST action's `successRoute` |
| flow step | a `Navigate` step of a declared flow |
| nested route | a route's `children` |
The board flags links whose route does not exist. Pages that no route serves get a dashed card. Each
resource is laid out as a row: the listing, then its record and create form, then the edit form.
Drag the background to pan and scroll to zoom. Drag a card by its header to move it; the board
remembers the arrangement for each mount, and **Auto layout** starts it again. Click a card to label
its arrows. **Edit** opens that card's file: in place in a browser, or in its own tab in IntelliJ
and VS Code.
#### Editing the navigation on the board
The board also edits the mount's navigation. Each action changes the YAML that declares it, and only
the lines involved: comments, quoting and flow `{…}` entries stay as you wrote them. The board then
redraws itself from the files.
- **Create a missing screen.** A link to a route the mount does not have shows as a red dashed card.
**Create screen…** asks for the route, the page file and a template. It writes the page and adds
the route to `routes.yaml` in one step. A route whose page file is missing gets the same button,
and only the file is created. A parameterised link such as `orders/${row.id}` becomes the route
`orders/:id`.
- **Give a page a route.** A page no route serves has a dashed card with **Add route…**, which adds
the entry to `routes.yaml`.
- **Link two screens.** Hover a card and drag the dot on its right edge onto another card, then
choose what the link is. The board only offers what fits the source screen: a **menu entry** from
the app shell, in the group you pick (nested groups included); a **button** on a page, in its
toolbar, its form buttons or its content; a listing's **row click** (`rowRoute`); or where a REST
action lands **after save** (`successRoute`).
- **Delete or re-point an arrow.** Click an arrow to select it. **Delete** (or the `Delete` key)
removes what declares it: the menu entry, the button, the flow step, the `rowRoute` or the
`successRoute`. Drag the circle at its end onto another card to point it there; only the target
text changes. Arrows between nested routes are edited in the routes file.
- **New screen.** Double-click the board's background to create a screen at that spot, from a
template, with its route.
- **Undo.** **Undo** and **Redo** in the toolbar, or `⌘Z` / `⇧⌘Z`, revert the board's last edit in
every file it touched. Undoing a new screen deletes its file again.
When there is no `routes.yaml`, the board creates one and lists it in the mount (`type: UI`). In
IntelliJ and VS Code the edits are ordinary editor edits, undoable in the IDE as well, and saved. In
a browser with no IDE they change the in-browser project.
**▶ Play** runs the mount from the files as you have edited them, so you can click through it like
the app. The menu (including menu entries that run a shell flow), row clicks, buttons and save landings take you where the app would, and a small
address bar with back and forward follows along. A mount that is only YAML needs no backend: play
expands each screen in the browser, the same way a statically deployed bundle does. A route served
by a view model goes to the preview backend, if you have one. The width selector shows the app on a
tablet or a phone, starting at the canvas's width.
Play always runs in the **project's renderer** (a badge in its bar names it), like the canvas. With `renderer: redwood` the whole mount runs in the real Redwood app, framed like
the Redwood canvas but live. Its menu navigates, rows and links open their screens, and Play's
address bar, back and forward move it. The editor is that app's backend. Every call the app makes
comes to the editor, which answers it from the files as you have edited them, using the same
in-browser expansion Play uses for Vaadin. A shell boots as the app shell, and a route loads its
screen. An action that needs a server goes to the preview backend, and with no backend it shows a
toast saying so. Like the Redwood canvas, it needs Oracle's CDN. Play starts on the screen you are editing; a card's **Play** starts on that card.
**Close** (or `Esc`) brings you back.
## Recognising components in the palette
The **Insert** tab shows every component as a card with a picture of it. Hover a card to see it
larger. A selector next to the title picks the look: **Vaadin**, **Redwood** or **Names only**. It
starts with the look of the canvas renderer. The pictures are real renders of a small sample of each
component, regenerated from the catalog: the Vaadin ones by the Vaadin canvas, the Redwood ones by
the [Redwood canvas](#the-redwood-canvas). What you pick is what lands on the canvas. A few
entries have no picture because there is nothing to see on the page: triggers, menu entries, and
embeds that only exist at runtime.
## Share links, and designs from a coding agent
**Share** (toolbar) copies a link that opens the current file in the editor. When the host knows
the whole mount it carries the other files as well. The design travels **inside the link**: it is
compressed into the URL fragment (`#mateuz=…`), which a browser never sends to a server. Nothing is
uploaded, and a statically hosted editor stays static. **Open…** takes a pasted link (or the
bare JSON document) and loads it as an ordinary edit, so `⌘Z` undoes it.
Opening a link in a browser replaces the local draft, and the previous one is kept under
`mateu-visual-editor-yaml.previous`. An IDE editor writes one file, so it loads only the linked file
and tells you when the link carried more.
The same link is how a **coding agent** hands you a screen. In the **AI** panel, describe the screen
and click **Copy agent instruction**, then paste it into Claude Code, Codex or a similar agent. The
instruction points the agent at the [agent guide](https://raw.githubusercontent.com/miguelperezcolom/mateu/master/frontend/web/monorepo/apps/visual-editor/public/agent.md),
which explains the document format, points at the published schemas and gives a Node/Python
one-liner that builds the link. The agent answers with a link on *your* editor; open it with
**Open…**. This is the reverse of the scaffold prompt above it, and just as tool-agnostic:
no API key, nothing in between.
A link opens the editor at the page's own address. To point links at a hosted editor instead, set
`window.__mateuEditorUrl` before the bundle loads.
## Permissions, translations and environments
- **Access rules.** The properties panel of a component has a small editor for `eyesOnly`,
`readOnlyUnless` and `disabledUnless` (roles, groups, scopes, permissions); the app editor's menu
items and the routes editor carry the same editor for `access`. See
[Permissions in YAML](/define/yaml/permissions/). Play has no identity, so it shows
everything.
- **Translation keys.** Label fields offer the project's translation keys (`${i18n.…}`) in their
combo, read from the translation files; you can still type any text. **▶ Play** has a locale
switcher that renders the app in each language your files declare. See
[Translations in YAML](/define/yaml/translations/).
- **Environments.** Environment files are plain YAML: open them in the text editor, validated by
the `type: Environment` branch of the schema. See [Environments](/deploy/environments/).
- **New › Mateu** in IntelliJ and VS Code creates an empty *Translations* or *Environment* file.
## Field types and sample data
Two files make a mount designable with **no backend and no API**:
- **`types.yaml`** (`type: Types`; any `specs/ui` file with that header is read too) — the [field types](/java-ui-definition/field-types/), the domain
vocabulary. It opens in a YAML editor checked against `types-schema.json` (unknown keys, data
types, stereotypes, tones, duplicate ids) with a summary of the types it declares. On a page, a
`FormField` or `GridColumn` picks its **`fieldType`** from a list of the catalogue's ids (or you type
one); the canvas and Play paint the field with the type's attributes, its own ones winning.
- **`sources.yaml`** — each source has a **Sample data** box (JSON or YAML: the response the endpoint
would return) and a **sample file** field. The editor is a design session, so its canvas and Play
always answer sources from their samples: a listing shows the sample rows (searched, sorted and
paged in the browser), a select its options, a save succeeds without persisting. At runtime the
samples are only used when the app opts in — see
[sample data](/concepts/data-sources/#sample-data-designing-without-an-api).
New › Mateu › **Field Types** (IntelliJ and VS Code) creates an empty catalogue.
## Project awareness — the roadmap
Everything above works today; what is still landing is the **"pick, don't type"** half of each
reference. The editors are currently **single-file** — each one edits the file it was opened on and
does not yet know what sibling files the mount contains — so a reference is authored as a string. The
project-awareness work gives the editor a view of the whole mount so it can offer the pickers this
manual describes and validate that a reference resolves.
## See also
- [Route registry](/concepts/routes-and-mounts/) — the routing model the routes file authors.
- [App shell as data](/define/yaml/app-shell/) — the chrome the app file authors.
- [Partials](/define/yaml/partials/) — reusable fragments.
- [YAML UI definition](/define/yaml/pages/) — the component-tree YAML a page is.
- [Permissions in YAML](/define/yaml/permissions/), [Translations in YAML](/define/yaml/translations/), [Environments](/deploy/environments/).
---
# Java
URL: https://mateu.io/java-user-manual/
> Learn Mateu in a guided path: start here, use cases, concepts, build apps, advanced, and real-world patterns.
Use this manual as a guided path.
## 1. Start here
- [Start here](/java-user-manual/start-here/)
- [Quickstart](/java-user-manual/start-here/quickstart/)
- [First app](/java-user-manual/start-here/first-app/)
## 2. See real use cases
- [Admin panel](/java-user-manual/use-cases/admin-panel/)
- [Distributed backoffice](/java-user-manual/use-cases/distributed-backoffice/)
- [Embedded UI](/java-user-manual/use-cases/embedded-ui/)
## 3. Learn the model
- [State, actions and fields](/java-user-manual/concepts/state-actions-and-fields/)
- [Routing and parameters](/java-user-manual/concepts/routing-and-parameters/)
- [UI vs Route](/java-user-manual/concepts/ui-vs-route/)
- [ViewModel lifecycle](/java-user-manual/concepts/viewmodel-lifecycle/)
- [Execution model](/java-user-manual/concepts/execution-model/)
- [Validation](/java-user-manual/concepts/validation/)
- [Field stereotypes](/java-user-manual/concepts/field-stereotypes/)
- [Declarative vs fluent](/java-user-manual/concepts/declarative-vs-fluent/)
- [Fluent components](/java-user-manual/concepts/fluent-components/)
- [Action behavior](/java-user-manual/concepts/action-behavior/)
- [UI effects](/java-user-manual/concepts/ui-effects/)
## 4. Build real apps
- [Application shell](/java-user-manual/build/application-shell/)
- [Navigation and menus](/java-user-manual/build/navigation-and-menus/)
- [Foreign keys and options](/java-user-manual/build/foreign-keys-and-options/)
- [Working with domain models](/java-user-manual/build/domain-models/)
- [Customizing CRUD and listings](/java-user-manual/build/customizing-crud-and-listings/)
- [Full control with Crud](/java-user-manual/build/full-control-crud-orchestrator/)
- [Static bundle](/deploy/static-bundle/)
- [Derived OpenAPI](/java-user-manual/build/derived-openapi/)
## 5. Advanced
- [Rules](/java-user-manual/advanced/rules/)
- [Breadcrumbs](/java-user-manual/advanced/breadcrumbs/)
- [Layout and composition](/java-user-manual/advanced/layout-and-composition/)
- [Custom web components](/java-user-manual/advanced/custom-web-components/)
- [Extensibility](/java-user-manual/advanced/extensibility/)
- [Testing](/java-user-manual/advanced/testing/)
- [Security](/java-user-manual/advanced/security/)
## 6. Real-world
- [Real-world architecture](/java-user-manual/real-world/)
- [Mateu in hexagonal architecture](/java-user-manual/real-world/mateu-in-hexagonal-architecture/)
- [Distributed control plane case study](/java-user-manual/real-world/distributed-control-plane/)
## 7. Reference
- [Reference](/reference/)
- [Key annotations](/reference/key-annotations/)
- [Key interfaces](/reference/key-interfaces/)
- [Fluent model](/reference/fluent-model/)
- [Useful types](/reference/useful-types/)
---
# Advanced
URL: https://mateu.io/java-user-manual/advanced/
The topics here build on the core Mateu model. Read them when you encounter a specific need: client-side behavior, navigation context, custom UI elements, or production concerns like security and testing.
## Topics
- [Rules](/java-user-manual/advanced/rules/) — conditional visibility, status badges, and display mappings evaluated in the browser
- [Breadcrumbs](/java-user-manual/advanced/breadcrumbs/) — static and dynamic navigation context for pages
- [Layout and composition](/java-user-manual/advanced/layout-and-composition/) — control page structure, regions, and inline styling
- [Custom web components](/java-user-manual/advanced/custom-web-components/) — embed any standard web component using `Element.builder()`
- [Extensibility](/java-user-manual/advanced/extensibility/) — override framework internals, add micro-frontends, or plug in an alternative renderer
- [Security](/java-user-manual/advanced/security/) — authenticate with Keycloak and restrict access with `@EyesOnly`
- [Internationalization (i18n)](/java-user-manual/advanced/i18n/) — translate labels, titles and messages via the `Translator` interface
- [Accessibility (WCAG)](/java-user-manual/advanced/accessibility/) — how WCAG compliance is inherited from the design system and where developer responsibility begins
- [Printing](/java-user-manual/advanced/printing/) — control print output with browser `@media print` CSS and a programmatic print action
- [Audit / Change History](/java-user-manual/advanced/audit/) — add a history button to any page by implementing `Auditable`
- [Testing](/java-user-manual/advanced/testing/) — unit-test ViewModels, actions, and repositories with no framework overhead
## Suggested reading order
Start with [Rules](/java-user-manual/advanced/rules/) and [Security](/java-user-manual/advanced/security/) — they apply to nearly every production application. Add [Custom web components](/java-user-manual/advanced/custom-web-components/) and [Extensibility](/java-user-manual/advanced/extensibility/) when you need to go beyond what Mateu renders by default.
## Related
- [Real-world patterns](/java-user-manual/real-world/) — how these techniques apply in hexagonal and microservices architectures
- [Concepts](/java-user-manual/concepts/) — core model reference
---
# Accessibility (WCAG)
URL: https://mateu.io/java-user-manual/advanced/accessibility/
> How Mateu achieves WCAG compliance through the design system, and where developer responsibility begins.
Mateu does not implement accessibility itself. It delegates rendering to a design system — Vaadin or Oracle Redwood — and each of those ships components that are designed to meet **WCAG 2.1 AA**. Keyboard navigation, ARIA attributes, focus management, screen-reader announcements, and colour contrast are all handled at the design system layer.
The practical consequence is: **if you use Mateu's declarative or fluent API to define your UI, your application inherits the accessibility guarantees of the chosen design system without writing any extra code.**
---
## What the design system handles
When Mateu renders a form field, a listing, a dialog, or a toolbar, the output is a design system component, not raw HTML. That component carries:
- Correct `role`, `aria-label`, `aria-describedby`, and `aria-live` attributes
- Keyboard interaction patterns (Tab, arrow keys, Enter, Escape)
- Focus trapping in modals and dialogs
- Screen-reader-friendly status announcements
- Colour tokens that meet contrast requirements
Mateu feeds each component its label, description, and validation messages — the design system wires them to the correct ARIA attributes.
---
## Where developer responsibility begins
The design system guarantee breaks down whenever you introduce content that bypasses the component layer:
### Raw HTML injection
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
The notifications polling pattern (and any use of `Hydratable`) may build HTML strings manually:
```java
// This bypasses the design system — accessibility is your responsibility
content = "You have tasks!";
```
If you inject raw HTML, ensure links have descriptive text, interactive elements are keyboard-reachable, and animations respect `prefers-reduced-motion`.
### Custom web components via `Element.builder()`
```java
Element.builder().name("my-custom-widget").build()
```
Custom elements must implement their own ARIA roles and keyboard behaviour. The design system does not wrap them.
### Raw CSS that affects visibility or contrast
`@Style` passes inline CSS directly to the component. Avoid:
- `opacity: 0` or `visibility: hidden` to hide content that should be hidden from assistive technology — use conditional rendering instead.
- Custom colour combinations that break contrast ratios defined by the theme.
### Image alt text
Images need meaningful alt text — set it on the `Image` component:
```java
Image.builder().src("/logo.png").alt("Acme Corp logo").build()
// or: new Image("/logo.png", "Acme Corp logo")
```
An empty `alt` (`""`) declares the image decorative, so screen readers skip it. The alt text
reaches every renderer: the `]
` alt on the web and in Redwood, the accessibility label on
React Native, the accessible name in the IntelliJ plugin.
---
## Summary
| Layer | Who is responsible |
|---|---|
| Component rendering (buttons, fields, grids, dialogs, …) | Design system (Vaadin, Redwood, …) |
| Labels, descriptions, validation messages | Mateu (passes them to the design system) |
| Raw HTML injected via `Hydratable` or `Text` | Developer |
| Custom web components via `Element.builder()` | Developer |
| Inline CSS that affects contrast or visibility | Developer |
| Image alt text | Developer |
---
## Related
- [Design systems](/design-systems/) — choose and configure the design system for your application
- [Push Notifications](/ux-patterns/notifications/) — the raw HTML injection pattern and its caveats
- [Custom web components](/java-user-manual/advanced/custom-web-components/) — embedding non-design-system elements
---
# Audit / Change History
URL: https://mateu.io/java-user-manual/advanced/audit/
> Add a change-history button to any page by implementing the Auditable interface.
If a ViewModel implements `Auditable`, the framework automatically adds a "History" button to the page toolbar. Clicking it opens a modal with a paginated, filterable listing of changes supplied by the `history()` method.
---
## The interface
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
```java
public interface Auditable {
ListingData history(
String searchText,
AuditFilters filters,
Pageable pageable,
HttpRequest httpRequest);
}
```
The framework calls `history()` when the user searches or pages through the modal listing, passing the current search text, filters, and pagination state.
---
## Provided types
### `AuditFilters`
The filter form shown above the history listing. All fields are optional.
```java
public record AuditFilters(
LocalDate dateFrom,
LocalDate dateTo,
String action,
String field,
String value
) {}
```
| Field | Description |
|---|---|
| `dateFrom` | Show entries on or after this date |
| `dateTo` | Show entries on or before this date |
| `action` | Filter by action label (e.g. `"APPROVE"`, `"DELETE"`, `"SEND"`) |
| `field` | Filter by the name of the changed field |
| `value` | Filter by old or new value |
### `AuditEntry`
The row type rendered in the history listing.
```java
public record AuditEntry(
LocalDateTime when,
String who,
String action,
String field,
String oldValue,
String newValue
) {}
```
| Field | Description |
|---|---|
| `when` | Timestamp of the change |
| `who` | User or system actor that made the change |
| `action` | Free-form action label — can be `"UPDATE"`, `"APPROVE"`, `"ARCHIVE"`, or any domain-specific string |
| `field` | Name of the changed field (empty for record-level actions) |
| `oldValue` | Value before the change |
| `newValue` | Value after the change |
---
## Example
```java
@UI("/customers/:id")
@Service
@Scope("prototype")
public class CustomerForm implements Auditable {
String id;
String name;
String email;
final AuditService auditService;
@Override
public ListingData history(
String searchText,
AuditFilters filters,
Pageable pageable,
HttpRequest httpRequest) {
return auditService.findByEntity("Customer", id, searchText, filters, pageable);
}
@Toolbar
Object save() { /* persist */ }
}
```
`auditService.findByEntity(...)` is your own service — Mateu only defines the contract. The service queries whatever audit store you use (a dedicated audit table, an event store, an external system, etc.) and maps the results to `AuditEntry`.
---
## `action` field conventions
Because `action` is a plain `String`, you can use any labels that fit your domain:
| Action string | When to use |
|---|---|
| `CREATE` | Record first created |
| `UPDATE` | One or more fields changed |
| `DELETE` | Record deleted or soft-deleted |
| `APPROVE` / `REJECT` | Workflow state transitions |
| `SEND` / `EXPORT` | Operations with external side effects |
| `LOGIN` / `LOGOUT` | Session-level events (for user audit) |
There is no enforced vocabulary — use what makes sense for each entity.
---
## Related
- [Key interfaces](/reference/key-interfaces/) — full interface reference
---
# Breadcrumbs
URL: https://mateu.io/java-user-manual/advanced/breadcrumbs/
Breadcrumbs give users navigation context: the sequence of pages leading to the current one.
Mateu supports two approaches: declarative annotations for static paths, and a supplier interface for paths that depend on runtime data.
**When to use:** add breadcrumbs whenever a page is reachable from multiple parent contexts, or when users need to understand where they are within a hierarchy.
---
## Declarative breadcrumbs
:::note[Imports]
The samples leave out their `import` lines and compile with single-type imports. Some names exist
in two Mateu packages (the `@Menu` annotation and the `Menu` record, `@Button` and `Button`…), so
wildcard imports of both packages fail with "reference is ambiguous" — see
[Java imports](/reference/java-imports/).
:::
Use `@Breadcrumbs` and `@Breadcrumb` when the path is always the same regardless of the current record:
```java
@Breadcrumbs({
@Breadcrumb(label = "Home", url = "/"),
@Breadcrumb(label = "Users", url = "/users"),
@Breadcrumb(label = "Detail", url = "/users/123")
})
public class UserDetail {}
```
```yaml
# src/main/resources/specs/ui/routes.yaml
type: Routes
routes:
- route: users/:id
viewModel: com.example.UserDetail
```
This is the simplest option. Use it for fixed hierarchy levels where the labels and URLs do not depend on data.
---
## Imperative breadcrumbs
Implement `BreadcrumbsSupplier` when the breadcrumb labels depend on the current record — for example, showing the entity name instead of a generic "Detail":
```java
public class UserDetail implements BreadcrumbsSupplier {
@Override
public List breadcrumbs(HttpRequest httpRequest) {
String id = httpRequest.lastPathItem(); // users/:id
String name = userQueryService.findNameById(id).orElse("User " + id);
return List.of(
new Breadcrumb("Home", "/"),
new Breadcrumb("Users", "/users"),
new Breadcrumb(name, "") // empty URL = current page (not clickable)
);
}
}
```
The `HttpRequest` gives access to the request path (`path()`, `lastPathItem()`), query parameters, and request headers, so you can look up any data needed to build the labels.
---
## Which to use
| Situation | Approach |
|---|---|
| Labels are always the same | `@Breadcrumbs` annotation |
| Labels depend on path variables or data | `BreadcrumbsSupplier` |
---
## Next
- [Layout and composition](/java-user-manual/advanced/layout-and-composition/) — control page structure and styling
- [Rules](/java-user-manual/advanced/rules/) — client-side field behavior
- [Security](/java-user-manual/advanced/security/) — restrict access to pages and menu entries
---
# Custom web components
URL: https://mateu.io/java-user-manual/advanced/custom-web-components/
Mateu can render any standard web component by declaring it as an `Element` in your component tree. This lets you embed third-party UI libraries, visualisations, or your own custom elements without writing a separate frontend application.
**When to use:** reach for `Element` when Mateu's built-in components are not enough — 3D viewers, maps, rich text editors, chart libraries, design-system widgets, or any component distributed as a web component.
---
## Overview: the full integration pattern
Integrating a custom web component involves up to five steps:
1. **Load the JavaScript** — inject a `
This is my app
The counter below is embedded from a remote Mateu app
This is my footer