---
title: Accessibility
description: Opt-in skip link and print styles for themed pages.
---

# Accessibility

When `ssg.a11y` is enabled, themed pages get:

- a **Skip to content** link as the first focusable control in `<body>`
- `id="ox-main"` on `<main>` so the skip link has a target
- print CSS that hides header, sidebar, search, and reader-chrome extras

The feature is off unless you turn it on. Disabled pages emit no skip link
and no extra print CSS.

```ts
import { oxContent } from "@ox-content/vite-plugin";

export default {
  plugins: [
    oxContent({
      ssg: {
        a11y: true,
      },
    }),
  ],
};
```

`false` or omitted keeps the extras off. `true` enables the defaults. An
object enables the feature and can override the skip-link label:

```ts
oxContent({
  ssg: {
    a11y: { skipLinkLabel: "Skip to main" },
  },
});
```

| Field           | Default             | Effect                                      |
| --------------- | ------------------- | ------------------------------------------- |
| `skipLinkLabel` | `"Skip to content"` | Text of the skip link. Escaped in the HTML. |

The skip link uses `class="ox-skip-link"` and `href="#ox-main"`. CSS keeps it
visually hidden until it receives keyboard focus, so the first Tab stop is
the skip link.

Print output keeps the article and hides site chrome: the header, sidebar,
search modal, and reader-chrome controls such as copy and back-to-top.

Bare mode never emits the skip link or the extra print CSS. Hostile labels
cannot break out of the link text.

## Embed states

Almost nothing resolves in the reader's browser. Cards, players, and link
previews are all built once and shipped as static HTML, so they have no loading
state to show — they arrive resolved. When a build-time fetch fails, the card
that ships is already the fallback or the error card; the reader never watches
it happen.

The exception is **islands**. A component tag hydrates in the browser, so it is
the one embed with states over time:

| State    | Marker               | Appearance                                                         |
| -------- | -------------------- | ------------------------------------------------------------------ |
| loading  | `.ox-island-loading` | spinner, after a short delay so a fast hydrate never flashes one   |
| resolved | `[data-ox-hydrated]` | the component itself                                               |
| error    | `.ox-island-error`   | the static content it was rendered from, marked as not interactive |

All three honour `prefers-reduced-motion: reduce`.

## Reduced motion

Built-in embed and island styles honour `prefers-reduced-motion: reduce`.
The loading spinners on Open Graph cards, GitHub cards, and unhydrated
islands stop rotating, the island skeleton stops shimmering, and hover
transitions on tabs and the YouTube placeholder are dropped.

Each indicator keeps its shape, so a card that is still loading still looks
like it is loading — only the movement goes away. These styles ship with the
embeds themselves and need no configuration.
