Accessibility utilities
sr-only!, the skip link and other accessibility helpers.
On this page
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 referenceLink to this section
The applicationLayout block (on by default, blocks.applicationLayout) emits these base rules:
| Selector | Styles |
|---|---|
html | overflow-x: clip;overflow-y: auto; |
body | position: relative;min-height: 100dvh;display: flex;flex-direction: column;justify-content: space-between; |
header, footer, main, section | display: block;width: 100%;overflow-x: clip;overflow-y: visible; |
main | flex-grow: 1; |
.container | width: calc(100% - var(--container-margin) * 2);max-width: var(--container-width);margin-inline: auto; |
.skip-link | position: absolute;top: 0;left: 0;z-index: 9999;padding: 0;width: 1px;height: 1px;margin: 0;overflow: hidden;clip: rect(0, 0, 0, 0);white-space: nowrap;border: 0;background-color: var(--color-foundations-surface-bg, #ffffff);color: var(--color-foundations-text, #000000); |
figure | width: 100%;margin-inline: auto; |
[id]:not(section):not(.figure-w-full):not(.figure-w-screen) | scroll-margin-top: var(--section-padding-y, 48px); |
Skip linkLink to this section
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>:
<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 Tab once after load.
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 textLink to this section
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.
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 motionLink to this section
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. .parallaxdrops 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, Parallax and Easing and transitions.
Base layout rulesLink to this section
The other rules in the table make the landmarks behave:
bodyis a full-height flex column, andmaingrows, so the footer sits at the bottom of short pages.header,footer,mainandsectionclip 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) getsscroll-margin-top: var(--section-padding-y), so an in-page link lands with some room above the target. With a sticky.headeron the page,htmlandbodyalso reserve the header's height asscroll-padding-top(see CSS variables).
Responsive and state variantsLink to this section
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.
CustomizingLink to this section
The skip link takes its colors from --color-foundations-surface-bg and --color-foundations-text, so it follows your color tokens. 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:
: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.