Implement a useContrastMode Hook for Dynamic Contrast Adjustment
This challenge involves creating a custom React hook, useContrastMode, that allows users to toggle between a normal and a high-contrast mode for a web application. This is crucial for accessibility, providing users with visual impairments a more comfortable and readable experience.
Problem Description
You need to implement a custom React hook named useContrastMode that manages and applies a contrast mode to your application. This hook should:
- Maintain State: Keep track of whether the high-contrast mode is currently enabled or disabled.
- Provide Toggle Functionality: Offer a function to easily switch between the normal and high-contrast modes.
- Apply CSS Class: Dynamically add or remove a specific CSS class (e.g.,
contrast-mode) to a designated element (typically thebodyor a root containerdiv) based on the current contrast mode state. - Persist State (Optional but Recommended): Ideally, the hook should persist the user's preference across page reloads, possibly using
localStorage.
Key Requirements:
- The hook must be written in TypeScript.
- It should return an object containing:
isContrastModeEnabled: A boolean indicating the current state.toggleContrastMode: A function to flip the state.
- The hook should accept an optional parameter to specify the
localStoragekey for persistence. - The hook should accept an optional parameter to specify the selector for the element to which the contrast class will be applied. Defaults to
document.body.
Expected Behavior:
- When
isContrastModeEnabledistrue, the designated HTML element should have the specified contrast class added. - When
isContrastModeEnabledisfalse, the contrast class should be removed. - Calling
toggleContrastModeshould invert theisContrastModeEnabledstate and update the DOM accordingly. - If
localStorageis used, the chosen mode should be restored when the hook is initialized on subsequent visits.
Edge Cases:
- No
localStoragesupport: The hook should gracefully handle environments wherelocalStorageis not available (e.g., private browsing modes or certain browser configurations). - Initial Load: Ensure the correct contrast class is applied immediately on the first render if a preference is found in
localStorage. - Dynamic Target Element: If the target element is not the
body, ensure it's correctly identified and the class is applied to it.
Examples
Example 1: Basic Usage
Assume you have a simple app structure and some CSS:
/* styles.css */
.contrast-mode {
filter: invert(1) hue-rotate(180deg); /* A common high-contrast effect */
background-color: #f0f0f0;
color: #000;
}
body {
background-color: #fff;
color: #333;
}
// App.tsx
import React from 'react';
import useContrastMode from './useContrastMode'; // Assuming hook is in this file
function App() {
const { isContrastModeEnabled, toggleContrastMode } = useContrastMode();
return (
<div>
<h1>My Awesome App</h1>
<p>This is some content that will change appearance.</p>
<button onClick={toggleContrastMode}>
{isContrastModeEnabled ? 'Disable High Contrast' : 'Enable High Contrast'}
</button>
<p>Current Contrast Mode: {isContrastModeEnabled ? 'Enabled' : 'Disabled'}</p>
</div>
);
}
export default App;
Input (User Action): User clicks the "Enable High Contrast" button.
Expected Output (DOM State):
The body element will have the class contrast-mode added. The button text will change to "Disable High Contrast". The paragraph showing the current mode will update.
Explanation:
The useContrastMode hook initializes isContrastModeEnabled to false. Clicking the button calls toggleContrastMode, which sets isContrastModeEnabled to true, adds the contrast-mode class to the body, and updates the UI.
Example 2: With localStorage Persistence
Consider the same setup as Example 1, but useContrastMode is initialized with a localStorage key.
// useContrastMode.ts
// ... (hook implementation using localStorage)
// App.tsx
import React from 'react';
import useContrastMode from './useContrastMode';
function App() {
// Persist preference using localStorage key 'myAppContrastPref'
const { isContrastModeEnabled, toggleContrastMode } = useContrastMode({
localStorageKey: 'myAppContrastPref',
});
return (
<div>
<h1>Accessible App</h1>
<button onClick={toggleContrastMode}>
{isContrastModeEnabled ? 'Disable High Contrast' : 'Enable High Contrast'}
</button>
</div>
);
}
export default App;
Input:
- User enables high contrast mode.
localStorage.setItem('myAppContrastPref', 'true')is called internally. - User refreshes the page.
Expected Output:
The body element will immediately have the contrast-mode class applied upon page load, and the button will display "Disable High Contrast".
Explanation:
On the first interaction, the hook updates the localStorage. On subsequent loads, the hook reads the value from localStorage and initializes isContrastModeEnabled accordingly, applying the class before any user interaction.
Example 3: Targeting a Specific Element
Imagine a dashboard where only a specific content area should be affected by contrast mode.
// App.tsx
import React from 'react';
import useContrastMode from './useContrastMode';
function App() {
const { isContrastModeEnabled, toggleContrastMode } = useContrastMode({
targetSelector: '#main-content', // Target a specific div
contrastClassName: 'high-contrast-content', // Use a different class name
localStorageKey: 'dashboardContrast'
});
return (
<div>
<header>Header Content</header>
<button onClick={toggleContrastMode}>
{isContrastModeEnabled ? 'Disable Contrast' : 'Enable Contrast'}
</button>
<main id="main-content" className="content-area"> {/* Element to be targeted */}
<h2>Main Content Area</h2>
<p>This text inside the main content will be affected.</p>
</main>
<footer>Footer Content</footer>
</div>
);
}
export default App;
/* styles.css */
.high-contrast-content {
filter: sepia(100%) saturate(150%); /* Different contrast effect */
border: 2px solid darkred;
}
.content-area {
padding: 20px;
border: 1px solid #ccc;
}
Input (User Action): User clicks "Enable Contrast".
Expected Output (DOM State):
The <main id="main-content"> element will have the class high-contrast-content added. The button text will change.
Explanation:
The hook identifies the element with id="main-content" using targetSelector and applies the high-contrast-content class to it, instead of the body.
Constraints
- The hook must be written in TypeScript.
- The hook should not rely on any specific CSS framework.
- The core functionality (state management, toggle) should be independent of
localStorage. localStorageshould only be used for persistence and should be handled in a way that doesn't break the app iflocalStorageis unavailable.- The
targetSelectorshould resolve to a single DOM element. If multiple elements match, the hook may choose to apply the class to the first one or throw an error (clarify chosen behavior in implementation).
Notes
- Consider how you will handle the initial state. Should it default to
falseor try to read fromlocalStorageimmediately? - Think about the order of operations: when the class is added/removed (e.g.,
useEffectis appropriate here). - For the
localStoragepersistence, ensure you are converting the string value fromlocalStorageback to a boolean correctly. - The
contrastClassNameshould be configurable to allow for flexibility in styling. - Consider adding a cleanup function in
useEffectto remove the class when the component unmounts, although for a global state like contrast mode applied tobody, this might not be strictly necessary if the hook is intended to be used at the root of the application.