Accessibility and RTL support
What Foundation's plugins already do for keyboard and screen reader users, what you must add yourself, and how to build a right-to-left layout that holds.
What the plugins handle
| Component | Provided | Your responsibility |
|---|---|---|
| Reveal | Focus trap, Escape, returns focus to the trigger | A labelled heading, and not opening it on page load |
| Accordion | aria-expanded and aria-controls on the title | Meaningful title text, sensible default open item |
| Tabs | aria-selected, arrow-key navigation | Panel tabindex="0" if it has no focusable content |
| Off-canvas | Focus containment while open, close on Escape | A close button with an accessible name |
| Dropdown | aria-expanded, aria-haspopup | Distinguishing a menu of actions from a list of links |
| Tooltip | aria-describedby on the trigger | Never the only source of essential information |
| Abide (validation) | Error keys, focus on the first invalid field | Text that explains what to do, not just that it is wrong |
<div class="reveal" id="confirm" data-reveal
aria-labelledby="confirmTitle" aria-describedby="confirmBody">
<h2 class="h5" id="confirmTitle">Remove the integration?</h2>
<p id="confirmBody">This deletes the stored token. You will need to reconnect.</p>
<button class="button secondary" data-close type="button">Cancel</button>
<button class="button alert" type="button">Remove integration</button>
</div>
<button class="button" type="button" aria-haspopup="dialog" data-open="confirm">
Remove integration
</button>⚠️
A reveal that opens automatically on page load traps the keyboard before the user has read anything. Open dialogs only in response to a user action, and always give them a heading that describes the decision.
What you still owe
<a class="show-on-focus button small" href="#main">Skip to content</a>
<header role="banner">
<nav aria-label="Primary">…</nav>
</header>
<main id="main" tabindex="-1">…</main>
<!-- Icon-only control: needs a name -->
<button class="close-button" type="button" aria-label="Close panel" data-close>
<span aria-hidden="true">×</span>
</button>
<!-- Live region for status that appears asynchronously -->
<div class="callout success" role="status" aria-live="polite" id="status"></div>/* Foundation's show-for-sr hides content visually but keeps it announced. */
.show-on-focus {
position: absolute;
top: -100px;
left: 0;
z-index: 200;
&:focus { top: 0; }
}
/* Never remove focus outlines globally. Style them instead. */
:focus-visible {
outline: 2px solid get-color(primary);
outline-offset: 2px;
}
/* Foundation's own reduced-motion guard covers Motion UI; be explicit for your JS. */
@media (prefers-reduced-motion: reduce) {
.reveal { animation: none !important; }
}show-for-sris the Foundation utility for text only a screen reader should hear. It is the right way to name the current page in pagination.- Foundation adds landmarks for some components but not for your page structure.
<header>,<nav>and<main>are still your job. - A button with only a glyph has no accessible name.
aria-labelon the button plusaria-hidden="true"on the glyph is the reliable pattern. - Focus order follows the DOM. If a layout visually reorders content with
grid-marginandorderclasses, check that the tab order still makes sense.
Right-to-left layouts
/* settings.scss — one variable switches the whole framework.
Foundation mirrors the grid, menus, buttons and callouts in the compiled CSS. */
$global-text-direction: rtl;
/* Foundation reads this and converts its own left/right declarations to
logical ones. Note the output roughly doubles for directional rules. */
$lang-direction: rtl;<html lang="ar" dir="rtl">
<!-- Foundation's compiled RTL stylesheet applies to this whole document -->
</html>
<!-- A single LTR island inside an RTL page -->
<div dir="ltr" class="invoice-number" lang="en">INV-1042</div>| LTR-era | Direction-neutral | Reason |
|---|---|---|
float: left | float: inline-start | Follows the writing direction |
margin-left | margin-inline-start | Same |
padding-right | padding-inline-end | Same |
text-align: left | text-align: start | Same |
border-left | border-inline-start | Same |
| A background arrow icon | A mirrored icon or transform: scaleX(-1) | Icons with direction must mirror |
The two things that break most often in an RTL build are hard-coded pixel offsets in your own CSS and directional icons — chevrons, arrows, progress indicators. Set $global-text-direction: rtl, rebuild, then read through your component partials looking for any remaining left or right in a property name.
FAQ
Do I need two builds for LTR and RTL?
With the Sass global you compile one or the other. To ship both, run the compiler twice with the variable flipped and a different output path, then choose the stylesheet based on the document language. A single build cannot serve both.
Is Foundation's focus management enough for a modal?
It traps focus and restores it to the trigger, which covers the two common failures. It does not give the dialog an accessible name — that comes from your
aria-labelledby pointing at a real heading inside the dialog.Related
Motion UI and animation Building a complete page with Foundation
Last refreshed 2026-09-18.