Skip to content

Instantly share code, notes, and snippets.

Last active Mar 8, 2021
What would you like to do?
Duolingo (more) Accessible Styles
/* ==UserStyle==
@name Duolingo (more) accessible styles
@version 0.1.0
@description A stylesheet to make Duolingo more accessible. Focuses on focus indicators at the moment.
@author Fotis Papadogeorgopoulos <> (
@license CC-BY-NC-SA-4.0
@var color user-focus-color "User focus color" #000
@var number user-focus-width "User focus color" 2px
@var number user-focus-border-radius "User focus border radius" 0.25rem
==/UserStyle== */
@-moz-document domain("") {
:root {
--user-focus-color: #000;
--user-focus-width: 2px;
--user-focus-border-radius: 0.25rem;
/* First, do no harm: enable focus styles as a blanket rule */
details:focus {
/* Add a focus indicator that follows borders. Black works OK for most buttons on the site */
box-shadow: 0 0 0 var(--user-focus-width) var(--user-focus-color);
border-radius: var(--user-focus-border-radius);
/* Keep an outline in Windows High-Contrast Mode (WHCM) */
outline: var(--user-focus-width) solid transparent;
/* Now we get to refine these. These are optional! */
/* These buttons use ::after for their borders, so we "delegate" the focus style to them */
/* If the data-test ids ever change, no big deal, the blanket focus styles still work */
[data-test="skill-popout"] button:focus {
box-shadow: none;
[data-test="skill-popout"] button:focus::after {
box-shadow: 0 0 0 var(--user-focus-width) var(--user-focus-color);
/* Ensure good color contrast even for skills that are unavailable.
* I could not find a good way to target only the unavailable ones*, so they all get the same colour
* * you could use [data-test="tree-section"] ~ :not([data-test="tree-section"]) [data-test="skill"],
* but that is fickle and not necessary in this case.
* Do not change the color of popouts (for now, at least)
[data-test="skill"] > [tabindex="0"] div {
color: #3c3c3c;
/* Size navigation links (flex children on narrow screen), to make indicators (box-shadow) visible */
ul li a[href] {
/* Fallback */
width: 100%;
/* What we want: getting the maximum size from children (image width) */
width: max-content;
/* The pop-out for checkpoints is either a bright yellow or a hot purple. Both of them have not-great contrast with white text. Use dark text instead. */
/* Completed checkpoint: nestled between to tree-sections (but not a tree section itself) */
/* Popup: inside a checkpoint, has a z-index of 1 (alternative if that breaks: [tabindex="0"] ~ div) */
[data-test="tree-section"] ~ :not([data-test="tree-section"]) [style="z-index: 1;"] div,
[data-test="tree-section"] ~ :not([data-test="tree-section"]) [style="z-index: 1;"] button {
color: #111 !important
/* Do not use sticky top-bottom bar on short screens. Solves resizing / zooming issues, where they cover the whole screen. */
@media screen and (max-height: 34rem) {
/* Top navigation wrapper */
#root > div > div > div {
position: static;
/* Top navigation (yeah, brittle selector, I know) */
#root > div > div > div> div:nth-child(2) {
/* relative instead of static, to allow the pop-out menu to position correctly */
position: relative;
/* Bottom navigation. It ends up at the top, but c'est la vie */
#root > div > div > div > ul {
position: static;
/* Barbell circle icon. The selector to make it static is a mess. Let's make it smaller instead. */
[data-test="global-practice"] {
width: 36px;
height: 36px;
[data-test="global-practice"] img {
height: 16px;
margin-top: 7px;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment