# Feature Specification: Dark/Light Mode **Feature**: `023-dark-mode` **Created**: 2026-03-06 **Status**: Draft **Source**: Migrated from spec/userstories.md ## User Scenarios & Testing ### User Story 1 - System preference respected on first visit (Priority: P1) A user opens the app for the first time. The app automatically adopts their operating system or browser dark/light preference without any manual configuration required. No preference data is transmitted to the server. **Why this priority**: This is the baseline behavior — it works without any user interaction and provides the correct experience immediately. **Independent Test**: Can be tested by opening the app in a browser with `prefers-color-scheme: dark` set at the OS level and verifying the dark theme is applied, then repeating with light preference. **Acceptance Scenarios**: 1. **Given** a user opens the app for the first time with no manual preference stored, **When** the OS/browser preference is `prefers-color-scheme: dark`, **Then** the app renders in dark mode. 2. **Given** a user opens the app for the first time with no manual preference stored, **When** the OS/browser preference is `prefers-color-scheme: light`, **Then** the app renders in light mode. 3. **Given** the app is rendering in either mode, **When** the page is loaded, **Then** no server request is made and no preference data is transmitted. --- ### User Story 2 - Manual toggle overrides system preference (Priority: P1) A user can switch between dark and light mode using a visible toggle available on any page. Their choice is persisted in localStorage and takes precedence over the OS preference on all subsequent visits. **Why this priority**: The explicit user preference must be honoured and must persist — without this, the toggle would reset on every visit, making it unusable. **Independent Test**: Can be tested by toggling the mode, closing and reopening the browser, and verifying the manually selected mode is still active even if it differs from the OS preference. **Acceptance Scenarios**: 1. **Given** the app is in light mode (system preference), **When** the user activates the dark mode toggle, **Then** the UI immediately switches to dark mode and the preference is stored in localStorage. 2. **Given** the user has a dark mode preference stored in localStorage, **When** the user revisits the app, **Then** dark mode is applied regardless of the current OS/browser preference. 3. **Given** the user has a light mode preference stored in localStorage and the OS preference is dark, **When** the user revisits the app, **Then** light mode is applied (localStorage takes precedence). 4. **Given** the app is running, **When** the user toggles the mode, **Then** no server request is made and no preference data is transmitted. --- ### User Story 3 - Toggle accessible from any page (Priority: P2) The dark/light mode toggle is reachable from every page of the app — event pages, local event overview, creation form, etc. — so the user never has to navigate away to change their preference. **Why this priority**: This is a usability enhancement. The feature works without it (if the toggle were only on one page), but accessibility from any page is important for a good experience. **Independent Test**: Can be tested by navigating to the event page, the local event overview, and the creation form and verifying the toggle is visible and functional on each. **Acceptance Scenarios**: 1. **Given** the user is on the local event overview page (`/`), **When** they look for the mode toggle, **Then** it is visible and functional. 2. **Given** the user is on an event page, **When** they look for the mode toggle, **Then** it is visible and functional. 3. **Given** the user is on the event creation form, **When** they look for the mode toggle, **Then** it is visible and functional. --- ### User Story 4 - Dark/light mode does not affect event-level color themes (Priority: P2) Dark/light mode affects only the app's global UI chrome (navigation, local event overview, forms, etc.). Individual event pages use their own color theme (US-15), which is independent of the app-level dark/light setting. **Why this priority**: Necessary to define the boundary between app-level theming and event-level theming clearly, but secondary to the core toggle behaviour. **Independent Test**: Can be tested by creating an event with a custom color theme, then toggling dark/light mode and verifying the event page theme is unaffected while the surrounding chrome does change. **Acceptance Scenarios**: 1. **Given** an event page is rendered with a custom color theme (US-15), **When** the user switches the app to dark mode, **Then** the event page color theme remains unchanged (only surrounding chrome changes). 2. **Given** the app is in dark mode, **When** the user navigates to the local event overview, **Then** the overview uses the dark color scheme. --- ### User Story 5 - Both modes meet WCAG AA contrast (Priority: P1) Both dark and light modes must meet accessibility contrast requirements at the WCAG AA minimum level, ensuring the app is usable for users with visual impairments in both modes. **Why this priority**: Accessibility is a baseline requirement per the project statutes, not an afterthought. **Independent Test**: Can be tested using automated contrast checking tools against both mode variants. **Acceptance Scenarios**: 1. **Given** the app is in dark mode, **When** text and interactive elements are checked for contrast ratio, **Then** all text/background pairings meet WCAG AA minimum (4.5:1 for normal text, 3:1 for large text). 2. **Given** the app is in light mode, **When** text and interactive elements are checked for contrast ratio, **Then** all text/background pairings meet WCAG AA minimum. --- ### Edge Cases - What happens when the OS `prefers-color-scheme` value changes while the app is open (e.g. user switches OS theme at runtime)? If no manual preference is stored in localStorage, should the app react? [NEEDS EXPANSION during implementation] - What happens when localStorage is unavailable (private browsing with strict settings)? The system preference fallback must still work without crashing. - How does app-level dark/light mode interact with the event-level color themes (US-15) when an event page is embedded in the app chrome? Themes should remain readable in both modes. ## Requirements ### Functional Requirements - **FR-001**: The app MUST detect and apply `prefers-color-scheme` as the default on first visit when no manual preference is stored in localStorage. - **FR-002**: The app MUST provide a visible toggle UI element to switch between dark and light mode, accessible from any page. - **FR-003**: The app MUST persist the user's manual mode preference in localStorage and apply it on subsequent visits, overriding the system preference. - **FR-004**: Dark/light mode MUST affect all global app chrome: navigation, local event overview, event creation/editing forms, and all non-event-page UI elements. - **FR-005**: Dark/light mode MUST NOT affect individual event page color themes (US-15); event pages are styled independently. - **FR-006**: The mode switch MUST be entirely client-side; no server request is made and no preference data is transmitted. - **FR-007**: Both dark and light modes MUST meet WCAG AA contrast requirements for all text and interactive elements. - **FR-008**: The toggle MUST be accessible (keyboard-navigable, labelled for screen readers). ### Key Entities - **DarkLightPreference**: A client-side-only value (`"dark"` | `"light"` | absent) stored in localStorage. No server-side equivalent. Determines which CSS theme is applied to the global app chrome. ## Success Criteria ### Measurable Outcomes - **SC-001**: On first visit with `prefers-color-scheme: dark`, the dark theme is applied without any user interaction. - **SC-002**: A user's manual toggle selection persists across browser sessions and overrides the OS preference. - **SC-003**: The mode toggle is visible and functional on all primary app pages (local event overview, event page, creation form). - **SC-004**: Automated contrast checks pass WCAG AA thresholds for all text elements in both dark and light modes. - **SC-005**: No network request is made when toggling the mode or when the stored preference is applied on page load.