A Bit Of Access

The code behind Alt Or Not, a browser extension / add-on for Twitter and Tweetdeck

Published by (updated )

This page describes the code behind Alt Or Not, a browser extension / add-on for Chrome, Firefox, and Edge.

For those interested I'll use this page to share the code, with additional comments to explain things. // Comments in the code look like this

The code here is updated to version 1.2, divided by file. Script.js and alt-or-not.css are used on both Twitter and TweetDeck. Twitter.js is used on Twitter and the TweetDeck preview, and tweetDeck.js is only used on TweetDeck. Go to the main Alt Or Not page for more information on the plugin and links to download it.

There are some additional files for the options page, these aren't included here as they only deal with retrieving and saving the options.

Script.js: Global variables and functions

// Observer config: we are checking for changes in the target node (element) and all its children
const cfg = { childList: true, subtree: true };
// Setting our initialization attempts to 0
var tries = 0;
// Default values for our settings
var disableButton = false;
var noCheck = false;
var hideLabels = false;
var hideAlt = false;
var toggleAlt = false;
var lightMode = false;
var prettyScroll = false;
// Loading our values from storage, depending on browser
// This is a function in case we want to reload on the fly later on
const load = function () {
    if (typeof browser !== "undefined") {
        // Firefox uses the browser namespace
        browser.storage.sync.get({
            disableButton: false,
            noCheck: false,
            hideLabels: false,
            hideAlt: false,
            toggleAlt: false,
            lightMode: false,
            prettyScroll: false
        }, function(items) {
            disableButton = items.disableButton;
            noCheck = items.noCheck;
            hideLabels = items.hideLabels;
            hideAlt = items.hideAlt;
            toggleAlt = items.toggleAlt;
            lightMode = items.lightMode;
            prettyScroll = items.prettyScroll;
        });
    } else {
        if (typeof chrome !== "undefined") {
            // Chrome and Edge use the chrome namespace
            chrome.storage.sync.get({
                disableButton: false,
                noCheck: false,
                hideLabels: false,
                hideAlt: false,
                toggleAlt: false,
                lightMode: false,
                prettyScroll: false
            }, function(items) {
                disableButton = items.disableButton;
                noCheck = items.noCheck;
                hideLabels = items.hideLabels;
                hideAlt = items.hideAlt;
                toggleAlt = items.toggleAlt;
                lightMode = items.lightMode;
                prettyScroll = items.prettyScroll;
            });
        } 
    }
}
load();
// We add and remove classes a lot, so compact functions are useful
const cla = function(e, c) {
    e.classList.add('tw-alt-'+c); 
};
const clr = function(e, c) {
    e.classList.remove('tw-alt-'+c); 
};
// Function to create our caption element and add it to our container
const caption = function(t, l) {
    if (!hideAlt && t != null) {
        let a = document.createElement('div');
        cla(a, 'txt');
        a.append(l);
        t.append(a);
    }
};
// Function to create our caption container
const captionContainer = function(t) {
    // See if the element already exists for the tweet
    let c = t.parentElement.querySelector('div.tw-alt-container');
    if (c == null) {
        // If not, we make a new one
        c = document.createElement('div');
        // Hiding it for screen readers, no need for repeated content 
        c.setAttribute('aria-hidden', true);
        cla(c, 'container');
        // Applying the light mode if it's enabled
        if (lightMode) {
            cla(c, 'light');
        }
        // If the feature is selected, we create a button, add an action, and add it to the container
        if (toggleAlt) {
            // We create a button
            let b = document.createElement('button');
            cla(b, 'toggle');
            b.innerHTML = 'Show alt';
            b.addEventListener('click', toggleCaptions);
            c.append(b);
            // Applying the class to the container that hides the text and shows the button
            cla(c, 'hidden');
        }
        // Add the container to the tweet
        t.after(c);
    }
    // Return the container element
    return c;
}
// The function that reveals the alt text
const toggleCaptions = function(e) {
    // Finding the parent of the button element
    let a = e.target.parentElement;
    // Toggling the hidden class on the container
    if (a.classList.contains('tw-alt-hidden')) {
        clr(a, 'hidden');
    } else {
        cla(a, 'hidden');
    }
}
// The callback function that runs the checking function on each changed node
const callback = function(ml, o) {
    ml.forEach(function(m) { check(m.target); });
};

Twitter.js: functions specific to Twitter

