Mado UI templates are copied application source. They are not a page runtime,
route generator or collection of page-sized custom elements. A template
default-exports an ordinary Mado page() and becomes application-owned as soon
as the CLI writes it.
The current template families contain:
application-shell: a responsivelayout()recipe with a skip link, application brand, native navigation and exactly one route-content landmark;authentication-shell: a focusedlayout()recipe for guest flows with a home link and exactly one route-content landmark;list-data-page: a representative collection page composed from the existing page header, filter form, table, pagination and content-state contracts;detail-page: a parameterized record overview with native breadcrumbs, grouped metadata, explicit actions and a real missing-record state;edit-form-page: a parameterized native form using Mado form and mutation state around an explicit application-owned persistence seam;settings-page: a native profile, locale and preferences form with explicit labels, descriptions and application-owned persistence.
No recipe chooses an identity provider, data access, API shape or domain language. Those decisions remain in the application.
Initialize the neutral template target once, then install the recipes:
npx @madojs/ui@latest init
npx @madojs/ui@latest add \
application-shell authentication-shell detail-page edit-form-page \
list-data-page settings-pageWith the default project paths, the copied application source is:
src/
├── styles/
│ ├── mado-ui-application-shell.css
│ ├── mado-ui-authentication-shell.css
│ ├── mado-ui-alert.css
│ ├── mado-ui-breadcrumbs.css
│ ├── mado-ui-button.css
│ ├── mado-ui-content-state.css
│ ├── mado-ui-description-list.css
│ ├── mado-ui-field.css
│ ├── mado-ui-filter-bar.css
│ ├── mado-ui-form-controls.css
│ ├── mado-ui-form-section.css
│ ├── mado-ui-layout.css
│ ├── mado-ui-navigation-list.css
│ ├── mado-ui-page-header.css
│ ├── mado-ui-pagination.css
│ ├── mado-ui-panel.css
│ ├── mado-ui-settings-row.css
│ ├── mado-ui-table.css
│ └── mado-ui-theme.css
└── templates/
├── layouts/
│ ├── application-shell.layout.ts
│ └── authentication-shell.layout.ts
└── pages/
├── detail.page.ts
├── edit-form.page.ts
├── list-data.page.ts
└── settings.page.ts
Import the installed styles once from the application's src/main.ts. The
CLI prints this list after add, and mado-ui doctor checks every locked CSS
file:
import "./styles/mado-ui-theme.css";
import "./styles/mado-ui-layout.css";
import "./styles/mado-ui-navigation-list.css";
import "./styles/mado-ui-application-shell.css";
import "./styles/mado-ui-authentication-shell.css";
import "./styles/mado-ui-alert.css";
import "./styles/mado-ui-breadcrumbs.css";
import "./styles/mado-ui-button.css";
import "./styles/mado-ui-content-state.css";
import "./styles/mado-ui-description-list.css";
import "./styles/mado-ui-field.css";
import "./styles/mado-ui-filter-bar.css";
import "./styles/mado-ui-form-controls.css";
import "./styles/mado-ui-form-section.css";
import "./styles/mado-ui-page-header.css";
import "./styles/mado-ui-pagination.css";
import "./styles/mado-ui-panel.css";
import "./styles/mado-ui-settings-row.css";
import "./styles/mado-ui-table.css";If mado-ui.json uses another styles path, adjust only the import prefix.
The CLI never edits main.ts or the route manifest.
Both shell files are Mado layout pages. Mount them only through layout() in
src/app.routes.ts; do not render a second shell around routes.view in
main.ts.
// src/app.routes.ts
import { layout, routes } from "@madojs/mado";
import { guestOnly, requireAuth } from "./modules/auth/auth.guard";
export const manifest = {
"/auth": layout({
layout: () =>
import("./templates/layouts/authentication-shell.layout"),
guard: guestOnly,
routes: {
"/sign-in": () => import("./pages/sign-in.page"),
},
}),
"": layout({
layout: () =>
import("./templates/layouts/application-shell.layout"),
guard: requireAuth,
routes: {
"/": () => import("./pages/overview.page"),
"/records": () =>
import("./templates/pages/list-data.page"),
"/records/:id": () =>
import("./templates/pages/detail.page"),
"/records/:id/edit": () =>
import("./templates/pages/edit-form.page"),
"/settings": () =>
import("./templates/pages/settings.page"),
},
}),
"*": () => import("./pages/not-found.page"),
};
export default routes(manifest);Here /auth/sign-in belongs to an explicit guest layout group, while /,
/records and /settings belong to the guarded empty-prefix application
group. Keep the explicit auth group before that catch-all group. guestOnly
and requireAuth are application guards: neither copied shell knows how
identity is stored or where redirects lead.
The shell intentionally contains three obvious copy-time decisions:
- replace the
Applicationbrand and decide whether it links to the public landing page or the application overview; - replace
navigationItemswith the real application map; - keep
routeUrl()anddata-linkon internal navigation links.
Its <main> renders ${child} exactly once. Route pages should therefore own
their content headings and sections, but must not introduce another main
landmark.
The authentication shell has two copy-time decisions: replace its concise
brand and home path, then keep routeUrl() and data-link on the internal
link. It also renders ${child} exactly once. A sign-in, registration or
recovery page owns its own form; the shell never owns credentials, session
state or redirection.
If the application group belongs under /app, update navigationItems to
/app, /app/records and /app/settings. Also update recordsPath (and the
derived create/detail/edit URLs) in all three record-page recipes. Keeping one
prefix across the route manifest and copied constants prevents breadcrumbs,
Edit and Cancel links from leaving the guarded zone.
list-data.page.ts demonstrates composition, not a data architecture. Before
shipping it:
- replace
records,RecordSummaryand the local filter with application-owned data; - replace
recordsPathandcreateRecordPathwith real manifest paths; - keep shareable filter and pagination state in the URL when that matches the product;
- preserve the native table caption, column and row headers;
- render an honest empty, loading or recoverable error state according to the application's actual async lifecycle.
The included search remains a native method="get" form. It works as a normal
navigation without JavaScript and lets Mado's queryParam() reflect the same
URL after takeover. The sample page deliberately contains no fetch(),
resource() or API client because the registry cannot know the consumer's
backend.
Both record pages take params.id from a dynamic route. Their small local
record collections are readable fixtures, not a data layer. Replace each
lookup with the owning module's resource or loaded page data, and keep the
missing branch until the real loader has an equally honest not-found
contract. Replace each copied recordsPath with the same route prefix used by
the list page and application shell.
The detail recipe preserves document semantics:
- the current breadcrumb is text with
aria-current="page"; - the page has one
h1, while each panel has its ownh2; - metadata remains a native
dland dates remaintime; - Edit is an ordinary internal link, not a click handler.
The edit recipe keeps HTML constraints as the source of truth and uses
useForm() only for reactive values, touched errors and submission state. Its
mutation() wraps an exported PersistRecord function. The default function
fails with an actionable error on purpose: a copied page must never claim that
it saved data before the application connects its real persistence boundary.
This recipe requires Mado >=0.17.0 <0.21.0. It relies on lifecycle-owned
mutation() teardown and preserves native minlength/maxlength interaction
state when .value mirrors form state.
Connect the page locally after copying:
// src/templates/pages/edit-form.page.ts
const persistRecord: PersistRecord = async (
values,
signal,
) => recordsConnector.update(values.id, values, { signal });
const editFormPage = createEditFormPage(persistRecord);
export default editFormPage;Replace the recipe's unconfigured default export with that block. Alternatively, keep the copied factory and inject the module function from a thin route page:
import {
createEditFormPage,
} from "../../templates/pages/edit-form.page";
import { updateRecord } from "./records.connector";
export default createEditFormPage((values, signal) =>
updateRecord(values.id, values, { signal })
);Mado disposes the in-flight mutation when its route is disposed; the copied page does not duplicate that lifecycle with a manual cleanup hook. It does not redirect, invalidate resource keys or reset edited values after success: those policies belong to the application route and data modules. Success and error feedback is tied to a snapshot of the submitted values. It disappears as soon as the current form becomes a different, unsaved draft; editing does not abort a POST that may already have reached the server.
settings.page.ts is deliberately a native method="post" form. Its example
values make the copied structure visible, but they are not user or account
data. Before shipping it:
- replace the profile and regional defaults with values owned by the application;
- keep every control's explicit label and description relationship;
- remove, rename or extend settings to match the product rather than treating this list as a schema;
- connect the POST destination or progressively enhance submission at the application's action boundary;
- retain a real native-submit path whenever the deployment can support one.
The reset button restores the form's authored initial values. If an
application later mirrors settings through reactive state, it must also make
reset update that state or remove the reset action. The recipe has no
@submit, mutation, success message or timer because it cannot honestly know
whether anything was persisted.
Passwords, multi-factor authentication, active sessions and destructive account actions intentionally do not appear here. They need separate, product-specific threat models and confirmation flows.
The authentication shell is presentation, not authentication. Put a guest
guard on its layout() group and an authenticated guard on the application
shell group, as in the route example above. The application owns session
loading, redirects, return URLs and authorization. Do not add those concerns
to either copied layout.
Guarded routes cannot be static snapshots. Public catalog preview routes use fixture content and no guards; a real application should test its guest and protected groups against its own identity service.
The application shell exposes a small set of copy-safe CSS properties:
--mado-ui-application-shell-background
--mado-ui-application-shell-border
--mado-ui-application-shell-brand-color
--mado-ui-application-shell-color
--mado-ui-application-shell-content-max-inline-size
--mado-ui-application-shell-focus-ring
--mado-ui-application-shell-header-background
--mado-ui-application-shell-header-padding
--mado-ui-application-shell-main-padding
--mado-ui-application-shell-sidebar-background
--mado-ui-application-shell-sidebar-padding
--mado-ui-application-shell-sidebar-width
--mado-ui-application-shell-skip-background
--mado-ui-application-shell-skip-border
--mado-ui-application-shell-skip-color
--mado-ui-application-shell-skip-radius
The authentication shell exposes equivalent focused-frame controls:
--mado-ui-authentication-shell-background
--mado-ui-authentication-shell-border
--mado-ui-authentication-shell-brand-color
--mado-ui-authentication-shell-color
--mado-ui-authentication-shell-content-max-inline-size
--mado-ui-authentication-shell-content-padding
--mado-ui-authentication-shell-content-padding-wide
--mado-ui-authentication-shell-focus-ring
--mado-ui-authentication-shell-header-max-inline-size
--mado-ui-authentication-shell-header-padding
--mado-ui-authentication-shell-header-padding-wide
--mado-ui-authentication-shell-main-padding
--mado-ui-authentication-shell-main-padding-wide
--mado-ui-authentication-shell-radius
--mado-ui-authentication-shell-shadow
--mado-ui-authentication-shell-skip-background
--mado-ui-authentication-shell-skip-border
--mado-ui-authentication-shell-skip-color
--mado-ui-authentication-shell-skip-radius
--mado-ui-authentication-shell-surface
--mado-ui-authentication-shell-wash
Both stylesheets fall back to the shared OKLCH theme tokens and system colors in forced-colors mode. Since the files are application-owned, structural changes do not require an override layer: edit the copied layout and stylesheet directly.
- Templates may depend on foundations, primitives and blocks.
- Registry source imports only the public
@madojs/madoAPI and browser platform APIs. - Templates never add a runtime dependency to
@madojs/ui. - The CLI copies files and dependencies but never invents routes, guards, APIs or authentication.
- Catalog previews run on isolated internal routes so the demonstration
application and authentication shells cannot nest a second
<main>inside the catalog shell.