How to write a Chrome extension to prevent bias in hiring

Kim T
Creative Technology Concepts & Code
4 min readJun 20, 2019
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.

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!

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

--

--

Kim T
Creative Technology Concepts & Code

Creative Technologist, coder, music producer, and bike fanatic. I find creative uses for technology.