// Setting the default texts for terms we check, based on the document language
var text = null;
switch(document.documentElement.lang) {
    case 'ar': text = { media: 'الوسائط', image: 'الصورة', video: 'الفيديو المُضمن' }; break; // Arabic
    case 'ar-x-fm': text = { media: 'الوسائط', image: 'الصورة', video: 'الفيديو المُضمن' }; break; // Arabic (Feminine)
    case 'bn': text = { media: 'মিডিয়া', image: 'চিত্র', video: 'এম্বেড করা ভিডিও' }; break; // Bangla
    case 'eu': text = { media: 'Media', image: 'Irudia', video: 'Kapsulatutako bideoa' }; break; // Basque
    case 'bg': text = { media: 'Мултимедийно съдържание', image: 'Изображение', video: 'Вграден видеоклип' }; break; // Bulgarian
    case 'ca': text = { media: 'Continguts', image: 'Imatge', video: 'Vídeo incrustat' }; break; // Catalan
    case 'hr': text = { media: 'Medijski sadržaj', image: 'Slika', video: 'Ugrađeni videozapis' }; break; // Croatian
    case 'cs': text = { media: 'Média', image: 'Obrázek', video: 'Vložené video' }; break; // Czech
    case 'da': text = { media: 'Medier', image: 'Billede', video: 'Indlejret video' }; break; // Danish
    case 'nl': text = { media: 'Media', image: 'Afbeelding', video: 'Ingesloten video' }; break; // Dutch
    case 'fil': text = { media: 'Media', image: 'Larawan', video: 'Naka-embed na video' }; break; // Filipino
    case 'fi': text = { media: 'Media', image: 'Kuva', video: 'Upotettu video'}; break; // Finnish
    case 'fr': text = { media: 'Médias', image: 'Image', video: 'Vidéo intégrée' }; break; // French
    case 'gl': text = { media: 'Multimedia', image: 'Imaxe', video: 'Vídeo encaixado' }; break; // Galician
    case 'de': text = { media: 'Medien', image: 'Bild', video: 'Eingebettetes Video' }; break; // German
    case 'el': text = { media: 'Πολυμέσα', image: 'Εικόνα', video: 'Ενσωματωμένο βίντεο' }; break; // Greek
    case 'gu': text = { media: 'મીડિયા', image: 'છબી', video: 'એમ્બેડ કરેલો વિડિઓ' }; break; // Gujarati
    case 'he': text = { media: 'מדיה', image: 'תמונה', video: 'סרטון מוטבע' }; break; // Hebrew
    case 'hi': text = { media: 'मीडिया', image: 'छवि', video: 'एम्बेडेड वीडियो' }; break; // Hindi
    case 'hu': text = { media: 'Média', image: 'Kép', video: 'Beágyazott videó' }; break; // Hungarian
    case 'id': text = { media: 'Media', image: 'Gambar', video: 'Video terlekat' }; break; // Indonesian
    case 'ga': text = { media: 'Meáin', image: 'Íomhá', video: 'Físeán leabaithe' }; break; // Irish
    case 'it': text = { media: 'Contenuti', image: 'Immagine', video: 'Video incorporato' }; break; // Italian
    case 'ja': text = { media: 'メディア', image: '画像', video: '埋め込み動画' }; break; // Japanese
    case 'kn': text = { media: 'ಮಾಧ್ಯಮ', image: 'ಚಿತ್ರ', video: 'ಎಂಬೆಡ್ ಮಾಡಿದ ವೀಡಿಯೋ' }; break; // Kannada*/
    case 'ko': text = { media: '미디어', image: '이미지', video: '담아간 동영상' }; break; // Korean
    case 'ms': text = { media: 'Media', image: 'Imej', video: 'Video terbenam' }; break; // Malay
    case 'mr': text = { media: 'मिडिया', image: 'प्रतिमा', video: 'एम्बेडेड व्हिडिओ' }; break; // Marathi
    case 'nb': text = { media: 'Medier', image: 'Bilde', video: 'Innebygd video' }; break; // Norwegian
    case 'fa': text = { media: 'رسانه تصویری', image: 'تصویر', video: 'ویدئوی جاسازی‌شده' }; break; // Persian
    case 'pl': text = { media: 'Multimedia', image: 'Zdjęcie', video: 'Osadzony film' }; break; // Polish
    case 'pt': text = { media: 'Mídia', image: 'Imagem', video: 'Vídeo inserido' }; break; // Portuguese
    case 'ro': text = { media: 'Conținut media', image: 'Imagine', video: 'Videoclip încorporat' }; break; // Romanian
    case 'ru': text = { media: 'Медиа', image: 'Изображение', video: 'Встроенное видео' }; break; // Russian
    case 'sr': text = { media: 'Медији', image: 'Слика', video: 'Уграђени видео' }; break; // Serbian
    case 'zh': text = { media: '媒体', image: '图像', video: '嵌入式视频' }; break; // Simplified Chinese
    case 'sk': text = { media: 'Médiá', image: 'Obrázok', video: 'Vložené video' }; break; // Slovak
    case 'es': text = { media: 'Fotos y videos', image: 'Imagen', video: 'Video insertado' }; break; // Spanish
    case 'sv': text = { media: 'Medier', image: 'Bild', video: 'Inbäddad video' }; break; // Swedish
    case 'ta': text = { media: 'ஊடகம்', image: 'படம்', video: 'உட்பொதிக்கப்பட்ட வீடியோ' }; break; // Tamil
    case 'th': text = { media: 'สื่อ', image: 'รูปภาพ', video: 'วิดีโอที่ฝังไว้' }; break; // Thai
    case 'zh-Hant': text = { media: '媒體', image: '圖片', video: '嵌入的影片' }; break; // Traditional Chinese
    case 'tr': text = { media: 'Medya', image: 'Resim', video: 'Yerleştirilmiş video' }; break; // Turkish
    case 'uk': text = { media: 'Медіафайли', image: 'Зображення', video: 'Вбудоване відео' }; break; // Ukrainian
    case 'ur': text = { media: 'میڈیا', image: 'تصویر', video: 'ایمبیڈ کردہ ویڈیو' }; break; // Urdu
    case 'vi': text = { media: 'Phương tiện', image: 'Hình ảnh', video: 'Video được nhúng' }; break; // Vietnamese
    default: text = { media: 'Media', image: 'Image', video: 'Embedded video' }; // English
}
// Checking the tweet forms
const checkForm = function(e) {
    // Finding the tweet button and proceding when it is found
    let b = document.querySelector('div[data-testid="tweetButtonInline"], div[data-testid="tweetButton"]');
    if (b != null) {
        // We start assuming a reminder isn't needed, and check if the button is enabled
        let remind = false;
        // We move through each attachment that's added to the tweet
        let attachments = document.querySelectorAll('div[data-testid="attachments"] div[role="group"]');
        attachments.forEach(function(i) {
            // Check if the alt text is set to the default "Media" text
            if (i.getAttribute('aria-label') == text.media) {
                // Check if the media is not a video
                if (i.querySelector('video') == null) {
                    // It is an image with default alt text, so we need to remind the user to add it
                    remind = true;
                }
            }
        });
        // Checking if the media added is a GIF with predefined alt text to show this text in the preview
        if (!remind && attachments.length == 1) {
            let l = document.querySelector('div[data-testid="attachments"] span[data-testid="altTextLabel"]');
            if (l != null) {
                // Check if an alt text is set and differs from the text in the alt text preview 
                if (l.innerText == l.parentElement.getAttribute('aria-label') && l.innerText != attachments[0].getAttribute('aria-label')) {
                    // Grabbing the little icon shown before the alt text preview
                    let icon = l.querySelector('svg');
                    // Replace the text with the prefilled alt text
                    l.innerText = l.innerText.replace(l.parentElement.getAttribute('aria-label'), 'Prefilled alt: '+attachments[0].getAttribute('aria-label'));
                    // Add the icon before the text
                    if (icon != null) {
                        l.prepend(icon);
                    }
                }
            }
        }
        // Finding all tweet buttons, also in case we are checking a thread
        document.querySelectorAll('div[data-testid="tweetButtonInline"], div[data-testid="tweetButton"]').forEach(function(btn) {
            // Getting the right spot for our label
            let t = btn.querySelector('span > span'); 
            if (remind) {
                // Add the label
                cla(t, 'mis');
                // If the user chose to disable buttons, we do that here
                if (disableButton) {
                    btn.setAttribute('aria-disabled', true);
                    btn.removeAttribute('tabindex');
                    btn.classList.add('r-icoktb');
                    btn.style.pointerEvents = 'none';
                }
            } else {
                // Clear the label from the button and enable if needed 
                if (t.classList.contains('tw-alt-mis')) {
                    clr(t, 'mis');
                    btn.removeAttribute('aria-disabled');
                    btn.setAttribute('tabindex', 0);
                    btn.classList.remove('r-icoktb');
                    btn.style.pointerEvents = 'auto';
                }
            }
        });
    }
};
// Checking tweets on the timeline for alt text
const check = function(e) {
    // Check if the user doesn't have labels and alt text disabled 
    if (!hideLabels || !hideAlt) {
        // Finding every image container we haven't checked yet
        e.querySelectorAll('section.css-1dbjc4n div[data-testid="tweetPhoto"]:not(.tw-alt-chk)').forEach(function(p) {
            // Finding the alt text in the aria-label attribute of the element
            let l = p.getAttribute('aria-label');
            // Check if the alt text is empty or the default "Image" text
            if (l != text.image && l != "" && l != null) {
                // We have alt text! Getting the container and adding the caption
                let c = captionContainer(target(p));
                caption(c,l);
            } else {
                // Adding the "No alt" label unless the user disabled it
                if (!hideLabels) {
                    cla(p.closest('a'), 'mis');
                }
            }
            // Adding a class to indicate we have checked this one
            cla(p, 'chk');
        });
        // Checking previews (for GIFs when autoplay is off)
        e.querySelectorAll('section.css-1dbjc4n div[data-testid="previewInterstitial"]:not(.tw-alt-chk)').forEach(function(g) {
            // Finding the alt text in the aria-label attribute of the element
            let l = g.getAttribute('aria-label');
            // Check if the alt text is empty or the default "Embedded video" text
            if (l != text.video && l != "") {
                // We have alt text! Getting the container and adding the caption
                let c = captionContainer(target(g));
                // Double check to see if alt text has already been added to avoid a duplicate
                if (c.querySelector('div.tw-alt-txt') == null) {
                    caption(c,'GIF: '+l);
                }
            } else {
                // Check the text on the play button to see if it is a GIF, and not a video, before we add our label
                let b = g.querySelector('div[data-testid="playButton"]');
                if (b != null && b.getAttribute('aria-label').includes('GIF')) {
                    // Adding the "No alt" label unless the user disabled it
                    if (!hideLabels) {
                        cla(g, 'mis');
                    }
                }
            }
            // Adding a class to indicate we have checked this one
            cla(g, 'chk');
        });
        // Checking videos (for GIFs when autoplay is on)
        e.querySelectorAll('section.css-1dbjc4n div[data-testid="videoPlayer"]:not(.tw-alt-chk) video').forEach(function(g) {
            // Finding the alt text in the aria-label attribute of the element
            let con = g.closest('div[data-testid="videoPlayer"]');
            let l = g.getAttribute('aria-label');
            // Check if the alt text is empty or the default "Embedded video" text
            if (l != text.video && l != "") {
                // We have alt text! Getting the container and adding the caption
                let c = captionContainer(target(g));
                if (c.querySelector('div.tw-alt-txt') == null) {
                    caption(c,'GIF: '+l);
                }
            } else {
                // Checking the preload attribute to see if it is a GIF, and not a video, before we add our label
                let b = g.getAttribute('preload');
                if (b == 'auto') {
                    // Adding the "No alt" label unless the user disabled it
                    if (!hideLabels) {
                        cla(con, 'mis');
                    }
                }
            }
            // Adding a class to indicate we have checked this one
            cla(con, 'chk');
        });
    }    
    // Remove the tweet from the timeline if the account uses an NFT profile pic (if enabled by the user)
    if (hideNFT) {
        e.querySelectorAll('section.css-1dbjc4n article[data-testid="tweet"] div[style*="hex-hw-shapeclip"]').forEach(function(n) {
            n.closest('article[data-testid="tweet"]').remove();
        });
    }
    // Check the inline form unless the user disabled this feature
    if (!noCheck) {
        checkForm();
    }
};
// Function to find the right element to contain the alt text
const target = function(e) {
    let t = e.closest('div[data-testid="tweet"] > :last-child, div[role="link"] > :last-child');
    if (t == null) {        
        t = e.closest('div[data-testid="tweet"], div[role="link"]');
        if (t != null) {
            t = t.querySelector('div.tw-alt-container');
        }
    }
    if (t == null) {
        t = e.closest('.r-1phboty, .r-18bvks7').closest('div.css-1dbjc4n');
    }
    return t;
}
// Initialising the observers
const init = function() {
    var o = false; 
    let tw = document.querySelector('main');
    if (tw != null && o != true) {
        // First running our check on tweets already visible
        check(tw);
        // Start observing the tweets
        let otw = new MutationObserver(callback);
        otw.observe(tw, cfg);
        if (!noCheck) {
            // Find and start observing the composer forms (if the option is enabled)
            let frm = document.querySelector('div#layers');
            if (frm != null) {
                let otf = new MutationObserver(checkForm);
                otf.observe(frm, cfg);
            }
            let frm2 = document.querySelector('header[role="banner"]');
            if (frm2 != null) {
                let otf = new MutationObserver(checkForm);
                otf.observe(frm2, cfg);
            }
        }
        o = true;
        if (prettyScroll) {
            // Add a class to the html element to make use of our scrollbar styling (if the option is enabled)
            document.documentElement.classList.add('prettyscroll');
        }
    }
    if (o != true) {
        // We aren't observing yet, retry in a second
        // We only retry if we have less than 5 failed attempts 
        tries++;
        if (tries < 5) { setTimeout(init, 1000); }
    }
};
// Start our first try at initialising
init();

