---
title: "Accessibility utilities"
description: "sr-only!, the skip link and other accessibility helpers."
requested_language: fr
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/fr/next/styling/accessibility
version: unreleased (main)
docs_index: https://docs.systhema.app/fr/next/llms.txt
---
> This page isn't translated yet. Showing English.


Systhema's base layer ships the pieces every page needs for keyboard and screen-reader users: a `.skip-link` that appears on focus, a reduced-motion fallback for every animation, and the layout rules that keep landmarks and anchor targets usable. Its components use Tailwind's `sr-only!` for text only screen readers announce.

## Quick reference

The `applicationLayout` block (on by default, `blocks.applicationLayout`) emits these base rules:

<!-- generated:utilities application-layout -->

| Selector                                                      | Styles                                                                                                                                                                                                                                                                                                                                                          |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `html`                                                        | `overflow-x: clip;`<br>`overflow-y: auto;`                                                                                                                                                                                                                                                                                                                      |
| `body`                                                        | `position: relative;`<br>`min-height: 100dvh;`<br>`display: flex;`<br>`flex-direction: column;`<br>`justify-content: space-between;`                                                                                                                                                                                                                            |
| `header, footer, main, section`                               | `display: block;`<br>`width: 100%;`<br>`overflow-x: clip;`<br>`overflow-y: visible;`                                                                                                                                                                                                                                                                            |
| `main`                                                        | `flex-grow: 1;`                                                                                                                                                                                                                                                                                                                                                 |
| `.container`                                                  | `width: calc(100% - var(--container-margin) * 2);`<br>`max-width: var(--container-width);`<br>`margin-inline: auto;`                                                                                                                                                                                                                                            |
| `.skip-link`                                                  | `position: absolute;`<br>`top: 0;`<br>`left: 0;`<br>`z-index: 9999;`<br>`padding: 0;`<br>`width: 1px;`<br>`height: 1px;`<br>`margin: 0;`<br>`overflow: hidden;`<br>`clip: rect(0, 0, 0, 0);`<br>`white-space: nowrap;`<br>`border: 0;`<br>`background-color: var(--color-foundations-surface-bg, #ffffff);`<br>`color: var(--color-foundations-text, #000000);` |
| `figure`                                                      | `width: 100%;`<br>`margin-inline: auto;`                                                                                                                                                                                                                                                                                                                        |
| `[id]:not(section):not(.figure-w-full):not(.figure-w-screen)` | `scroll-margin-top: var(--section-padding-y, 48px);`                                                                                                                                                                                                                                                                                                            |

<!-- /generated -->

## Skip link

A skip link lets keyboard users jump past the header straight to the content (WCAG 2.4.1). `.skip-link` is visually hidden until it receives focus, then pins itself to the top-left corner above everything else, in the page's foundation colors. Make it the first focusable element on the page and point it at your `<main>`:

```html
<body>
  <a class="skip-link" href="#main-content">Skip to content</a>
  <header class="header">…</header>
  <main id="main-content">…</main>
</body>
```

Systhema's `RootLayout` and page templates already render it this way. The preview focuses the link from a button so you can see the focused state; on a real page, press <kbd>Tab</kbd> once after load.

```tsx preview iframe height=240 title="Skip link"
import { useRef } from 'react'
import { Button } from '@systhemaui/next'

export default function Demo() {
  const link = useRef<HTMLAnchorElement>(null)

  return (
    <div className="flex min-h-48 w-full flex-col items-center justify-center gap-4">
      <a ref={link} className="skip-link" href="#skip-link-demo-main">
        Skip to content
      </a>
      <Button.button variant="secondary" onClick={() => link.current?.focus()}>
        Focus the skip link
      </Button.button>
      <main id="skip-link-demo-main" className="text-small color-body">
        Page content
      </main>
    </div>
  )
}
```

The rule lives in the always-loaded base layer, so it works without a `focus:not-sr-only` utility that a content scan could miss.

## Visually hidden text

Use `sr-only!` (Tailwind's `sr-only` with `!important`) for text that only screen readers should read: the label of an icon-only control, or an "(opens in new tab)" cue. The `!` keeps component styles from making the text visible again.

```tsx preview title="Icon-only button"
export default function Demo() {
  return (
    <button
      type="button"
      className="flex size-11 items-center justify-center rounded-full border border-(--color-foundations-surface-border) color-heading"
    >
      <svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" className="size-4">
        <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.5" />
      </svg>
      <span className="sr-only!">Close</span>
    </button>
  )
}
```

Give each control one label. An `aria-label` overrides the text inside the control, so don't add both; and mark decorative icons `aria-hidden="true"` (plus `focusable="false"` on a raw `<svg>`).

## Reduced motion

Under `@media (prefers-reduced-motion: reduce)`:

- The fade utilities (`animate-fadein*`, `animate-fadeout`) and the scroll-reveal classes (`aos`, `animates-on-scroll`) snap to their final visible state: no animation, no transform, full opacity. This holds whether or not the scroll-reveal JavaScript runs, so content never stays invisible.
- `.parallax` drops its transform and the parallax engines write none.

The escape hatch is emitted at the same specificity as the rule that hides an unrevealed element (`.aos:not(.animated):not(.animate-disable)`) and after it, because a media query adds no specificity and would otherwise lose the `!important` cascade. Your own transitions are not covered: add `motion-reduce:transition-none` where something moves. Details are on [Animation](https://docs.systhema.app/fr/next/styling/animation.md#reduced-motion), [Parallax](https://docs.systhema.app/fr/next/styling/parallax.md#reduced-motion) and [Easing and transitions](https://docs.systhema.app/fr/next/styling/easing.md#reduced-motion).

## Base layout rules

The other rules in the table make the landmarks behave:

- `body` is a full-height flex column, and `main` grows, so the footer sits at the bottom of short pages.
- `header`, `footer`, `main` and `section` clip horizontal overflow (`overflow-x: clip`), so a slide-in animation or a full-bleed figure never adds a horizontal scrollbar, while sticky positioning keeps working.
- Every element with an `id` (except sections and full-width figures) gets `scroll-margin-top: var(--section-padding-y)`, so an in-page link lands with some room above the target. With a sticky `.header` on the page, `html` and `body` also reserve the header's height as `scroll-padding-top` (see [CSS variables](https://docs.systhema.app/fr/next/styling/css-variables.md#root-variables)).

## Responsive and state variants

`sr-only!` takes Tailwind's variants, so `md:not-sr-only!` shows a label from `md` up. `.skip-link` has no variants; it reacts to `:focus` and `:focus-visible`.

## Customizing

The skip link takes its colors from `--color-foundations-surface-bg` and `--color-foundations-text`, so it follows your [color tokens](https://docs.systhema.app/fr/next/reference/tokens/colors.md). Its corner radius reads `--button-border-radius` (4px when unset) and its outline width `--focus-ring-width` (2px when unset); the default tokens define neither, so set them in your own CSS to change them:

```css title="src/app/(site)/globals.css"
:root {
  --button-border-radius: 999px;
  --focus-ring-width: 3px;
}
```

These rules are always emitted: `blocks.applicationLayout` exists in the config type, but core doesn't read it, so `false` does not remove them.

## Related

- [Accessibility](https://docs.systhema.app/fr/next/concepts/accessibility.md)
- [Animation](https://docs.systhema.app/fr/next/styling/animation.md)
- [Listeners](https://docs.systhema.app/fr/next/components/listeners.md)
