home projects speaker

Accessible syntax highlight theme

I really like syntax highlighting, and it helps greatly during development, but also when I read code. I have grown very fond of Solarized Dark and One Dark (not light!), but I wanted to be more accessible, so I created my own themes that adheres with the WCAG requirements.

The colors

WCAG AA compliant for color contrast. WCAG AAA compliance forces the values on a light background to be too similar to each other to be used effectively for syntax highlighting.

Dark theme

All colors have a contrast ratio of >=5 against the background color #262831, Sky Captain.

#859BA3
#C79500
#2CAAA0
#469EDD
#8C9B9B
#E4E4E7
#262831
#FF6666

Light theme

All colors have a contrast ratio of >=5 against the background color #F5F6F7, Gram’s Hair.

#566D71
#806200
#1E766D
#1D699F
#5C6A6A
#000000
#F5F6F7
#CD0404

Contrast ratio

ColorHexRatioNormal TextLarge Text
Arona#859BA35.04:1AAAAA
Vivid Amber#C795005.4:1AAAAA
Sea Kale#2CAAA05.14:1AAAAA
Flax Flower Blue#469EDD5.03:1AAAAA
Boredom#8C9B9B5.08:1AAAAA
Lilac Mist#E4E4E711.57:1AAAAAA
Pompelmo#FF66665.13:1AAAAA
Distance#566D715.07:1AAAAA
Ground Earth#8062005.29:1AAAAA
Advantageous#1E766D5.01:1AAAAA
Jazz Blue#1D699F5.43:1AAAAA
Mountain Pass#5C6A6A5.21:1AAAAA
Black#00000019.4:1AAAAAA
Rebellion Red#CD04045.36:1AAAAA

CSS Variables

css
:root {
  --color-1: #859ba3;
  --color-2: #c79500;
  --color-3: #2caaa0;
  --color-4: #469edd;
  --color-5: #8c9b9b;
  --color-6: #e4e4e7;
  --color-7: #262831;
  --color-8: #ff0000;
}

:root.theme--light {
  --color-1: #566d71;
  --color-2: #806200;
  --color-3: #1e766d;
  --color-4: #1d699f;
  --color-5: #5c6a6a;
  --color-6: #000000;
  --color-7: #f5f6f7;
  --color-8: #ff0000;
}

PrismJS example with dark colors

css
code[class*='language-'] .namespace,
pre[class*='language-'] .namespace {
  opacity: 0.7;
}

code[class*='language-'] .token.comment,
code[class*='language-'] .token.prolog,
code[class*='language-'] .token.doctype,
code[class*='language-'] .token.cdata,
pre[class*='language-'] .token.comment,
pre[class*='language-'] .token.prolog,
pre[class*='language-'] .token.doctype,
pre[class*='language-'] .token.cdata {
  color: #859ba3;
}

code[class*='language-'] .token.null,
code[class*='language-'] .token.operator,
code[class*='language-'] .token.boolean,
code[class*='language-'] .token.number,
pre[class*='language-'] .token.null,
pre[class*='language-'] .token.operator,
pre[class*='language-'] .token.boolean,
pre[class*='language-'] .token.number {
  color: #c79500;
}

code[class*='language-'] .token.attr-name,
code[class*='language-'] .token.string,
pre[class*='language-'] .token.attr-name,
pre[class*='language-'] .token.string {
  color: #2caaa0;
}

code[class*='language-'] .token.entity,
code[class*='language-'] .token.url,
.language-css code[class*='language-'] .token.string,
.style code[class*='language-'] .token.string,
pre[class*='language-'] .token.entity,
pre[class*='language-'] .token.url,
.language-css pre[class*='language-'] .token.string,
.style pre[class*='language-'] .token.string {
  color: #2caaa0;
}

code[class*='language-'] .token.selector,
pre[class*='language-'] .token.selector {
  color: #e4e4e7;
}

code[class*='language-'] .token.atrule,
code[class*='language-'] .token.attr-value,
code[class*='language-'] .token.keyword,
code[class*='language-'] .token.control,
code[class*='language-'] .token.directive,
code[class*='language-'] .token.important,
code[class*='language-'] .token.unit,
pre[class*='language-'] .token.atrule,
pre[class*='language-'] .token.attr-value,
pre[class*='language-'] .token.keyword,
pre[class*='language-'] .token.control,
pre[class*='language-'] .token.directive,
pre[class*='language-'] .token.important,
pre[class*='language-'] .token.unit {
  color: #469edd;
}