TweetDeck.js: functions specific to TweetDeck

// Setting a global variable to store the default "Add description" label in Tweetdeck
// This way we don't need to keep track of languages
var tdnoalt = '';
// Setting up the callback for our mutation observer, when content of a column changes it gets re-checked
const callbackTD = function(ml, o) {
    ml.forEach(function(m) { checkTD(m.target); });
};
// Checking the tweet form
const checkTDForm = function(e) {
    // Finding the tweet button and proceding when it is found
    let b = document.querySelector('div[data-drawer="compose"] button.js-send-button');
    if (b != null) {
        // We start assuming a reminder isn't needed, and check if the button is enabled
        let remind = false;
        // We move through each image that's added to the tweet
        document.querySelectorAll('div[data-drawer="compose"] div.js-add-image-description').forEach(function(i) {
            // Getting the content of the "Add description" label, and storing it for later if we haven't already
            let l = i.innerHTML;
            if (tdnoalt == '') {
                tdnoalt = l;
            }
            if (l == tdnoalt) {
                // The alt is set to the standard "Add description" text, we will remind the user
                remind = true;
            }
        });
        if (remind) {
            // Add the label
            cla(b, 'mis');
            // If the user chose to disable buttons, we do that here
            if (disableButton) {
                b.classList.add('is-disabled');
                b.style.pointerEvents = 'none';
            }
        } else {
            // Clear the label from the button and enable if needed 
            if (b.classList.contains('tw-alt-mis')) {
                clr(b, 'mis');
                b.classList.remove('is-disabled');
                b.style.pointerEvents = 'auto';
            }
        }
    }
};
// Checking tweets on the timeline for alt text
const checkTD = function(e) {
    // Finding every image container we haven't checked yet
    e.querySelectorAll('a.js-media-image-link:not(.tw-alt-chk)').forEach(function(p) {
        if (p.querySelectorAll('div').length == 0) {
            // Finding the alt text in the aria-label attribute of the element
            let l = p.getAttribute('title');
            let i = p.querySelector('img');
            if (l == "" && i != null) {
                l = i.getAttribute('alt');
            }
            // Check if the alt text is empty or the default "Image"
            if (l != "Image" && l != "" && l != null) {
                // We have alt text! Getting the container and adding the caption
                let c = captionContainer(p.closest('.js-media').parentElement);
                caption(c,l);
            } else {
                // Adding the "No alt" label unless the user disabled it
                if (!hideLabels) {
                    cla(p, 'mis');
                }
            }
        }
        // Adding a class to indicate we have checked this one
        cla(p, 'chk');
    });
};
// Initialising the observers
const initTD = function() {
    var o = false;
    if (document.querySelector('section.js-column') != null) {
        if (!hideLabels || !hideAlt) {
            // Unless the user disabled both the "No alt" label and alt text display, we find and observer all TweetDeck columns
            let otd = new MutationObserver(callbackTD);
            document.querySelectorAll('section.js-column').forEach(function(t) {
                // First running our check on tweets already visible
                checkTD(t);
                // Start observing the tweets
                otd.observe(t, cfg);
            });
        }
        if (!noCheck) {
            // Unless the user disabled the feature, we find and start observing the tweet form
            let tdc = document.querySelector('div[data-drawer="compose"]');
            if (tdc != null) {
                let otdf = new MutationObserver(checkTDForm);
                otdf.observe(tdc, cfg);
            }
        }
        o = true;
    }
    // We aren't observing yet, retry in a second
    // We only retry if we have less than 5 failed attempts 
    if (o != true) {
        tries++;
        if (tries < 5) { setTimeout(initTD, 1000); }
    }
};
// Start our first try at initialising
init();
                

