# LegalPage

> A legal document page with a switcher between documents, the updated date, a draft notice, numbered sections with copyable anchors and a section rail.

Source: https://docs.nasaqui.com/components/legal-page

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/legal-page.json
```

Terms of service, privacy policy, cookie policy: long documents people search for one clause in. The page has a switcher between the documents, the date it was last updated, an optional draft notice, numbered sections whose headings are `#anchors` with a copy-link button, and an "On this page" rail. The text sits in one readable column of about 68 characters.

## When to use

- Public legal pages of a product.
- Any long policy or agreement where clause links get shared.

## When not to use

- Product documentation: use [docs-shell](https://docs.nasaqui.com/components/docs-shell).
- A blog article: use [blog-post](https://docs.nasaqui.com/components/blog-post).
- The cookie banner: use [cookie-consent](https://docs.nasaqui.com/components/cookie-consent).

## Import

```tsx
import { LegalPage } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { LegalPage } from "@fadymondy/nasaq/web";

export function Terms() {
  return (
    <LegalPage
      document={{
        id: "terms",
        title: "Terms of service",
        updated: "2026-09-01",
        sections: [
          { title: "Using the service", body: "You agree to use it lawfully." },
          { title: "Payments", body: "Fees are billed monthly." },
        ],
      }}
    />
  );
}
```

## Anatomy

```
LegalPage             data-slot="legal-page"
├─ switcher           <nav> of documents (two or more)
├─ header             title, summary, updated / effective / version, draft Alert
├─ sections           data-slot="legal-section"  number, h2 with id, copy-link button, Markdown body
├─ footer             your contact details (optional)
└─ rail               TableOfContents (from the lg breakpoint)
```

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `document` | `LegalDocument` | required | `{ id, title, summary?, updated, effective?, version?, draft?, sections }`. |
| `documents` | `{ id, title }[]` | none | The site's documents. The switcher shows for two or more. |
| `onSelectDocument` | `(id) => void` | none | A document was picked. Navigate, then pass the new `document`. |
| `onCopyLink` | `(url) => void` | none | After a section link was copied. |
| `footer` | `ReactNode` | none | Contact details under the last section. |
| `scrollOffset` | `number` | `96` | Space headings keep from the top when scrolled to. |
| `labels` | `LegalPageLabels` | English or Arabic | String overrides. |

`LegalSection` is `{ id?, title, body }` where `body` is Markdown. Give sections an explicit `id` once a document is published so shared links keep working when titles change. Without one the id is a slug of the title (Arabic letters are kept); repeats get `-2`, `-3`.

### Helpers

`resolveLegalSections`, `legalHashTarget`, `legalSectionUrl`.

## Examples

```tsx
<LegalPage document={terms} documents={[{ id: "terms", title: "Terms" }, { id: "privacy", title: "Privacy" }]} onSelectDocument={goTo} />
```

```tsx
<LegalPage document={{ ...privacy, draft: true, version: "2.0-rc1" }} footer={<a href="mailto:legal@example.com">legal@example.com</a>} />
```

A link such as `/terms#payments` opens scrolled to that section.

## Accessibility

| Key | Action |
| --- | --- |
| Tab | Switcher, copy-link buttons, links in the text, the rail. |
| Enter, Space | Pick a document, copy a section link. |

- Every section is a `section` labelled by its heading. The copy-link button names the section, and the result is announced in a polite status.
- The draft notice is an alert with text and an icon, not colour alone.
- The current document in the switcher has `aria-current="page"`.

## RTL & i18n

- The page mirrors: rail on the end side, numbers before titles on the reading start.
- Titles and text use `dir="auto"`; dates use the locale with Latin digits by default.
- Built-in strings are English and Arabic. Provide the document text in the reader's language.

## Styling & tokens

Uses the typography roles and `--nq-*` border, selected and focus tokens. Target `data-slot="legal-page"` and `legal-section`.

## Do / Don't

- Do show the date the document changed and, when it matters, the date it takes effect.
- Do keep section ids stable.
- Don't put the whole document in one section. Anchors are only useful when sections are small.
- Don't present a draft as final. Set `draft`.

## Related

- [docs-shell](https://docs.nasaqui.com/components/docs-shell), [blog-post](https://docs.nasaqui.com/components/blog-post), [markdown](https://docs.nasaqui.com/components/markdown), [cookie-consent](https://docs.nasaqui.com/components/cookie-consent)

## Lab

https://docs.nasaqui.com/?path=/docs/components-website-legal-page--docs

## Code

### React

```tsx
import { LegalPage } from "@fadymondy/nasaq/web";

export function Terms() {
  return (
    <LegalPage
      document={{
        id: "terms",
        title: "Terms of service",
        updated: "2026-09-01",
        sections: [
          { title: "Using the service", body: "You agree to use it lawfully." },
          { title: "Payments", body: "Fees are billed monthly." },
        ],
      }}
    />
  );
}
```

### shadcn

```tsx
import { LegalPage } from "@/components/ui/legal-page";

export function Terms() {
  return (
    <LegalPage
      document={{
        id: "terms",
        title: "Terms of service",
        updated: "2026-09-01",
        sections: [
          { title: "Using the service", body: "You agree to use it lawfully." },
          { title: "Payments", body: "Fees are billed monthly." },
        ],
      }}
    />
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqLegalPage } from "@fadymondy/nasaq/vue";

const terms = {
  id: "terms",
  title: "Terms of service",
  updated: "2026-09-01",
  sections: [
    { title: "Using the service", body: "You agree to use it lawfully." },
    { title: "Payments", body: "Fees are billed monthly." },
  ],
};
</script>

<template>
  <NqLegalPage :document="terms" />
</template>
```

### Blade

```blade
@php
    $terms = [
        'id' => 'terms',
        'title' => 'Terms of service',
        'updated' => '2026-09-01',
        'sections' => [
            ['title' => 'Using the service', 'body' => 'You agree to use it lawfully.'],
            ['title' => 'Payments', 'body' => 'Fees are billed monthly.'],
        ],
    ];
@endphp
<x-nq::legal-page :document="$terms" />
```

### HTML + Alpine

```html
<div data-slot="legal-page" x-data="nqLegalPage(JSON.parse('{\u0022ids\u0022:[\u0022using-the-service\u0022,\u0022payments\u0022],\u0022offset\u0022:96}'))" class="mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:flex-row lg:gap-12">
    <div class="flex min-w-0 flex-1 flex-col gap-8">
        <header class="flex max-w-[68ch] flex-col gap-3">
            <h1 data-slot="text" dir="auto" class="text-h1 text-foreground text-start">Terms of service</h1>
                        <p class="flex flex-wrap items-center gap-x-4 gap-y-1 text-caption text-muted-foreground">
                <span>Last updated <time data-slot="date-time" datetime="2026-09-01T00:00:00+00:00" dir="auto"     class="tabular-nums [unicode-bidi:isolate]">September 1, 2026</time>
</span>
                                            </p>
                    </header>
        <div class="flex max-w-[68ch] flex-col gap-10">
                            <section aria-labelledby="using-the-service-title" data-slot="legal-section" class="group/section flex flex-col gap-3">
                    <h2 id="using-the-service" style="scroll-margin-top: 96px" class="flex items-baseline gap-3 text-start">
                        <span aria-hidden="true" class="text-h3 font-semibold tabular-nums text-muted-foreground">1.</span>
                        <span data-slot="text" dir="auto" id="using-the-service-title" class="text-h2 text-foreground min-w-0">Using the service</span>
                        <button type="button" aria-label="Copy link to this section: Using the service" x-on:click="copyLink(&#039;using-the-service&#039;)"
                            class="ms-1 inline-flex size-6 shrink-0 items-center justify-center self-center rounded-control text-muted-foreground opacity-0 outline-none transition-opacity duration-150 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-nq-focus group-hover/section:opacity-100 pointer-coarse:opacity-100">
                            <svg aria-hidden="true" x-show="copied !== &#039;using-the-service&#039;" class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M9 17H7A5 5 0 0 1 7 7h2"/>
  <path d="M15 7h2a5 5 0 1 1 0 10h-2"/>
  <line x1="8" x2="16" y1="12" y2="12"/>
</svg>                            <svg aria-hidden="true" style="display: none" x-show="copied === &#039;using-the-service&#039;" class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>                        </button>
                    </h2>
                    <div data-slot="markdown" class="flex min-w-0 flex-col text-nq-fg-body gap-4 text-body leading-relaxed"><p data-slot="text" dir="auto" class="text-body text-nq-fg-body text-start">You agree to use it lawfully.</p>
</div>
                </section>
                            <section aria-labelledby="payments-title" data-slot="legal-section" class="group/section flex flex-col gap-3">
                    <h2 id="payments" style="scroll-margin-top: 96px" class="flex items-baseline gap-3 text-start">
                        <span aria-hidden="true" class="text-h3 font-semibold tabular-nums text-muted-foreground">2.</span>
                        <span data-slot="text" dir="auto" id="payments-title" class="text-h2 text-foreground min-w-0">Payments</span>
                        <button type="button" aria-label="Copy link to this section: Payments" x-on:click="copyLink(&#039;payments&#039;)"
                            class="ms-1 inline-flex size-6 shrink-0 items-center justify-center self-center rounded-control text-muted-foreground opacity-0 outline-none transition-opacity duration-150 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-nq-focus group-hover/section:opacity-100 pointer-coarse:opacity-100">
                            <svg aria-hidden="true" x-show="copied !== &#039;payments&#039;" class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M9 17H7A5 5 0 0 1 7 7h2"/>
  <path d="M15 7h2a5 5 0 1 1 0 10h-2"/>
  <line x1="8" x2="16" y1="12" y2="12"/>
</svg>                            <svg aria-hidden="true" style="display: none" x-show="copied === &#039;payments&#039;" class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>                        </button>
                    </h2>
                    <div data-slot="markdown" class="flex min-w-0 flex-col text-nq-fg-body gap-4 text-body leading-relaxed"><p data-slot="text" dir="auto" class="text-body text-nq-fg-body text-start">Fees are billed monthly.</p>
</div>
                </section>
                    </div>
                <p role="status" class="sr-only" x-text="copied ? &#039;Link copied&#039; : ''"></p>
    </div>
    <aside class="hidden w-56 shrink-0 lg:block">
        <div class="sticky top-20">
            <nav data-slot="table-of-contents" aria-label="On this page" class="flex flex-col gap-2">
        <p class="eyebrow">On this page</p>
        <ol class="flex flex-col border-s border-border">
                            <li>
                    <a href="#using-the-service" data-id="using-the-service" dir="auto" x-on:click="go($event)" x-bind:aria-current="isActive($el) ? 'location' : null"
                        x-bind:class="isActive($el) ? 'border-primary font-medium text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'"
                        class="-ms-px block border-s-2 py-1 text-body-sm outline-none transition-colors duration-150 ease-nq focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-nq-focus ps-3">Using the service</a>
                </li>
                            <li>
                    <a href="#payments" data-id="payments" dir="auto" x-on:click="go($event)" x-bind:aria-current="isActive($el) ? 'location' : null"
                        x-bind:class="isActive($el) ? 'border-primary font-medium text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'"
                        class="-ms-px block border-s-2 py-1 text-body-sm outline-none transition-colors duration-150 ease-nq focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-nq-focus ps-3">Payments</a>
                </li>
                    </ol>
    </nav>
        </div>
    </aside>
</div>
```