code[class*='language-'] .token.regex,
code[class*='language-'] .token.statement,
pre[class*='language-'] .token.regex,
pre[class*='language-'] .token.statement {
  color: #2caaa0;
}

code[class*='language-'] .token.placeholder,
code[class*='language-'] .token.variable,
pre[class*='language-'] .token.placeholder,
pre[class*='language-'] .token.variable {
  color: #469edd;
}

code[class*='language-'] .token.property,
code[class*='language-'] .token.tag,
pre[class*='language-'] .token.property,
pre[class*='language-'] .token.tag {
  font-style: italic;
}

code[class*='language-'] .token.important,
code[class*='language-'] .token.statement,
pre[class*='language-'] .token.important,
pre[class*='language-'] .token.statement {
  font-weight: bold;
}

code[class*='language-'] .token.punctuation,
pre[class*='language-'] .token.punctuation {
  color: #8c9b9b;
}

code[class*='language-'] .token.entity,
pre[class*='language-'] .token.entity {
  cursor: help;
}

code[class*='language-'] .token.debug,
pre[class*='language-'] .token.debug {
  color: #ff6666;
}

pre,
pre.ph {
  background-color: #262831;
  color: #e4e4e7;
}

React example

tsx
import React from 'react';
import {
  BrowserRouter,
  Routes,
  Route,
  useLocation,
  useNavigate,
} from 'react-router-dom';
import { GlobalAppShell } from '@tfso/global-app-shell';
import type {
  GlobalNavigationPropsType,
  GlobalHeaderWCPropsType,
} from './types';

// -- Tiny helper that runs inside the Router and syncs props to the WC --
function SyncNavWithRouter({
  refNav,
  baseProps, // whatever you previously put in navigationArgs
}: {
  refNav: React.RefObject<any>;
  baseProps: GlobalNavigationPropsType;
}) {
  const location = useLocation();
  const navigate = useNavigate();

  const onNavigate = React.useMemo(
    () => (to: string) => {
      if (!to) return;
      navigate(to); // SPA navigation
    },
    [navigate]
  );

  React.useEffect(() => {
    if (!refNav.current) return;

    // Push updated props into the web component whenever the route changes.
    refNav.current.props = {
      ...baseProps,
      onNavigate,
      currentPath: location.pathname, // <- critical for is-active
      // keep urlPrefix if you use one, e.g. baseProps.urlPrefix
    };
  }, [refNav, baseProps, onNavigate, location.pathname]);

  return null;
}

export default function App({
  navigationArgs,
  headerArgs,
}: {
  navigationArgs: GlobalNavigationPropsType;
  headerArgs: GlobalHeaderWCPropsType;
}) {
  const refNav = React.useRef<any>(null);
  const refHeader = React.useRef<any>(null);

  React.useEffect(() => {
    if (refHeader.current) {
      refHeader.current.props = headerArgs;
    }
  }, [headerArgs]);

  return (
    <>
      <tfso-global-header ref={refHeader}></tfso-global-header>
      <tfso-global-navigation ref={refNav}></tfso-global-navigation>

      <GlobalAppShell isOpen={true}>
        <BrowserRouter /* basename="/app" if you mount under a sub-path */>
          {/* Keep WC in sync with the router */}
          <SyncNavWithRouter refNav={refNav} baseProps={navigationArgs} />

          {/* Your routed content */}
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/reports" element={<ReportsPage />} />
            <Route path="/reports/:id" element={<ReportDetails />} />
            {/* ...other routes */}
          </Routes>
        </BrowserRouter>
      </GlobalAppShell>
    </>
  );
}

About the author

Hi! My name is Alexander, and I am a creative frontender, specializing in UX, accessibility, universal design, frontend-architecture, node and design systems. I am passionate with open source projects and love to dabble with new emerging technologies related to frontend. With over 27 years of frontend experience, I have earned the right to be called a veteran. I am a lover of life, technologist at heart. If I am not coding, I am cooking and I love whisky and cigars. Oh, and coffee, I LOVE coffee!

If you want to know more about me, here is some links you might want to check out: GitHub, Instagram, Twitter, LinkedIn, CodePen, Slides.com, npm,

Speaker

I am also an avid speaker on several topics! Check out some of the things I speak about, and contact me if you are interested in having me at your next event!