circuit-board Make websites more readable with a Bash script

Calculate the contrast ratio between your website's text and background to make sure your site is easy to read.

If you want people to find your website useful, they need to be able to read it. The colors you choose for your text can affect the readability of your site. Unfortunately, a popular trend in web design is to use low-contrast colors when printing text, such as gray text on a white background. Maybe that looks really cool to the web designer, but it is really hard for many of us to read.

The W3C provides Web Content Accessibility Guidelines, which includes guidance to help web designers pick text and background colors that can be easily distinguished from each other. This is called the "contrast ratio." The W3C definition of the contrast ratio requires several calculations: given two colors, you first compute the relative luminance of each, then calculate the contrast ratio. The ratio will fall in the range 1 to 21 (typically written 1:1 to 21:1). The higher the contrast ratio, the more the text will stand out against the background. For example, black text on a white background is highly visible and has a contrast ratio of 21:1. And white text on a white background is unreadable at a contrast ratio of 1:1.

The W3C says body text should have a contrast ratio of at least 4.5:1 with headings at least 3:1, as defined by Success Criterion 1.4.3 Contrast (Minimum). The W3C also recommends at least 7:1 for body text and at least 4.5:1 for headings, as listed in Success Criterion 1.4.6 Contrast (Enhanced).

Calculating the contrast ratio can be a chore, so it's best to automate it. I've done that with this handy Bash script. In general, the script does these things:

  1. Gets the text color and background color
  2. Computes the relative luminance of each
  3. Calculates the contrast ratio

Get the colors

You may know that every color on your monitor can be represented by red, green, and blue (R, G, and B). To calculate the relative luminance of a color, my script will need to know the red, green, and blue components of the color. Ideally, my script would read this information as separate R, G, and B values. Web designers might know the specific RGB code for their favorite colors, but most humans don't know RGB values for the different colors. Instead, most people reference colors by names like "red" or "gold" or "maroon."

Fortunately, the GNOME Zenity tool has a desktop color-picker app that lets you use different methods to select a color, then returns the RGB values in a predictable format of rgb(R,G,B). Using Zenity makes it easy to get a color value:

color=$( zenity --title 'Set text color' --color-selection --color='black' )

In case the user (accidentally) clicks the Cancel button, the script assumes a color:

color=$( zenity --title 'Set text color' --color-selection --color='black' )
if [ $? -ne 0 ] ; then
  echo 'color canceled .. assume black'
  color='rgb(0,0,0)'
fi

My script does the same to set the background color value as $background, using white as the default color.

Compute the relative luminance

Once you have the foreground color in $color and the background color in $background, the next step is to compute the relative luminance for each. On its website, the W3C provides an algorithm to compute the relative luminance of a color:

For the sRGB colorspace, the relative luminance of a color is defined as L = 0.2126×R + 0.7152×G + 0.0722×B where R, G and B are defined as:

  • if RsRGB ≤ 0.04045 then R = RsRGB/12.92 else R = ((RsRGB+0.055)/1.055)2.4
  • if GsRGB ≤ 0.04045 then G = GsRGB/12.92 else G = ((GsRGB+0.055)/1.055)2.4
  • if BsRGB ≤ 0.04045 then B = BsRGB/12.92 else B = ((BsRGB+0.055)/1.055)2.4

and RsRGB, GsRGB, and BsRGB are defined as:

  • RsRGB = R8bit/255
  • GsRGB = G8bit/255
  • BsRGB = B8bit/255

Since Zenity returns color values in the format rgb(R,G,B), the script can pull apart the R, G, and B values to compute the relative luminance. Gawk makes this a simple task: using the comma as the field separator (-F,) and using the tr command to translate any character to a comma, we can pick just the text we want from the rgb(R,G,B) color value:

  R=$( echo $color | tr '(' ',' | gawk -F, '{print $2}' )
  G=$( echo $color | gawk -F, '{print $2}' )
  B=$( echo $color | tr -d ')' | gawk -F, '{print $3}' )

Calculating the final relative luminance is best done using the bc command line calculator. bc supports the simple if-then-else needed in the calculation, which makes this part easier to do. But since bc cannot directly calculate exponentiation using a non-integer exponent, we need to do some extra math using the natural logarithm instead:

  cat <<EOF | bc -l
scale=4
rsrgb=$R/255
gsrgb=$G/255
bsrgb=$B/255
if ( rsrgb <= 0.04045 ) r = rsrgb/12.92 else r = e( 2.4 * l((rsrgb+0.055)/1.055) )
if ( gsrgb <= 0.04045 ) g = gsrgb/12.92 else g = e( 2.4 * l((gsrgb+0.055)/1.055) )
if ( bsrgb <= 0.04045 ) b = bsrgb/12.92 else b = e( 2.4 * l((bsrgb+0.055)/1.055) )
0.2126*r + 0.7152*g + 0.0722*b
EOF

This passes several instructions to bc, including the if-then-else statements that are part of the relative luminance formula. Because the bc calculations take up several lines, the script uses cat <<EOF to read from a long block of text terminated by the line EOF before sending the results to bc to calculate and print the final value.

Calculate the contrast ratio

With the relative luminance of the text color and the background color, now the script can calculate the contrast ratio. The W3C determines the contrast ratio with this formula:

(L1 + 0.05) / (L2 + 0.05), where

  • L1 is the relative luminance of the lighter of the colors, and
  • L2 is the relative luminance of the darker of the colors.

Given two relative luminance values $r1 and $r2, it's easy to calculate the contrast ratio using the bc calculator:

  cat <<EOF | bc
scale=2
if ( $1 > $2 ) { l1=$1; l2=$2 } else { l1=$2; l2=$1 }
(l1 + 0.05) / (l2 + 0.05)
EOF

This uses an if-then-else statement to determine which value ($r1 or $r2) is the lighter or darker color. bc performs the resulting calculation and prints the result, which the script can store in a variable.

The final script

With the above, we can pull everything together into a final script. I use Zenity to display the final result in a text box:

#!/bin/bash

function luminance()
{
  R=$( echo $1 | tr '(' ',' | gawk -F, '{print $2}' )
  G=$( echo $1 | gawk -F, '{print $2}' )
  B=$( echo $1 | tr -d ')' | gawk -F, '{print $3}' )

  cat <<EOF | bc -l
scale=4
rsrgb=$R/255
gsrgb=$G/255
bsrgb=$B/255
if ( rsrgb <= 0.04045 ) r = rsrgb/12.92 else r = e( 2.4 * l((rsrgb+0.055)/1.055) )
if ( gsrgb <= 0.04045 ) g = gsrgb/12.92 else g = e( 2.4 * l((gsrgb+0.055)/1.055) )
if ( bsrgb <= 0.04045 ) b = bsrgb/12.92 else b = e( 2.4 * l((bsrgb+0.055)/1.055) )
0.2126*r + 0.7152*g + 0.0722*b
EOF
}

function contrast()
{
  cat <<EOF | bc
scale=2
if ( $1 > $2 ) { l1=$1; l2=$2 } else { l1=$2; l2=$1 }
(l1 + 0.05) / (l2 + 0.05)
EOF
}

# get colors

color=$( zenity --title 'Set text color' --color-selection --color='black' )
if [ $? -ne 0 ] ; then
  echo 'color canceled .. assume black'
  color='rgb(0,0,0)'
fi

background=$( zenity --title 'Set background' --color-selection --color='white' )
if [ $? -ne 0 ] ; then
  echo 'background canceled .. assume white'
  background='rgb(255,255,255)'
fi

lum1=$( luminance $color )
lum2=$( luminance $background )

rel=$( contrast $lum1 $lum2 )

# print results

( cat<<EOF
If text is $color on $background
the contrast ratio is:

$rel

Body text should be at least 4.5 (best if at least 7)
Headings should be at least 3 (best if at least 4.5)
EOF
) | zenity --text-info --title='Relative Luminance' --width=400 --height 300

The Zenity color picker does all the hard work of interpreting colors, which the user can select by clicking in the color wheel or by entering a value. Zenity accepts standard hex color values used on websites, like #000000 or #000 or rgb(0,0,0), all of which are the same black. Here's an example calculation for black text on a white background:

First, pick a text color:

Picking a color
Picking a color

Then, pick a background color:

Picking a background color
Picking a background color

The script will print the results in a text box:

Printing the results
Printing the results

Zenity also understands HTML named colors like "cadetblue" or "orange" or "gold." Enter the color name in Zenity then hit Tab, and Zenity will convert the color name into a hex color value.