Not found
The page for a wrong address or an error: what happened in plain words, a search box, and the way home.
Install
If this is the first component in the project, set up once. This writes ermine.json and copies the theme stylesheet. The installation guide covers Vue, Nuxt and Astro.
npx ermineui initpnpm dlx ermineui inityarn dlx ermineui initbunx ermineui initThen add the block:
npx ermineui add not-foundpnpm dlx ermineui add not-foundyarn dlx ermineui add not-foundbunx ermineui add not-foundThat copies 2 files into src/components/blocks/not-found/, and the 2 components it is built from. Files you already have are kept.
Use
import { NotFound } from '@/components/blocks/not-found'<SignUp :loading="loading" :error="error" terms-href="/terms" sign-in-href="/sign-in" @submit="createAccount" />
<ForgotPassword v-model:sent="sent" :loading="loading" @submit="sendLink" />
<NotFound search @search="(query) => router.push({ path: '/search', query: { q: query } })" />In Nuxt, with the auto-import setting, the tags work in templates without the import. In Astro, use it inside a Vue island.
What gets copied
| File | Where it goes |
|---|---|
NotFound.vue | src/components/blocks/not-found/NotFound.vue |
index.ts | src/components/blocks/not-found/index.ts |
It is built from: Button, Input group. These are added with it.
The folders are the suggested ones. ermine.json says where yours are.
Source
This is exactly what the command copies. To install by hand, create each file below and the helpers listed above.
NotFound.vue 131 lines
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '../../ui/button'
import { SearchInput } from '../../ui/input-group'
withDefaults(
defineProps<{
/** The status, shown large: "404", "500", or a word such as "Offline". */
code?: string
title?: string
description?: string
/** Where the main button goes. */
homeHref?: string
homeLabel?: string
/** Show a search box, for finding what the address was meant to lead to. */
search?: boolean
searchLabel?: string
/** Show a "Try again" button, which emits `retry`: for an error that may pass, rather than a wrong address. */
retry?: boolean
/** Heading level of the title. A page of its own wants h1. */
titleAs?: string
}>(),
{
code: '404',
title: 'Page not found',
description: 'The page you were looking for has moved, or never existed. Check the address, or try one of these.',
homeHref: '/',
homeLabel: 'Back to home',
searchLabel: 'Search the site',
titleAs: 'h1',
},
)
const emit = defineEmits<{
/** Something was searched for. Send the reader to your search page. */
search: [query: string]
/** "Try again" was pressed. Reload, or retry what failed. */
retry: []
}>()
const query = ref('')
</script>
<template>
<section class="er-not-found">
<!-- The number is decoration: the title says the same thing in words. -->
<p class="er-not-found-code" aria-hidden="true">{{ code }}</p>
<component :is="titleAs" class="er-not-found-title">{{ title }}</component>
<p v-if="description" class="er-not-found-description">{{ description }}</p>
<form v-if="search" class="er-not-found-search" role="search" @submit.prevent="query.trim() && emit('search', query.trim())">
<SearchInput v-model="query" :aria-label="searchLabel" :placeholder="searchLabel" />
<Button type="submit" variant="outline">Search</Button>
</form>
<div class="er-not-found-actions">
<Button as="a" :href="homeHref">{{ homeLabel }}</Button>
<Button v-if="retry" variant="outline" @click="emit('retry')">Try again</Button>
<!-- More ways on: links to the places people most often want. -->
<slot />
</div>
</section>
</template>
<style>
@layer components {
.er-not-found {
display: grid;
justify-items: center;
gap: 0.75rem;
width: 100%;
max-width: 32rem;
margin-inline: auto;
padding-block: 2rem;
text-align: center;
}
.er-not-found-code {
margin: 0;
color: var(--er-muted-foreground);
font-family: var(--er-font-display);
font-size: clamp(4rem, 22cqi, 7rem);
font-weight: var(--er-weight-display);
font-variant-numeric: tabular-nums;
letter-spacing: calc(-0.04em * var(--er-tracking-heading));
line-height: 1;
}
.er-not-found-title {
margin: 0;
font-family: var(--er-font-display);
font-size: 1.5rem;
font-weight: var(--er-weight-heading);
letter-spacing: calc(-0.01em * var(--er-tracking-heading));
line-height: 2rem;
text-wrap: balance;
}
.er-not-found-description {
margin: 0;
color: var(--er-muted-foreground);
font-size: 0.9375rem;
line-height: 1.5;
text-wrap: pretty;
}
.er-not-found-search {
display: flex;
gap: 0.5rem;
width: 100%;
max-width: 24rem;
margin-top: 0.5rem;
text-align: left;
}
.er-not-found-search .er-search-input {
flex: 1;
min-width: 0;
}
.er-not-found-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 0.5rem;
}
}
</style>index.ts 3 lines
/** Not found — the page for a wrong address or an error: what happened in plain words, a search box, and the way home. */
export { default as NotFound } from './NotFound.vue'