Pages 404, 500 et 503 prêtes à l’emploi dans le registre éditorial : le code de statut composé comme un chiffre, un titre bicolore, une fiche technique réglée (ID de requête à copier, fenêtre de maintenance dans le fuseau horaire du visiteur), recherche du site ou inscription aux alertes, et des liens utiles ; mise en page scindée en option avec une planche illustrée.
Agent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["error-page"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :
<mv-tabs id="ep-demo" value="not-found" data-size="sm" style="gap:0">
<style>
#ep-demo > [role="tablist"] { justify-self: center; margin: .75rem auto; }
#ep-demo .mv-error-page { --mv-error-page-min-height: 0; }
</style>
<div role="tablist" aria-label="Error page variant">
<button role="tab" data-value="not-found">Not found</button>
<button role="tab" data-value="server-error">Server error</button>
<button role="tab" data-value="maintenance">Maintenance</button>
<button role="tab" data-value="split">Split</button>
</div>
<!-- ═════════ 404: not found (default) ═════════ -->
<div role="tabpanel" data-value="not-found">
<mv-error-page>
<section class="mv-error-page" data-variant="not-found" aria-labelledby="ep-nf-title">
<div class="mv-error-page-inner">
<p class="mv-error-page-meta">
<span class="mv-error-page-kicker">Error 404</span>
<span>Page not found</span>
<span class="mv-error-page-path">/pricing/enterprize</span>
</p>
<p class="mv-error-page-code" aria-hidden="true">404</p>
<div class="mv-error-page-content">
<h1 class="mv-error-page-title" id="ep-nf-title">We can’t find that page. <span class="mv-error-page-muted">It may have moved.</span></h1>
<p class="mv-error-page-lead">The link may be out of date, or the address has a typo. Search the site, or pick up again from one of the pages below.</p>
<form class="mv-error-page-form" role="search" action="#ep-demo" data-demo-search>
<label class="mv-sr-only" for="ep-nf-q">Search Ledgerline</label>
<div class="mv-input-group" data-size="lg">
<label class="mv-input-group-addon" for="ep-nf-q"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg></label>
<input class="mv-input" id="ep-nf-q" name="q" type="search" placeholder="Search guides, pricing and docs" autocomplete="off">
<button class="mv-button" data-size="sm" type="submit">Search</button>
</div>
</form>
<p class="mv-error-page-note">Did you mean <a href="#ep-demo"><span class="mv-error-page-path">/pricing/enterprise</span></a>?</p>
</div>
<nav class="mv-error-page-links" aria-label="Helpful pages">
<ul>
<li><a href="#ep-demo"><span class="mv-error-page-link-label">Home</span><span class="mv-error-page-link-text">Product tour, customers and plans</span></a></li>
<li><a href="#ep-demo"><span class="mv-error-page-link-label">Documentation</span><span class="mv-error-page-link-text">Guides, API reference and changelog</span></a></li>
<li><a href="#ep-demo"><span class="mv-error-page-link-label">Help center</span><span class="mv-error-page-link-text">Answers from our support team, 24/7</span></a></li>
<li><a href="#ep-demo"><span class="mv-error-page-link-label">System status</span><span class="mv-error-page-link-text">Live uptime for every region</span></a></li>
</ul>
</nav>
</div>
</section>
</mv-error-page>
</div>
<!-- ═════════ 500: server error ═════════ -->
<div role="tabpanel" data-value="server-error">
<mv-error-page id="ep-se">
<section class="mv-error-page" data-variant="server-error" aria-labelledby="ep-se-title">
<div class="mv-error-page-inner">
<p class="mv-error-page-meta">
<span class="mv-error-page-kicker">Error 500</span>
<span>Internal server error</span>
<a href="#ep-demo">status.ledgerline.com →</a>
</p>
<p class="mv-error-page-code" aria-hidden="true">500</p>
<div class="mv-error-page-content">
<h1 class="mv-error-page-title" id="ep-se-title">Something broke on our side. <span class="mv-error-page-muted">Your data is safe.</span></h1>
<p class="mv-error-page-lead">The server hit an unexpected error while loading your invoices. Our on-call engineers have been alerted, and trying again usually works within a minute.</p>
<div class="mv-error-page-actions">
<a class="mv-button" href="" data-retry>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 4v5h-5"/></svg>
<span data-retry-label>Try again</span>
</a>
<a class="mv-button" data-variant="outline" href="#ep-demo">View system status</a>
</div>
<p class="mv-error-page-status" id="ep-se-status" role="status" aria-live="polite"></p>
<p class="mv-error-page-note">Still stuck? <a href="#ep-demo">Contact support</a> and include the request ID below.</p>
</div>
<dl class="mv-error-page-facts">
<div><dt>Status</dt><dd>500 Internal Server Error</dd></div>
<div><dt>Request ID</dt><dd><code id="ep-se-ref">req_01J8X4K2Q7M9WZ</code><mv-copy-button target="#ep-se-ref" data-variant="ghost" label="Copy request ID" success-label="Request ID copied"></mv-copy-button></dd></div>
<div><dt>Time</dt><dd><time datetime="2026-09-25T14:32:08Z">Sep 25, 2026, 2:32 PM UTC</time></dd></div>
</dl>
</div>
</section>
</mv-error-page>
</div>
<!-- ═════════ 503: maintenance ═════════ -->
<div role="tabpanel" data-value="maintenance">
<mv-error-page id="ep-mt">
<section class="mv-error-page" data-variant="maintenance" aria-labelledby="ep-mt-title">
<div class="mv-error-page-inner">
<p class="mv-error-page-meta">
<span class="mv-error-page-kicker">Scheduled maintenance</span>
<span>Error 503</span>
<a href="#ep-demo">status.ledgerline.com →</a>
</p>
<p class="mv-error-page-code" aria-hidden="true">503</p>
<div class="mv-error-page-content">
<h1 class="mv-error-page-title" id="ep-mt-title">We’re down for planned maintenance. <span class="mv-error-page-muted">Back shortly.</span></h1>
<p class="mv-error-page-lead">We’re moving every workspace to faster storage. <strong>Nothing is lost</strong>, and bank feeds catch up on their own once we’re back.</p>
<form class="mv-error-page-form" id="ep-mt-form" action="#ep-demo" novalidate>
<label for="ep-mt-email">Get an email when we’re back</label>
<div class="mv-input-group" data-size="lg">
<label class="mv-input-group-addon" for="ep-mt-email"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg></label>
<input class="mv-input" id="ep-mt-email" name="email" type="email" placeholder="[email protected]" autocomplete="email" aria-describedby="ep-mt-status">
<button class="mv-button" data-size="sm" type="submit">Notify me</button>
</div>
<p class="mv-error-page-status" id="ep-mt-status" role="status" aria-live="polite"></p>
</form>
<p class="mv-error-page-note">Live progress on the <a href="#ep-demo">status page</a>. One email, then we delete your address.</p>
</div>
<dl class="mv-error-page-facts">
<div><dt>Started</dt><dd><time id="ep-mt-start" datetime="2026-09-25T13:00:00Z">Sep 25, 1:00 PM UTC</time></dd></div>
<div><dt>Back by</dt><dd><time id="ep-mt-end" datetime="2026-09-25T16:30:00Z" data-relative>Sep 25, 4:30 PM UTC</time></dd></div>
<div><dt>Affected</dt><dd>Web app, API and mobile sync</dd></div>
</dl>
</div>
</section>
</mv-error-page>
</div>
<!-- ═════════ Split layout (combinable with every variant) ═════════ -->
<div role="tabpanel" data-value="split">
<mv-error-page>
<section class="mv-error-page" data-variant="not-found" data-layout="split" aria-labelledby="ep-sp-title" style="--mv-error-page-bg:var(--mv-bg-subtle)">
<div class="mv-error-page-inner">
<div class="mv-error-page-main">
<p class="mv-error-page-meta">
<span class="mv-error-page-kicker">Error 404</span>
<span>Careers</span>
</p>
<div class="mv-error-page-content">
<h1 class="mv-error-page-title" id="ep-sp-title">This role has been filled. <span class="mv-error-page-muted">Fourteen others are open.</span></h1>
<p class="mv-error-page-lead">The posting for <strong>Senior Product Designer, Lisbon</strong> closed on August 14. Similar roles are listed below, and new ones open every month.</p>
<div class="mv-error-page-actions">
<a class="mv-button" href="#ep-demo">See open roles</a>
<a class="mv-button" data-variant="ghost" href="#ep-demo">Get job alerts →</a>
</div>
</div>
<nav class="mv-error-page-links" aria-label="Similar roles">
<ul>
<li><a href="#ep-demo"><span class="mv-error-page-link-label">Product Designer</span><span class="mv-error-page-link-text">Berlin or remote in Europe</span></a></li>
<li><a href="#ep-demo"><span class="mv-error-page-link-label">Design Systems Lead</span><span class="mv-error-page-link-text">Lisbon, hybrid</span></a></li>
</ul>
</nav>
</div>
<figure class="mv-error-page-visual" aria-hidden="true">
<div class="mv-error-page-plate">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMaxYMin meet" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M40 150 C 90 150, 100 92, 150 92 S 220 132, 262 118 S 318 64, 340 58" stroke-dasharray="3 6"/>
<rect x="36" y="146" width="8" height="8" fill="currentColor" stroke="none"/>
<rect x="146" y="88" width="8" height="8" fill="currentColor" stroke="none"/>
<rect x="258" y="114" width="8" height="8" fill="currentColor" stroke="none"/>
<g style="color:var(--mv-error-page-accent)">
<rect x="328" y="46" width="24" height="24" stroke-dasharray="4 4"/>
<path d="M336 54l8 8M344 54l-8 8"/>
</g>
</svg>
<p class="mv-error-page-code">4<span>0</span>4</p>
</div>
<figcaption class="mv-error-page-caption"><span><b>Fig. 404</b> · /careers/senior-product-designer-lisbon</span><span>Closed Aug 14</span></figcaption>
</figure>
</div>
</section>
</mv-error-page>
</div>
<script type="module">
const root = document.querySelector("#ep-demo");
// Search: stays on the page in this demo (a real form goes to /search).
for (const form of root.querySelectorAll("[data-demo-search]")) form.addEventListener("submit", (e) => e.preventDefault());
// Retry: cancel the reload here and report the attempt instead.
const seStatus = root.querySelector("#ep-se-status");
let seTimer;
root.querySelector("#ep-se")?.addEventListener("mv-retry", (e) => {
e.preventDefault();
const control = e.detail.control;
control.setAttribute("aria-busy", "true");
control.setAttribute("aria-disabled", "true");
clearTimeout(seTimer);
seTimer = setTimeout(() => {
control.removeAttribute("aria-busy");
control.removeAttribute("aria-disabled");
const at = new Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" }).format(new Date());
seStatus.dataset.state = "error";
seStatus.textContent = `Still unavailable at ${at}. We’ll keep the page ready for your next try.`;
}, 900);
});
// Maintenance window around the current time, so the relative note reads well.
const step = 15 * 60_000, now = Date.now();
const start = new Date(Math.floor((now - 50 * 60_000) / step) * step);
const end = new Date(Math.ceil((now + 2 * 3600_000) / step) * step);
const fallback = (d) => new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", timeZone: "UTC", timeZoneName: "short" }).format(d);
for (const [id, d] of [["#ep-mt-start", start], ["#ep-mt-end", end]]) {
const time = root.querySelector(id);
if (time) { time.dateTime = d.toISOString(); time.textContent = fallback(d); }
}
customElements.whenDefined("mv-error-page").then(() => root.querySelector("#ep-mt")?.refresh?.());
// Alert signup: validate and confirm in the live region.
const form = root.querySelector("#ep-mt-form");
const status = root.querySelector("#ep-mt-status");
const input = form?.querySelector("input");
form?.addEventListener("submit", (e) => {
e.preventDefault();
const email = input.value.trim();
if (!email || !input.checkValidity()) {
status.dataset.state = "error";
status.textContent = "Enter an email address like [email protected].";
input.setAttribute("aria-invalid", "true");
return;
}
input.removeAttribute("aria-invalid");
input.value = "";
status.dataset.state = "success";
status.textContent = `Done. We’ll write to ${email} as soon as we’re back.`;
});
</script>
</mv-tabs>
API
Attributes
Name
Type
Default
Description
data-variant
not-found | server-error | maintenance
not-found
On .mv-error-page: the kind of error. Sets the color of the kicker mark (accent, danger, warning); the content pattern of each kind is shown in the demo.
data-layout
split
On .mv-error-page: text in .mv-error-page-main on one side, figure.mv-error-page-visual on the other (from 56rem). Works with every variant.
data-motif
grid | dots | none
grid
On .mv-error-page-plate: background motif, faded toward the code.
data-retry
boolean
On a control (an <a href=""> reloads even without JS): with <mv-error-page>, fires mv-retry, then reloads the page. Put the label in a [data-retry-label] child to have it replaced while reloading.
data-relative
boolean
On a <time datetime>: <mv-error-page> adds a relative note after it (“in 2 hours”), refreshed every minute.
data-format
auto | time | date
auto
On a <time datetime>: auto shows the time alone when it is today, else weekday, date and time, always with the viewer's time zone.
locale
string
On <mv-error-page>: locale for dates and relative times. Default: the nearest lang attribute, then en-US.
Properties
Name
Type
Description
strings
object
Override the default text: retrying (“Reloading…”), soon (“any minute now”), overdue (“taking longer than planned”). Can be set before the element is defined.
Methods
Name
Description
refresh()
Re-format every <time datetime> and relative note (call it after changing a datetime).
Events
Name
Description
mv-retry
Cancelable, fired by a [data-retry] control before the reload; detail = { control }. Call preventDefault() to retry in place (refetch in a single-page app).
Content structure
Name
Description
.mv-error-page-inner
Default layout: -meta, -code, -content, then optionally -facts and -links. From 48rem the code and facts sit left of the content.
.mv-error-page-content
Copy: -title (with -muted), -lead, then -actions, -form, -status and -note as needed.
form.mv-error-page-form
Site search (role="search") or an alert signup: a label (visible or .mv-sr-only), an .mv-input-group with its button, and an optional -status.
dl.mv-error-page-facts
Ruled spec sheet: div > dt (mono label) + dd. Holds the status line, a request ID with <mv-copy-button>, or the maintenance window in <time> elements.
nav.mv-error-page-links
Helpful links: ul > li > a > -link-label + -link-text. 1, 2, then up to 4 columns.
figure.mv-error-page-visual
Split layout: .mv-error-page-plate (motif, optional <svg> or <img> behind, the -code) + figcaption.mv-error-page-caption.
CSS classes
Name
Description
mv-error-page
Root section, a query container named mv-error-page.
mv-error-page-meta / -kicker
Mono uppercase meta line on a hairline; the kicker gets a small colored square. A trailing item (path, status link) is pushed to the end.
mv-error-page-code
The status code set like a figure. Decorative: add aria-hidden="true", the kicker and title carry the meaning. A <span> inside greys one digit.
mv-error-page-path
Mono path or identifier inside running text or the meta line; long values wrap.
mv-error-page-status
Form or retry feedback (role="status"), hidden when empty; data-state="success | error" colors it.
mv-error-page-main
Split layout: wraps the text side (meta, content, facts, links).
mv-error-page-relative
Relative note added after a <time data-relative>.
CSS variables
Name
Default
Description
--mv-error-page-max
72rem
Max content width.
--mv-error-page-min-height
100svh
Minimum height; content is centered vertically. Set 0 inside a layout with a header.
--mv-error-page-bg
var(--mv-bg)
Section background.
--mv-error-page-rule
var(--mv-border-strong)
Color of the main hairlines (meta, facts, links).
--mv-error-page-accent
var(--mv-accent)
Kicker mark of the not-found variant, and the accent of a plate illustration.
Accessibility
One <h1> per page: the title states the problem in words, the kicker names the error, and the big code is aria-hidden, so the meaning never rests on the number alone. Set document.title to match (“Page not found · Ledgerline”) and return the real HTTP status from the server (404, 500, 503 with Retry-After) so crawlers and assistive tech are not told the page worked. The search form has role="search" and a label; the links sit in a labeled <nav>; form and retry feedback go to a polite role="status" region, errors spelled out in text. The retry control gets aria-busy and aria-disabled while it reloads. Times are real <time datetime> elements, rewritten in the viewer's time zone with a full-date tooltip. Keyboard: native links, inputs and buttons only. The plate and its illustration are aria-hidden. Reduced motion: no entrance. Forced colors: marks and hairlines use system colors, the motif is dropped.