Improve Image Accessibility with Cloudinary

Ekene Eze

Accessibility is one of the most prominent concerns in modern web development. The need to optimize websites for screen readers and cater to the needs of the visually impaired cannot be overemphasized. In this post, we’ll look at how we can improve the accessibility of our media assets using Cloudinary and Next.js. We’ll build a demo with Next.js to implement accessibility enhancements like - auto-generating alt text for images, color-blind friendly images, dark/light modes, and more.

Prerequisites

To follow along with this tutorial, you will need to have:

  • A free Cloudinary account.
  • Working experience with React and Next.js.

Setting up the Project

Create a new Next.js application using the following command:

1npx create-next-app media-assets

The above command creates a starter next.js application in the project folder.

Now, run these commands to navigate into the project directory and install the required dependencies:

1cd media-assets
2npm i @cloudinary/url-gen isomorphic-dompurify

After the installation is complete, run:

1npm run dev

Next.js will start a live development server at http://localhost:3000

Implementing the dark-mode effect

We will implement dark mode using the Next.js custom [_document](https://nextjs.org/docs/advanced-features/custom-document#caveats) configuration.

First, override the default Document by creating a _document.js file in the /pages folder, then add the following code snippet:

1//pages/_document.js
2import Document, { Html, Head, Main, NextScript } from "next/document";
3import DOMPurify from "isomorphic-dompurify";
4class MyDocument extends Document {
5 render() {
6 return (
7 <Html lang="en">
8 <Head></Head>
9 <body>
10 <script
11 dangerouslySetInnerHTML={{
12 __html: DOMPurify.sanitize(themeInitializerScript)
13 }}
14 ></script>
15 <Main />
16 <NextScript />
17 </body>
18 </Html>
19 );
20 }
21}
22// This function needs to be a String
23const themeInitializerScript = `(function() {
24 ${setInitialColorMode.toString()}
25 setInitialColorMode();
26})()
27`;

In the snippet above, we import Document, { Html, Head, Main, NextScript } from the next/document package. We also Import DOMPurify from our isomorphic-dompurify package and use it to sanitize our dangerouslySetHtml data.

Next, we define a themeInitializerScript to set our initial color mode, this is the data we sanitize using the DOMPurify package. Notice that we called a setInitialColorMode() function our themeInitializerScript ? let’s define that function below:

1//pages/_document.js
2function setInitialColorMode() {
3 // Check initial color preference
4 function getInitialColorMode() {
5 const persistedPreferenceMode = window.localStorage.getItem("theme");
6 const hasPersistedPreference = typeof persistedPreferenceMode === "string";
7 if (hasPersistedPreference) {
8 return persistedPreferenceMode;
9 }
10 // Check the current preference
11 const preference = window.matchMedia("(prefers-color-scheme: dark)");
12 const hasMediaQueryPreference = typeof preference.matches === "boolean";
13 if (hasMediaQueryPreference) {
14 return preference.matches ? "dark" : "light";
15 }
16 return "light";
17 }

In the snippet above, we:

  • Create the getInitialColorMode() function to fetch the persistedPreferenceMode from the LocalStorage using the theme key.
  • Create a hasPersistancePreference variable that stores a boolean result after checking to ensure that the typeof of the persistedPreferenceMode is a string.
  • Declare a conditional statement that returns the persistedPreferenceMode if hasPersistedPreference is true.
  • Set preference variable that equals the value of window.matchMedia("(prefers-color-scheme: dark)").
  • Set a check to confirm the typeof of preference.matches is equal to a boolean and stores its value in the hasMediaQueryPreference.
  • Lastly, we create a conditional statement to check if hasMediaQueryPreference is true, and perform different actions when the conditions match and fail respectively.

Next, let’s link these settings to our next app by adding the following snippet:

1//pages/_document.js
2const currentColorMode = getInitialColorMode();
3 const element = document.documentElement;
4 element.style.setProperty("--initial-color-mode", currentColorMode);
5 // If darkmode, apply darkmode
6 if (currentColorMode === "dark")
7 document.documentElement.setAttribute("data-theme", "dark");
8}

In the snippet above, we store the value of our getInitialColorMode() function into a new variable currentColorMode. We do the same to document.documentElement as we store its value into a new element variable to make it easier to reference.

With that, we can easily assign a CSS property to that element using the setProperty() function, which sets the value of our --initial-color-mode property to the currentColorMode(). Lastly, we check if the currentcColorMode is dark and, if so, set the data-theme attribute to dark. Next, let’s configure the index page with the necessary markup and logic for our demo.

First, we’ll set up state management in our application using React hooks. This will allow us to track and modify the state of our applications theme such that we can toggle it from light to dark and vice versa:

1//pages/index.js
2import { useEffect, useState } from "react"
3const HomePage = () => {
4const [ darkTheme, setDarkTheme ] = useState(undefined)
5useEffect(() => {
6 const root = window.document.documentElement;
7 const initialColorValue = root.style.getPropertyValue(
8 "--initial-color-mode"
9 );
10 // Set initial darkmode to light
11 setDarkTheme(initialColorValue === "dark");
12 }, []);
13}

Here, we:

  • Import the useState, and useEffect hooks from react for state management.
  • Initialize a darkTheme state and assign it an undefined value for a start.
  • Create a useEffect hook without a dependency that sets the initial theme to light mode.

Next, we create another useEffect hook that depends on the darkTheme state to conditionally toggle our application from light mode to dark mode. It does this by setting the data-theme attribute on the DOM and storing the value in localStorage:

1// pages/index.js
2useEffect(() => {
3 if (darkTheme !== undefined) {
4 if (darkTheme) {
5 // Set value of darkmode to dark
6 document.documentElement.setAttribute("data-theme", "dark");
7 window.localStorage.setItem("theme", "dark");
8 } else {
9 // Set value of darkmode to light
10 document.documentElement.removeAttribute("data-theme");
11 window.localStorage.setItem("theme", "light");
12 }
13 }
14 }, [darkTheme]);

In the above snippet, we will do a check to ensure that the darkTheme value is not undefined, then we set the data-theme attribute and the localstorage's theme key to dark. Else, we remove the data-theme attribute and set the theme key to light.

Next, we create a checkbox input with a checked attribute and set its value to the darkTheme state. Then, we add an onChange event to listen and execute our handleToggle() function when the darkTheme state changes:

1//pages/index.js
2<div>
3 {darkTheme !== undefined && (
4 <form action="#">
5 <label className="switch">
6 <input
7 type="checkbox"
8 checked={darkTheme}
9 onChange={handleToggle}
10 />
11 <span className="slider"></span>
12 </label>
13 </form>
14 )}
15</div>

Next, let's define the handleToggle``() function to toggle the theme by adding the following snippet:

1const handleToggle = (event) => {
2 setDarkTheme(event.target.checked);
3 };

With this, we have successfully configured dark mode theming in this project. Next, let’s implement the color blind effects on assets uploaded to our Cloudinary application.

Implementing Color Blind Effects on Assets

For this part of the article, we will integrate our application with Cloudinary using the Cloudinary React SDK. Here, we will work to cater for a commonly observed condition, deuteranopia (difficulty seeing different shades of red, green, and yellow).

Open the index.js file and update it with the snippet below:

1// pages/index.js
2import { Cloudinary } from "@cloudinary/url-gen";
3import { simulateColorBlind } from "@cloudinary/url-gen/actions/effect";
4
5const HomePage = () => {
6 const [colorBlindEffect, setColorBlindEffect] = useState("");
7 const handleClick = () => {
8 setColorBlindEffect("");
9 const cld = new Cloudinary({
10 cloud: {
11 cloudName:YOUR_CLOUDNAME
12 }
13 });
14 const myImage = cld.image("color-effect/afro");
15 myImage.effect(simulateColorBlind().condition("deuteranopia"));
16 const imageUrl = myImage.toURL();
17 setColorBlindEffect(imageUrl);
18 };
19 const reset = () => {
20 setColorBlindEffect("/afro.jpg");
21 };
22
23 return (
24 <>
25 <div className="container">
26 <section id="image">
27 <div className="image-wrapper">
28 <img
29 src={colorBlindEffect ? colorBlindEffect : "/afro.jpg"}
30 className="afro"
31 alt="woman on afro"
32 />
33 </div>
34 <div>
35 <button
36 onClick={handleClick}
37 className="btn"
38 >
39 ColorBlind Switch
40 </button>
41 <button className="btn" onClick={reset}>
42 Restore Image
43 </button>
44 </div>
45 </section>
46 </div>
47 </>
48 );
49};
50export default HomePage;

In the snippet above, we:

  • Import Cloudinary and the simulateColorBlind method to help achieve the desired functionalities.
  • We then define a colorBlindEffect state to hold the initial value of the effect.
  • Next, we create an HTML image element to display the image, along with two buttons to initiate the Cloudinary conversion of the image, and restore the original image respectively.
  • In the handleClick function, we implement the logic to reset our colorBlindEffect state and fetch a sample image from Cloudinary. With the image saved in a variable myImage, we then apply the color blind effect on it by calling the simulateColorBlind().condition() method. In our case, we want to simulate the deuteranopia condition, so we specify it in the method as condition("deuteranopia"). To convert the value of myImage to a URL, we call the toURL() method on it and save the value to a new variable imageUrl. Finally, we update our colorBlindEffect state with the imageURL.

With this, we have successfully implemented a colour-blind effect for the deuteranopia condition using Cloudinary. You can access this project here for a more hands-on experience with the source code.

Implementing Auto Alt Text Generation

For this part of the article, we’ll show you how you can generate descriptive text for web images using Cloudinary's Google Auto Tagging add-on. To achieve this, we’ll leverage the next.js API routes feature to create a Node environment that communicates with the Cloudinary API.

In your pages folder, create an API route called update.js and add the following code:

1// pages/update.js
2import { v2 as Cloudinary } from "cloudinary";
3export default function handler(req, res) {
4 try {
5 Cloudinary.config({
6 cloud_name: process.env.CLOUD_NAME,
7 api_key: process.env.API_KEY,
8 api_secret: process.env.API_SECRET,
9 secure: true
10 });
11 const imageUpdate = Cloudinary.api.update(
12 "color-effect/afro",
13 { categorization: "google_tagging", auto_tagging: 0.9 },
14 function (error, result) {
15 console.log(result, error);
16 return result;
17 }
18 );
19 imageUpdate.then((data) => {
20 res.status(200).json(data.tags[0]);
21 });
22 } catch (error) {
23 console.log(error.message);
24 res.status(500).json();
25 }
26}

Here, we:

  • Import the Cloudinary Node SDK and export a handler function with req and res parameters.
  • Create a conditional statement to handle the Cloudinary configurations and execute an update call.
  • Update the image using the update API and set the categorization property to google_tagging.

With the above configurations, we will be able to receive a URL from Cloudinary that contains an array of automatically generated texts that defines the image displayed.

Next, let's display the returned result on the client by adding the following snippet to our index.js file:

1//pages/index.js
2const [altText, setAltText] = useState("");
3const [altTextToggle, setAltTextToggle] = useState(false);
4
5const toggleAlt = async () => {
6 const altT = await fetch("/api/update");
7 const alt = await altT.json();
8 setAltText(alt);
9 setAltTextToggle(!altTextToggle);
10}

Here, we make a fetch request to our /api/update route. As earlier mentioned, this endpoint will return a URL from Cloudinary that contains an array of generated texts that describes the image displayed. When we get this response, we update the altText in state with the first item in the array that we returned from the server.

Now that we have the descriptive text in state, let’s apply it to our HTML image element and also just display it on screen for visibility:

1<section id="image">
2 <div className="image-wrapper">
3 <img
4 src={colorBlindEffect ? colorBlindEffect : "/afro.jpg"}
5 className="afro"
6 alt={altText}
7 />
8 {altTextToggle ? <i>{altText}</i> : ""}
9 </div>
10 <button className="btn" onClick={toggleAlt}>
11 view alt text
12 </button>
13</section>

Bringing it all together, we’ve just built a small Next.js app with light and dark modes. We implement an accessibility-friendly image display that caters for the deuteranopia condition and lastly showed you how to auto-generate alt texts for your images using Cloudinary. Here’s a working demo for your convenience.

Feel free to use the source code via this sandbox and extend it as you see fit.

Ekene Eze

Director of DX at Plasmic

I work at Plasmic as the Director of Developer Experience and I love executing projects that help other software engineers. Passionate about sharing with the community, I often write, present workshops, create courses, and speak at conferences about web development concepts and best practices.