How to write a Chrome extension to prevent bias in hiring

Kim T
Kim T
Jun 20, 2019 · 4 min read
Image for post
Image for post
Antibias Chrome Extension

Every person is biased, affecting the decisions they make everyday. People with a position of power can have their biases magnified resulting in huge effects across society. Technology is often considered a force for good, but can also magnify cognitive biases.

LinkedIn makes it really easy to find and review potential candidates for job positions. Although more accurate/trustworthy information helps us hire the right people, it can also help reinforce our biases. Some of my colleagues wondered whether actually removing or hiding some information would equal the playing field.

Image for post
Image for post

I was tasked with writing the Chrome extension, and this is a quick explainer of how it works.

Every Chrome extension has a manifest.json which holds the configuration settings. We need to give permissions for the extension (which runs in the toolbar) to communicate with page tabs and the page local storage. We also want to allow it to run on only linkedin.com urls:

“permissions”: [
“tabs”,
“storage”,
http://*.linkedin.com/*",
https://*.linkedin.com/*"
],

Next we add a background script which runs behind-the-scenes to control the storage and toolbar:

"background": {
"persistent": false,
"scripts": ["js/background.js"]
},

In background.js we listen to the chrome extension installed event and then trigger a function to update the toolbar icon:

chrome.runtime.onInstalled.addListener(function (details) {
toggleState(enabled);
});
function toggleState(state) {
var icon = chrome.browserAction;
if (state === true) {
icon.setBadgeText({text: 'on'});
icon.setBadgeBackgroundColor({color: '#0b8043'});
} else {
icon.setBadgeText({text: 'off'});
icon.setBadgeBackgroundColor({color: '#616161'});
}
}

In our package.json we also configure the popup window which shows when clicking the toolbar icon:

"browser_action": {
"default_popup": "html/popup.html",
"default_icon": {
"16": "images/16x16.png",
"32": "images/32x32.png",
"48": "images/48x48.png",
"128": "images/128x128.png"
},
"matches": ["https://*.linkedin.com/*"]
},

Next we add the content script which will run on the page when on a linkedin page:

"content_scripts": [
{
"persistent": false,
"matches": ["https://*.linkedin.com/*"],
"web_accessible_resources": ["css/*", "js/*", "images/*"],
"css" : ["css/content.css"],
"js" : ["js/content.js"],
"run_at": "document_start"
}
],

The content script loads a css file to hide/replace any images of people on the page. I decided to go with manual css selectors as it would place an uncessary load on the browser to intelligently detect which images are of people:

Hide text (except on hover) using an svg squiggle:

.antibias .profile .name a:not(:hover) {
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzMiIGhlaWdodD0iNTYiIHZpZXdCb3g9IjAgMCA3MyA1NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwKSI+CjxyZWN0IHdpZHRoPSI3MyIgaGVpZ2h0PSI1NiIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgNy42NjYzQzYuNzA0ODIgNy41MTk3MyAxMy40Nzc3IDEwLjYwNTggMTcuNjI0MiAxNi45Njc2TDI4LjU0MTIgMzMuNzE2OUMzMi4yODkxIDM5LjQ2NyA0MC43MTA3IDM5LjQ2NyA0NC40NTg2IDMzLjcxNjlMNTUuMzc1NiAxNi45Njc2QzU5LjUyMjEgMTAuNjA1NyA2Ni4yOTUxIDcuNTE5NjggNzMgNy42NjYzMVYxOC42NzJDNjkuODE0OCAxOC41MjI2IDY2LjU2MjMgMTkuOTQ5NCA2NC41OTA5IDIyLjk3NEw1My42NzQgMzkuNzIzM0M0NS41ODY1IDUyLjEzMTUgMjcuNDEzNCA1Mi4xMzE1IDE5LjMyNTggMzkuNzIzM0w4LjQwODg4IDIyLjk3NEM2LjQzNzUxIDE5Ljk0OTQgMy4xODUxMiAxOC41MjI2IDAgMTguNjcyVjcuNjY2M1oiIGZpbGw9ImJsYWNrIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDAiPgo8cmVjdCB3aWR0aD0iNzMiIGhlaWdodD0iNTYiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4=');
background-position: 0px center;
background-repeat: repeat-x;
background-size: 26px;
color: transparent !important;
display: inline-block;
}

Hide image (except on hover) using a png background:

.antibias .profile .photo:not(:hover) {
background-repeat: repeat !important;
background-size: 800px;
content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=');
background-image: url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEAS') !important;
}

Unfortunately there were a few compromises to get this working:

  1. Had to inline images into css as LinkedIn CSP security policy was blocking them loading via url
  2. Needed to use negative css :not logic to overwrite the values when static, but use the original values when hovering
  3. Had to replace inner content with a blank image, and always use background image to get the tiling effect we wanted.

The final file content.js runs some more advanced functionality. It listens to various window events and then updates the page content:

window.addEventListener('DOMContentLoaded', function() {
updatePage();
});
window.addEventListener('load', function() {
updatePage();
});
window.onpopstate = function() {
window.setTimeout(function () {
updatePage();
}, 200);
};
window.addEventListener('scroll', function(e) {
scrolling += 1;
if (scrolling === 10) {
window.requestAnimationFrame(function() {
updatePage();
});
scrolling = 0;
}
});

Update page loops through all the css selectors and updates their background position to be unique based on their image src url:

function updatePage() {
document.body.classList.add('antibias');
selectors.forEach((selector) => {
var elements = document.querySelectorAll(selector);
[].forEach.call(elements, function(element) {
updateElement(element);
});
});
}
function updateElement(el) {
var val = getImageVal(el);
if (val && val !== '') {
if (!el.hasAttribute('data-bias')) {
el.setAttribute('data-bias', val);
}
var hash = hashCode(val);
el.style.backgroundPosition = `${hash.substring(0, 3)}px ${hash.substring(1, 4)}px`;
el.style.filter = `hue-rotate(${hash.substring(2, 5)}deg)`;
}
}
function getImageVal(el) {
return el.hasAttribute('src') ? el.getAttribute('src') : el.style.backgroundImage.slice(5, -2);
}

That’s a quick walkthrough of how the extension works, the actual code has more communication between tabs and code to store your preference in localstorage. As a result of this we were honored by Fast Company for a world-changing idea!

Image for post
Image for post
Fast Company World-Changing Ideas: Honorable Mention 2019

You can install the extension here:
https://chrome.google.com/webstore/detail/antibias/gfeilphhbcfaekkffklahpndiidiloln

And view the full source code here:
https://github.com/Beyond-Digital/antibias-chrome-extension

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store