Legal document layout
A dated header, numbered sections and a contents rail. The shell for terms and privacy.
<script setup lang="ts">
import type { LegalSection } from '@shared/components/common/pages/LegalDocumentLayout.vue'
import LegalDocumentLayout from '@shared/components/common/pages/LegalDocumentLayout.vue'
const SECTIONS: LegalSection[] = [
{
id: 'accounts',
title: 'Accounts',
body: [
'You are responsible for the activity on your account and for keeping your credentials safe.',
'Tell us as soon as you believe an account has been used without permission.',
],
},
{
id: 'acceptable-use',
title: 'Acceptable use',
body: ['There are a few things you may not do with the service.'],
list: [
'Resell access without a written agreement',
'Probe or overload the infrastructure',
'Upload anything you do not have the right to upload',
],
},
{
id: 'termination',
title: 'Termination',
body: ['Either side may end the agreement with notice. Your data stays available to export for thirty days.'],
},
]
</script>
<template>
<div class="h-[30rem] overflow-hidden rounded-xl border">
<LegalDocumentLayout
title="Terms of service"
updated="3 March 2026"
intro="These terms cover how the service may be used and what each side is responsible for."
:sections="SECTIONS"
/>
</div>
</template>TIP
The preview on this page holds the layout in a fixed box and scrolls inside it. On its own it fills the viewport, which is what it does on a real page.
Usage
The document is data, not markup. Pass sections as LegalSection[] and the layout numbers them, renders the paragraphs and the optional bullets, and builds the contents rail from the same array, so the two cannot disagree.
Like the error layout it runs under the blank layout, so it paints the app's ground itself.
updated is a pre-formatted string rather than a Date. An effective date is part of the document's text, and rendering it through the app's language would change what the document says when someone switches.
LegalSection
| Key | Type | Description |
|---|---|---|
id | string | The anchor, and what the contents list links to. |
title | string | The section heading, numbered automatically. |
body | string[] | Paragraphs, in order. |
list | string[] | An optional bullet list after the paragraphs. |
The placeholder notice
The layout prints a standing notice that the sections are placeholder copy. A template ships with filler here by necessity, and saying so plainly is more useful to a buyer than letting it read as real legal text they might leave in place.
API
Source: src/shared/components/common/pages/LegalDocumentLayout.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | required | The document's title. |
updated | string | required | The effective date, already formatted. |
intro | string | required | One paragraph under the title. |
sections | LegalSection[] | required | The document. |
Emits
None.
Slots
None.
Accessibility
- The title is the
<h1>and each section is an<h2>, so the document has a real outline to navigate by. - The contents rail is a
<nav>named by its own heading, and it is a numbered list, so its order is announced. - Section headings are anchor targets with scroll margin, so jumping to one does not land it under the top of the window.
- Clause numbers carry
dir="ltr"and tabular numerals, so "10." does not render as ".10" in a right-to-left layout. - Every text run carries
dir="auto", so a paragraph resolves its direction from its own first strong character. - The contents rail is hidden below
lg. On a phone a long contents list is a screen of chrome before the document starts. - The back link goes to the app rather than calling history back: these pages are linked from the sign-up form, and someone who opens them in a new tab has no history to go back to.