Alt-or-not.css: styles

// Style for the red "No alt" label
.tw-alt-mis::after { 
    display: inline-block;
    background-color: #800;
    color: #FFF; 
    font-family: sans-serif;
    font-size: 1rem;
    padding: 0.25rem 0.5rem;
    border-radius: 0.5rem;
    content: "No alt";
    font-weight: bold;
    position: absolute;
    top: 0.5rem;
    left: 0.5rem;
    box-shadow: 0 0 2px 0 #000;
}
// Positioning adjustments for the "No alt" label in buttons
div[data-testid="tweetButtonInline"] .tw-alt-mis::after, 
div[data-testid="tweetButton"] .tw-alt-mis::after, 
button.js-send-button.tw-alt-mis::after {
    position: relative;
    top: 0;
    left: 0;
    margin-left: 0.5rem;
}
// The element containing alt texts gets minimal styling to correctly match the width of the image 
.tw-alt-container { 
    min-width: 100%;
    max-width: fit-content;
    margin: 0;
    padding: 0.5rem 0 0 0;
}
// In case the alt text is displayed in a quote tweet it gets padding all around and width is adjusted to fit
.r-rs99b7 .tw-alt-container {
    min-width: calc(100% - 1rem);
    padding: 0.5rem;
}
// Style of the alt text elements
.tw-alt-txt {
    display: block;
    box-sizing: border-box;
    background-color: #202020;
    color: #FFF;
    font-family: sans-serif;
    font-size: 1rem;
    line-height: 1.3rem;
    padding: 0.75rem;
    margin: 0;
    white-space: pre-wrap;
    border: 1px solid #505050;
}
// Border adjustments for the first alt text of a tweet
.tw-alt-txt:first-of-type {
    border-top-left-radius: 0.5rem;
    border-top-right-radius: 0.5rem;
    border-bottom-width: 0;
}
// Border adjustments for the last alt text of a tweet (which can also be the first)
.tw-alt-txt:last-of-type {
    border-bottom-left-radius: 0.5rem;
    border-bottom-right-radius: 0.5rem;
    border-bottom-width: 1px;
}
// Hiding the alt text
.tw-alt-hidden .tw-alt-txt {
    display: none;
}
// The styling for the "Show alt" button
.tw-alt-container button.tw-alt-toggle,
.tw-alt-container button.tw-alt-toggle:active,
.tw-alt-container button.tw-alt-toggle:hover,
.tw-alt-container button.tw-alt-toggle:focus {
    display: none; 
    background-color: #17bf63;
    color: #FFF;
    font-family: sans-serif;
    font-size: 1rem;
    font-weight: bold;
    line-height: 1.3rem;
    padding: 0.5rem 0.75rem;
    border-radius: 2.3rem;
    border: 0;
    margin: 0;
    cursor: pointer;
}
// Giving the button a subtle hover effect in line with Twitter styles
.tw-alt-container button.tw-alt-toggle:hover {
    background-color: #15ac59;
}
// Making the button visible when the alt text is hidden
.tw-alt-hidden button.tw-alt-toggle {
    display: inline-block !important;
}
// Inverting the alt text colors for the light mode
.tw-alt-light .tw-alt-txt {
    background-color: #FFF;
    color: #202020;
}
// A few styles for our scrollbars
.prettyscroll, .prettyscroll * { 
    scrollbar-width: thin; 
    scrollbar-color: hsl(205, 25%, 75%) transparent;
}
.prettyscroll::-webkit-scrollbar, 
.prettyscroll *::-webkit-scrollbar { 
    width: 10px;
}
.prettyscroll::-webkit-scrollbar-thumb, 
.prettyscroll *::-webkit-scrollbar-thumb { 
    min-height: 50px; 
    border-radius: 5px; 
    background-color: hsl(205, 25%, 75%); 
}
.prettyscroll section[role="region"] > div.r-19u6a5r { 
    margin: 0 3px 0 6px; box-shadow: 0 0 3px hsl(0deg,0%,0%,50%); 
}