Skip to content

Instantly share code, notes, and snippets.

@kiritocode1
Last active September 5, 2025 05:48
Show Gist options
  • Save kiritocode1/3a0dd88ed25b974310edefb6e62fdbd8 to your computer and use it in GitHub Desktop.
Save kiritocode1/3a0dd88ed25b974310edefb6e62fdbd8 to your computer and use it in GitHub Desktop.
How to add Accessibility In any next app

1. Setup Next themes

https://www.npmjs.com/package/next-themes

2. once next themes is setup , test it out .... checkout a theme switcher , ask claude to make one for your next version .

3. Add this use-screen-reader.js hook , to a folder to use it in main component

"use client";

import { useEffect, useState } from "react";

export const useScreenReader = () => {
	const [isEnabled, setIsEnabled] = useState(false);

	useEffect(() => {
		const handleMouseOver = (event) => {
			if (!isEnabled) return;

			const target = event.target ;
			if (!target) return;

			// Get text content or aria-label
			const text = target.getAttribute("aria-label") || target.textContent?.trim() || target.getAttribute("title") || target.getAttribute("alt");

			if (text && text.length > 0) {
				// Use Web Speech API to speak the text
				if ("speechSynthesis" in window) {
					// Cancel any ongoing speech
					speechSynthesis.cancel();

					const utterance = new SpeechSynthesisUtterance(text);
					utterance.rate = 0.8;
					utterance.pitch = 1;
					utterance.volume = 0.7;

					speechSynthesis.speak(utterance);
				}
			}
		};

		if (isEnabled) {
			document.addEventListener("mouseover", handleMouseOver);
		}

		return () => {
			document.removeEventListener("mouseover", handleMouseOver);
		};
	}, [isEnabled]);

	const toggleScreenReader = () => {
		setIsEnabled(!isEnabled);
		if (isEnabled) {
			// Stop any ongoing speech when disabling
			speechSynthesis.cancel();
		}
	};

	return { isEnabled, toggleScreenReader };
};

3. Use this Component :

"use client";

import * as React from "react";
import { Moon, Type, RotateCcw, Plus, Minus, AlignJustify, Volume2, MousePointer, Pause, ImageOff, Link, Waves, Palette, FlipHorizontal, VolumeX, PersonStanding } from "lucide-react";
import { useTheme } from "next-themes";
import { motion } from "framer-motion";
import { useScreenReader } from "@/hooks/use-screen-reader";

const drawerVariants = {
	hidden: {
		x: "100%",
		opacity: 0,
		rotateY: 5,
		transition: {
			type: "spring",
			stiffness: 300,
			damping: 30,
		},
	},
	visible: {
		x: 0,
		opacity: 1,
		rotateY: 0,
		transition: {
			type: "spring",
			stiffness: 300,
			damping: 30,
			mass: 0.8,
			staggerChildren: 0.07,
			delayChildren: 0.2,
		},
	},
};

const itemVariants = {
	hidden: {
		x: 20,
		opacity: 0,
		transition: {
			type: "spring",
			stiffness: 300,
			damping: 30,
		},
	},
	visible: {
		x: 0,
		opacity: 1,
		transition: {
			type: "spring",
			stiffness: 300,
			damping: 30,
			mass: 0.8,
		},
	},
};

const PageAccessibilityChanger = () => {
	const { theme, setTheme } = useTheme();

	const [isOpen, setIsOpen] = React.useState(false);
	const { isEnabled: screenReaderEnabled, toggleScreenReader } = useScreenReader();

	// Accessibility state management
	const [textSpacing, setTextSpacing] = React.useState(false);
	const [lineHeight, setLineHeight] = React.useState(false);
	const [dyslexiaFont, setDyslexiaFont] = React.useState(false);
	const [adhdMode, setAdhdMode] = React.useState(false);
	const [saturation, setSaturation] = React.useState(false);
	const [invertColors, setInvertColors] = React.useState(false);
	const [highlightLinks, setHighlightLinks] = React.useState(false);
	const [largeCursor, setLargeCursor] = React.useState(false);
	const [pauseAnimations, setPauseAnimations] = React.useState(false);
	const [hideImages, setHideImages] = React.useState(false);

	const ensureGlobalFilterStyle = () => {
		let style = document.getElementById("global-filters");
		if (!style) {
			style = document.createElement("style");
			style.id = "global-filters";
			style.textContent = `
				/* Apply filters to html element to avoid layout shifts */
				html { 
					filter: saturate(var(--saturation, 1)) invert(var(--invert, 0)) !important; 
				}
				[data-accessibility-panel], [data-accessibility-panel] * { 
					filter: none !important; 
				}
			`;
			document.head.appendChild(style);
		}
		return style;
	};

	const handleFontSizeChange = (size) => {
		// Apply font-size globally but exclude the accessibility panel
		let style = document.getElementById("global-font-size");
		if (!style) {
			style = document.createElement("style");
			style.id = "global-font-size";
			document.head.appendChild(style);
		}
		style.textContent = `
			:where(p, span, li, a, button, input, textarea, label, h1, h2, h3, h4, h5, h6):not([data-accessibility-panel]):not([data-accessibility-panel] *) { 
				font-size: ${size} !important; 
			}
		`;
	};

	const applyGlobalCSS = (property, value) => {
		const root = document.documentElement;
		root.style.setProperty(property, value);
	};

	const removeGlobalCSS = (property) => {
		const root = document.documentElement;
		root.style.removeProperty(property);
	};

	const handleTextSpacing = () => {
		setTextSpacing(!textSpacing);
		let style = document.getElementById("text-spacing-style");
		if (!textSpacing) {
			applyGlobalCSS("--text-spacing", "0.16em");
			if (!style) {
				style = document.createElement("style");
				style.id = "text-spacing-style";
				document.head.appendChild(style);
			}
			style.textContent = `
				:where(p, span, li, a, button, input, textarea, label, h1, h2, h3, h4, h5, h6):not([data-accessibility-panel]):not([data-accessibility-panel] *) { 
					letter-spacing: var(--text-spacing) !important; 
				}
			`;
		} else {
			removeGlobalCSS("--text-spacing");
			if (style) style.remove();
		}
	};

	const handleLineHeight = () => {
		setLineHeight(!lineHeight);
		let style = document.getElementById("line-height-style");
		if (!lineHeight) {
			applyGlobalCSS("--line-height", "1.6");
			if (!style) {
				style = document.createElement("style");
				style.id = "line-height-style";
				document.head.appendChild(style);
			}
			style.textContent = `
				:where(p, span, li, a, button, input, textarea, label, h1, h2, h3, h4, h5, h6):not([data-accessibility-panel]):not([data-accessibility-panel] *) { 
					line-height: var(--line-height) !important; 
				}
			`;
		} else {
			removeGlobalCSS("--line-height");
			if (style) style.remove();
		}
	};

	const handleDyslexiaFont = () => {
		setDyslexiaFont(!dyslexiaFont);
		if (!dyslexiaFont) {
			applyGlobalCSS("--dyslexia-font", "OpenDyslexic, Comic Sans MS, sans-serif");
			const style = document.createElement("style");
			style.id = "dyslexia-font";
			style.textContent = `
				body, body *:not([data-accessibility-panel]):not([data-accessibility-panel] *) { 
					font-family: var(--dyslexia-font) !important; 
				}
			`;
			document.head.appendChild(style);
		} else {
			removeGlobalCSS("--dyslexia-font");
			const existingStyle = document.getElementById("dyslexia-font");
			if (existingStyle) existingStyle.remove();
		}
	};

	const handleAdhdMode = () => {
		setAdhdMode(!adhdMode);
		if (!adhdMode) {
			applyGlobalCSS("--reduced-motion", "reduce");
			document.body.style.setProperty("animation-duration", "0.01ms");
			document.body.style.setProperty("animation-iteration-count", "1");
			document.body.style.setProperty("transition-duration", "0.01ms");
		} else {
			removeGlobalCSS("--reduced-motion");
			document.body.style.removeProperty("animation-duration");
			document.body.style.removeProperty("animation-iteration-count");
			document.body.style.removeProperty("transition-duration");
		}
	};

	const handleSaturation = () => {
		setSaturation(!saturation);
		if (!saturation) {
			ensureGlobalFilterStyle();
			applyGlobalCSS("--saturation", "0.3");
		} else {
			applyGlobalCSS("--saturation", "1");
		}
	};

	const handleInvertColors = () => {
		setInvertColors(!invertColors);
		if (!invertColors) {
			ensureGlobalFilterStyle();
			applyGlobalCSS("--invert", "1");
		} else {
			applyGlobalCSS("--invert", "0");
		}
	};

	const handleHighlightLinks = () => {
		setHighlightLinks(!highlightLinks);
		if (!highlightLinks) {
			applyGlobalCSS("--link-highlight", "2px solid #ff0000");
			const style = document.createElement("style");
			style.id = "link-highlight";
			style.textContent = `
				a { border: var(--link-highlight) !important; background: rgba(255, 0, 0, 0.1) !important; }
				[data-accessibility-panel] a { border: none !important; background: none !important; }
			`;
			document.head.appendChild(style);
		} else {
			removeGlobalCSS("--link-highlight");
			const existingStyle = document.getElementById("link-highlight");
			if (existingStyle) existingStyle.remove();
		}
	};

	const handleTextToSpeech = () => {
		if ("speechSynthesis" in window) {
			const utterance = new SpeechSynthesisUtterance(document.body.innerText);
			utterance.rate = 0.8;
			utterance.pitch = 1;
			speechSynthesis.speak(utterance);
		}
	};

	const handleLargeCursor = () => {
		setLargeCursor(!largeCursor);
		if (!largeCursor) {
			applyGlobalCSS("--cursor-size", "large");
			document.body.style.cursor =
				"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M0 0h32v32H0z' fill='%23000'/%3E%3C/svg%3E\"), auto";
		} else {
			removeGlobalCSS("--cursor-size");
			document.body.style.cursor = "";
		}
	};

	const handlePauseAnimations = () => {
		setPauseAnimations(!pauseAnimations);
		if (!pauseAnimations) {
			applyGlobalCSS("--animation-play-state", "paused");
			const style = document.createElement("style");
			style.id = "pause-animations";
			style.textContent = "body *:not([data-accessibility-panel]):not([data-accessibility-panel] *) { animation-play-state: var(--animation-play-state) !important; }";
			document.head.appendChild(style);
		} else {
			removeGlobalCSS("--animation-play-state");
			const existingStyle = document.getElementById("pause-animations");
			if (existingStyle) existingStyle.remove();
		}
	};

	const handleHideImages = () => {
		setHideImages(!hideImages);
		if (!hideImages) {
			applyGlobalCSS("--hide-images", "none");
			const style = document.createElement("style");
			style.id = "hide-images";
			style.textContent = `
				img, picture, video, svg { display: var(--hide-images) !important; }
				[data-accessibility-panel] img, [data-accessibility-panel] picture, [data-accessibility-panel] video, [data-accessibility-panel] svg { display: initial !important; }
			`;
			document.head.appendChild(style);
		} else {
			removeGlobalCSS("--hide-images");
			const existingStyle = document.getElementById("hide-images");
			if (existingStyle) existingStyle.remove();
		}
	};

	const resetAllSettings = () => {
		handleFontSizeChange("1rem");
		setTheme("light");
		setTextSpacing(false);
		setLineHeight(false);
		setDyslexiaFont(false);
		setAdhdMode(false);
		setSaturation(false);
		setInvertColors(false);
		setHighlightLinks(false);
		setLargeCursor(false);
		setPauseAnimations(false);
		setHideImages(false);
		if (screenReaderEnabled) {
			toggleScreenReader();
		}

		// Clear all custom styles
		document.body.style.letterSpacing = "";
		document.body.style.lineHeight = "";
		document.body.style.fontFamily = "";
		document.body.style.filter = "";
		document.body.style.cursor = "";
		document.body.style.removeProperty("animation-duration");
		document.body.style.removeProperty("animation-iteration-count");
		document.body.style.removeProperty("transition-duration");

		// Remove custom style elements
		["global-font-size", "text-spacing-style", "line-height-style", "link-highlight", "pause-animations", "hide-images", "global-filters", "dyslexia-font"].forEach((id) => {
			const element = document.getElementById(id);
			if (element) element.remove();
		});

		// Clear CSS custom properties
		["--text-spacing", "--line-height", "--dyslexia-font", "--reduced-motion", "--saturation", "--invert", "--link-highlight", "--cursor-size", "--animation-play-state", "--hide-images"].forEach(
			(prop) => {
				removeGlobalCSS(prop);
			},
		);
	};

	// Keyboard shortcut handler
	React.useEffect(() => {
		const handleKeyDown = (event) => {
			if (event.ctrlKey && event.key === "F2") {
				event.preventDefault();
				setIsOpen(!isOpen);
			}
		};

		document.addEventListener("keydown", handleKeyDown);
		return () => document.removeEventListener("keydown", handleKeyDown);
	}, [isOpen]);

	const accessibilityOptions = [
		{ icon: Plus, label: "Bigger Text", action: () => handleFontSizeChange("1.2rem") },
		{ icon: Minus, label: "Smaller Text", action: () => handleFontSizeChange("0.8rem") },
		{ icon: AlignJustify, label: "Text Spacing", action: handleTextSpacing, active: textSpacing },
		{ icon: Type, label: "Line Height", action: handleLineHeight, active: lineHeight },
		{ icon: Type, label: "Dyslexia Friendly", action: handleDyslexiaFont, active: dyslexiaFont },
		{ icon: Waves, label: "ADHD Mode", action: handleAdhdMode, active: adhdMode },
		{ icon: Palette, label: "Saturation", action: handleSaturation, active: saturation },
		{ icon: Moon, label: "Light-Dark", action: () => setTheme(theme === "light" ? "dark" : "light") },
		{ icon: FlipHorizontal, label: "Invert Colors", action: handleInvertColors, active: invertColors },
		{ icon: Link, label: "Highlight Links", action: handleHighlightLinks, active: highlightLinks },
		{ icon: Volume2, label: "Text To Speech", action: handleTextToSpeech },
		{ icon: screenReaderEnabled ? VolumeX : Volume2, label: "Screen Reader", action: toggleScreenReader, active: screenReaderEnabled },
		{ icon: MousePointer, label: "Cursor", action: handleLargeCursor, active: largeCursor },
		{ icon: Pause, label: "Pause Animation", action: handlePauseAnimations, active: pauseAnimations },
		{ icon: ImageOff, label: "Hide Images", action: handleHideImages, active: hideImages },
	];

	return (
		<div
			className="fixed bottom-8 right-8 z-50"
			data-accessibility-panel
		>
			<div
				className="group"
				data-accessibility-panel
			>
				<button
					aria-label="Accessibility options"
					onClick={() => setIsOpen(true)}
					className="flex items-center justify-center gap-3 rounded-full w-16 group-hover:w-64 h-16 bg-indigo-600 hover:bg-indigo-700 shadow-lg transition-all duration-300 overflow-hidden pr-4 pl-0 group-hover:pl-6"
					data-accessibility-panel
				>
					<PersonStanding className="w-6 h-6 scale-150 shrink-0 dark:text-white translate-x-3 group-hover:translate-x-0" />
					<div className="overflow-hidden max-w-0 group-hover:max-w-[160px] transition-all duration-300">
						<span className="block text-white text-base font-medium whitespace-nowrap opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300">
							Accessibility options
						</span>
					</div>
				</button>
			</div>

			{isOpen && (
				<>
					<div
						className="fixed inset-0 bg-black/40"
						aria-hidden="true"
						onClick={() => setIsOpen(false)}
						data-accessibility-panel
					></div>
					<motion.aside
						variants={drawerVariants}
						initial="hidden"
						animate="visible"
						className="fixed inset-y-0 right-0 h-full w-full max-w-md p-6 rounded-l-2xl shadow-xl bg-slate-50"
						role="dialog"
						aria-modal="true"
						aria-labelledby="accessibility-title"
						data-accessibility-panel
					>
						<motion.div className="h-full flex flex-col">
							{/* Header */}
							<motion.div variants={itemVariants}>
								<div className="px-0 pb-4">
									<div className="flex items-center justify-between">
										<h2
											id="accessibility-title"
											className="text-xl font-semibold text-indigo-600"
										>
											Accessibility options
										</h2>
										<div className="flex items-center gap-2">
											<span className="text-indigo-600 border border-indigo-200 rounded px-2 py-1 text-sm">Ctrl+F2</span>
											<button
												aria-label="Close accessibility panel"
												onClick={() => setIsOpen(false)}
												className="h-8 w-8 p-0 rounded hover:bg-gray-100"
												data-accessibility-panel
											>
												×
											</button>
										</div>
									</div>
								</div>
							</motion.div>

							{/* Grid of accessibility options */}
							<motion.div
								variants={itemVariants}
								className="flex-1 grid grid-cols-3 gap-4"
							>
								{accessibilityOptions.map((option, index) => (
									<motion.div
										key={index}
										variants={itemVariants}
										transition={{ delay: index * 0.05 }}
									>
										<button
											onClick={option.action}
											className={`h-20 w-full flex flex-col items-center hover:ring ring-indigo-300 justify-center gap-2 rounded-lg transition-all border ${
												option.active ? "bg-indigo-100 border-indigo-300 text-indigo-700" : "bg-white hover:bg-slate-50 border-slate-200 text-slate-800"
											}`}
											data-accessibility-panel
										>
											<option.icon className={`w-6 h-6 ${option.active ? "text-indigo-700" : "text-slate-800"}`} />
											<span className={`text-xs font-medium ${option.active ? "text-indigo-700" : "text-slate-800"}`}>{option.label}</span>
										</button>
									</motion.div>
								))}
							</motion.div>

							{/* Footer */}
							<motion.div
								variants={itemVariants}
								className="mt-6 flex items-center justify-between"
							>
								<button
									onClick={resetAllSettings}
									className="text-indigo-600 border border-indigo-200 hover:bg-indigo-50 flex items-center gap-2 rounded px-3 py-2"
									data-accessibility-panel
								>
									<RotateCcw className="w-4 h-4" />
									Reset All Settings
								</button>
								<span className="text-xs text-slate-500">Created by Nashik Police Department</span>
							</motion.div>
						</motion.div>
					</motion.aside>
				</>
			)}
		</div>
	);
};

export default PageAccessibilityChanger;

5. Always add a dark: css class for tailwind ... every component should be dark friendly.

6. Try Using ShadCN UI components as much as possible as it increases the SEO and Accessibility and theme issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment