Oolvay includes a built-in accessibility system that allows users to personalize how the application is rendered. Accessibility preferences improve readability, reduce motion-related discomfort, and help users interact with interfaces in ways that match their individual needs.
The framework ships with font-size scaling and motion-reduction support out of the box. Both preferences are stored in the database, mirrored to cookies, and applied during server rendering before first paint. This eliminates flashes of incorrect styling, layout shifts, and hydration mismatches that commonly occur when accessibility settings are applied client-side.
While accessibility is ultimately the responsibility of the application developer, these features support common user expectations and align with portions of the Web Content Accessibility Guidelines (WCAG).
In this guide you’ll learn how accessibility preferences are stored, how the rendering pipeline works, how font scaling and motion reduction are implemented, and how to add your own accessibility settings.
| Route | File | Description |
|---|---|---|
/preferences | app/(protected)/preferences/page.tsx | Preferences overview page |
/preferences/accessibility | app/(protected)/preferences/accessibility/page.tsx | Accessibility settings page |
The accessibility page is available only to authenticated users and provides controls for managing personal rendering preferences.
Oolvay ships with two accessibility preferences by default.
| Preference | Type | Default |
|---|---|---|
| Font size | Enum | 16 |
| Reduce motion | Boolean | false |
Both preferences are persisted in the database and mirrored to cookies whenever they are updated.
The built-in accessibility features support several WCAG recommendations.
| Feature | Relevant WCAG guidance |
|---|---|
| Font size scaling | WCAG 1.4.4: Resize Text |
| Motion reduction | WCAG 2.3.3: Animation from Interactions |
| Server-side preference application |
0 / 2,000 characters
| Helps prevent unexpected visual changes during page load |
These features are only one part of an accessible application. Developers remain responsible for semantic HTML, keyboard navigation, focus management, color contrast, alternative text, form labeling, and any additional accessibility requirements specific to their application.
Oolvay’s accessibility preferences are only one part of its accessibility story.
The framework is built on top of Radix UI primitives and accessible HTML semantics. Dialogs, dropdown menus, popovers, tabs, accordions, and other interactive components inherit keyboard navigation, focus management, and ARIA attributes from their underlying implementations.
Examples include:
Accessibility ultimately depends on how components are used. Oolvay provides accessible foundations, but developers remain responsible for meaningful labels, alternative text, color contrast, content structure, and application-specific accessibility requirements.
A five-step slider allows users to increase or decrease the root font size of the application.
The selected value is applied directly to the root <html> element.
html {
font-size: 18px;
}Because the application uses rem units throughout the interface, typography, spacing, component sizing, and layout scale proportionally.
Available values are defined in lib/auth/font-sizes.ts.
| Label | Value |
|---|---|
| XS | 14px |
| S | 15px |
| M | 16px |
| L | 18px |
| XL | 20px |
The available steps live in lib/auth/font-sizes.ts.
To add or remove a size, update FONT_SIZES and FONT_SIZE_VALUES, then update the STEPS array in app/(protected)/preferences/accessibility/components/font-size-selector.tsx so the UI matches the available values.
Reduce motion allows users to disable non-essential animations and transitions throughout the application.
When enabled, a global CSS override is injected during server rendering.
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}This approach disables animations without requiring individual components to implement motion checks manually.
The landing page hero animation performs an additional optimization.
Instead of starting the normal requestAnimationFrame loop, the component reads the user’s preference directly from the session and skips animation entirely. Particles are rendered once in their final state using a single draw() call.
This prevents unnecessary CPU and GPU activity while respecting the user’s preference.
The accessibility system consists of four layers.
When a user changes a preference, the database and cookie are updated simultaneously. Future requests can then read the cookie immediately without requiring an additional database query.
Both preferences are stored on the user table in db/schemas/auth-schema.ts.
preferredFontSize: fontSizeEnum("preferred_font_size").default("16"),
reduceMotion: t.boolean("reduce_motion").default(false),When a user saves a preference, the server action writes to the database and updates a cookie with the same value.
The cookie is intentionally readable by both the server and the browser (httpOnly: false) so it can be accessed during every subsequent server render without requiring an additional database round-trip.
cookieStore.set("preferred-font-size", size, {
httpOnly: false,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: siteConfig.authAndSession.expiresInDays * 24 * 60 * 60,
})Cookie lifetime matches the configured session lifetime.
components/providers/preferences-provider.tsx reads both cookies on every request and injects an inline <style> tag before anything renders.
<style
dangerouslySetInnerHTML={{
__html: `
html { font-size: ${preferredFontSize}px; }
${
reduceMotion
? `
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}`
: ""
}
`,
}}
/>Because styles are injected during server rendering, preferences are applied before first paint and remain consistent throughout hydration.
The accessibility system is enabled globally through app/layout.tsx.
<PreferencesProvider> wraps the entire application tree, allowing accessibility preferences to be applied before any page or component renders.
<body className="min-h-full flex flex-col">
<ConsentProvider>
<PostHogProvider>
<PreferencesProvider>
<ThemeFlashGuard />
<TooltipProvider>
<RootProvider>{children}</RootProvider>
</TooltipProvider>
</PreferencesProvider>
</PostHogProvider>
</ConsentProvider>
</body>/preferences/accessibilityrouter.refresh()PreferencesProvider injects the appropriate styles| Preference | Allowed values |
|---|---|
preferredFontSize | 14, 15, 16, 18, 20 |
reduceMotion | true, false |
All values should be validated in the corresponding server action before they are written to the database.
Add the column to the user table in db/schemas/auth-schema.ts, then generate and apply the migration.
bun db:generate && bun db:migrateWrite a server action following the pattern in actions/update-preferred-font-size.ts. Validate the value, write to the database, set a cookie, and return { success: boolean }.
Read the new cookie inside components/providers/preferences-provider.tsx and apply the effect via the injected <style> tag or a data-* attribute on <html>.
Build the UI control in
app/(protected)/preferences/accessibility/components/ as a "use client"
component. Call your server action on change and trigger router.refresh() so
the page re-fetches with the updated value.
Register the new control in app/(protected)/preferences/accessibility/page.tsx alongside FontSizeSelector and ReduceMotionToggle.
| File | Responsibility |
|---|---|
db/schemas/auth-schema.ts | Stores accessibility preferences |
lib/auth/font-sizes.ts | Defines available font-size values |
components/providers/preferences-provider.tsx | Applies preferences during server rendering |
actions/update-preferred-font-size.ts | Updates font-size preference |
actions/update-reduce-motion.ts | Updates motion preference |
app/(protected)/preferences/accessibility/page.tsx | Accessibility settings page |
app/(protected)/preferences/accessibility/components/ | Accessibility UI controls |
components/landing/hero-canvas.tsx | Reads motion preference for hero animation |