Error page layout
One centered shell for 404, 403, 500, maintenance and coming soon, so they read as a family.
<script setup lang="ts">
import ErrorPageLayout from '@shared/components/common/pages/ErrorPageLayout.vue'
import { Button } from '@shared/components/ui/button'
import { Compass } from 'lucide-vue-next'
</script>
<template>
<div class="relative h-[26rem] overflow-hidden rounded-xl border [&>div]:min-h-full">
<ErrorPageLayout
code="404"
title="We cannot find that page"
:icon="Compass"
pattern="mesh"
>
The link may be old, or the record may have been removed. Try the
dashboard, or search for what you were after.
<template #actions>
<Button size="sm" variant="outline">
Go back
</Button>
<Button size="sm">
Open the dashboard
</Button>
</template>
</ErrorPageLayout>
</div>
</template>TIP
The previews on this page hold the layout in a fixed box. On its own it fills the viewport, which is what it does on a real page.
Usage
title is the only required prop. code is the big numeral, and pages without one leave it out. The default slot is the message.
It fills the viewport and paints the app's ground itself, because these pages run under the blank layout with no shell around them.
A background pattern sits behind the column at 6% opacity. pattern="none" turns it off.
Examples
Tones and patterns
tone colors the code and the icon tile: muted, primary, warning or destructive. Body copy never changes color, which is what keeps every one of these pages readable.
<script setup lang="ts">
import ErrorPageLayout from '@shared/components/common/pages/ErrorPageLayout.vue'
import { Button } from '@shared/components/ui/button'
import { Lock, ServerCrash, Wrench } from 'lucide-vue-next'
import { computed, ref } from 'vue'
const PRESETS = {
destructive: { code: '500', title: 'Something broke on our side', icon: ServerCrash, tone: 'destructive', pattern: 'grid' },
warning: { code: '', title: 'Down for maintenance', icon: Wrench, tone: 'warning', pattern: 'waves' },
primary: { code: '403', title: 'You do not have access', icon: Lock, tone: 'primary', pattern: 'dots' },
} as const
const which = ref<keyof typeof PRESETS>('destructive')
const preset = computed(() => PRESETS[which.value])
</script>
<template>
<div class="space-y-3">
<div class="flex flex-wrap gap-2">
<Button
v-for="key in (['destructive', 'warning', 'primary'] as const)"
:key="key"
size="sm"
:variant="which === key ? 'default' : 'outline'"
@click="which = key"
>
{{ key }}
</Button>
</div>
<div class="relative h-[24rem] overflow-hidden rounded-xl border [&>div]:min-h-full">
<ErrorPageLayout
:key="which"
:code="preset.code || undefined"
:title="preset.title"
:icon="preset.icon"
:tone="preset.tone"
:pattern="preset.pattern"
>
We are working on it. This page will come back on its own.
</ErrorPageLayout>
</div>
</div>
</template>API
Source: src/shared/components/common/pages/ErrorPageLayout.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
code | string | — | The large numeral. Omit it for pages with no code. |
title | string | required | The heading. |
icon | Component | — | Drawn in a rounded tile above the code. |
tone | 'muted' | 'primary' | 'warning' | 'destructive' | 'muted' | Colors the code and the tile. |
pattern | 'dots' | 'grid' | 'waves' | 'geometric' | 'mesh' | 'none' | 'mesh' | The background pattern. |
Emits
None.
Slots
| Slot | Description |
|---|---|
default | The message, one sentence. |
extra | Anything richer: a countdown, a status list, a form. |
actions | Buttons under the message, centered and wrapping. |
Accessibility
- The title is the page's
<h1>, so an error page has a heading like any other. - The code carries
dir="ltr"and tabular numerals, so a numeral does not flip in a right-to-left layout or jitter if it counts down. - The message carries
dir="auto". It is a full sentence, and inside a right-to-left shell a trailing full stop is bidi-neutral and would be painted at the start of the line. - The column is capped at a readable width and centered, so the message does not run the width of a desktop screen.