Our goals:
- create extension logic
- store settings in the browser Local Storage
- autoload and activate extensions only on the Netflix page
- create popup menu
- create forms with subtitles options
Requirements:
- basic knowledge of HTML, CSS and JavaScript
Netflix by its API sends every subtitle sentence separately. It uses CSS styles for styling subtitles. With access to the page DOM we can manipulate those received styles with Chrome extension.
The manifest
Firstly, we have to create the manifest file called manifest.json. This tells the browser about the extension setup, such as the UI files, background scripts and the capabilities the extension might have.
Here is a complete manifest.
{
"name": "Netflix subtitles styler",
"version": "1.0",
"description": "Netflix subtitles styler",
"author": "twistezo",
"permissions": ["tabs", "storage", "declarativeContent", "https://*.netflix.com/"],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"page_action": {
"default_popup": "popup.html",
"default_icon": "logo.png"
},
"manifest_version": 2
}
As you see, we have some standard information, such as name, version, description, homepage_url and manifest_version.
One of the important parts of the manifest is the permissions section. This is an array with elements that our extension can access.
In our case, we need to have access to tabs to find the active tab, execute scripts and send messages between the UI and the extension. We need storage for store extension settings in the browser and declarativeContent for taking action depending on the tab content. The last element https://*.netflix.com/ allows extension access only to the netflix.com domain.
Chrome extensions have a separate logic from the UI so we need to have background.scripts, which tells the extension where it can find its logic. persistent: false means that this script will be used only if needed. page_action is the section with the UI part. We have here a simple HTML file for a popup menu and an extension's PNG logo.
Extension logic
First we have to setup runtime.onInstalled behaviours, remove any current rules (for example from older versions) and declare function to add new rules. We use Local Storage for the storage settings so we can allocate default settings after the extension is installed.
We will be using three subtitle style parameters:
vPos
– vertical position from bottom [px]
fSize
– font size [px]
fColor
– font color [HEX]
Create background.js
:
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.local.set({ vPos: 300, fSize: 24, fColor: "#FFFFFF" });
chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
chrome.declarativeContent.onPageChanged.addRules([
]);
});
});
Our rule goal is to disable the extension button on all domains other than netflix.com
. We create new rule with PageStateMatcher
conditions and declare ShowPageAction
where new rule will be assigned.
{
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostSuffix: "netflix.com" }
})
],
actions: [new chrome.declarativeContent.ShowPageAction()]
}
The next step is to add tabs.onUpdated listener, which will execute our script while loading or refreshing the active tab.
{
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostSuffix: "netflix.com" }
})
],
actions: [new chrome.declarativeContent.ShowPageAction()]
}
Firstly we check that changeInfo.status has the status complete. This means that the website on this tab is loaded. Then we get settings from Local Storage and declare which script should be run on the current tab with tabId. Finally, in callback we send the message with settings from the UI to the script.
Extension UI
To create an extension popup menu with form, we create three files: popup.html
and popup.css
with visual layers and popup.js
with logic for communicating between the menu and isolated background.js
script.
Our UI goal:

Here we have a simple HTML form with built-in validation: popup.html
:
<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600" rel="stylesheet" />
<link rel="stylesheet" href="popup.css" />
</head>
<body>
<div class="container logo"\>
NETFLIX SUBTITLES STYLER
</div>
<form id="popup-form" class="container"\>
<div class="input-info"\>Vertical position from bottom \[px\]</div>
<input class="form-control" id="vPos" type="number" value="" min="0" max="5000" />
<div class="input-info"\>Font size \[px\]</div>
<input id="fSize" type="number" value="" min="0" max="300" />
<div class="input-info"\>Font color \[HEX\]</div>
<input id="fColor" type="text" value="" pattern="^#\[0-9A-F\]{6}$" />
<button id="change" type="submit"\>Change</button>
</form>
<div class="container footer"\>
&copy; twistezo, 2019
</div>
<script src="popup.js"\></script>
</body>
</html>
Styling the popup menu is not the goal of this article, so I suggest you visit https://github.com/twistezo/netflix-subtitles-styler and copy the whole popup.css
file into your project.
UI logic - popup.js
:
const form = document.getElementById("popup-form");
const inputElements = ["vPos", "fSize", "fColor"];
chrome.storage.local.get(inputElements, data => {
inputElements.forEach(el => {
document.getElementById(el).value = data[el];
});
});
form.addEventListener("submit", event => {
event.preventDefault();
const [vPos, fSize, fColor] = [...inputElements.map(el => event.target[el].value)];
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
chrome.storage.local.set({ vPos, fSize, fColor });
chrome.tabs.executeScript(
tabs[0].id,
{
file: "script.js"
},
() => {
const error = chrome.runtime.lastError;
if (error) "Error. Tab ID: " + tab.id + ": " + JSON.stringify(error);
chrome.tabs.sendMessage(tabs[0].id, { vPos, fSize, fColor });
}
);
});
});
In above script, we load settings from Local Storage and attach them to form inputs. Then we create listener to submit
event with functions for the save settings to Local Storage and send them by message to our script. As you see, we use Local Storage in every component. The Chrome extension doesn't have its own data space so the simplest solution is to use browser local space like Local Storage. We also often use the sendMessage
function. It's caused by Chromme extensions' architecture - they have separate logic from the UI.
The script
Now it is time to create script.js
with logic for manipulating Netflix subtitles styles.
First, we create onMessage
listener for receiving messages with settings from the extension.
chrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => {
});
Then in the same file we create the function for changing proper Netflix styles to our styles in real time.
changeSubtitlesStyle = (vPos, fSize, fColor) => {
console.log("%cnetflix-subtitles-styler : observer is working... ", "color: red;");
callback = () => {
const subtitles = document.querySelector(".player-timedtext");
if (subtitles) {
subtitles.style.bottom = vPos + "px";
const firstChildContainer = subtitles.firstChild;
if (firstChildContainer) {
const firstChild = firstChildContainer.firstChild;
if (firstChild) {
firstChild.style.backgroundColor = "transparent";
}
const secondChildContainer = firstChildContainer.nextSibling;
if (secondChildContainer) {
for (const span of secondChildContainer.childNodes) {
span.style.fontSize = fSize + "px";
span.style.fontWeight = "normal";
span.style.color = fColor;
}
secondChildContainer.style.left = "0";
secondChildContainer.style.right = "0";
}
}
}
};
const observer = new MutationObserver(callback);
observer.observe(document.body, {
subtree: true,
attributes: false,
childList: true
});
};
For Netflix, every time it receives whole subtitle sentences it swaps only the subtitles part of the page DOM. So we have to use an observer function like MutationObserver
, which will be triggering our changeSubtitlesStyle
function every time when the page DOM has changed. In the callback
function we see simple manipulation of styles. The commented lines have information about where you can find proper styles.
Time to run
I assume that you do not have a developer account in Chrome Webstore. So to run this extension go to chrome://extensions/
in your Chrome, click the Load unpacked
, select folder with the extension and that's it! Then, obviously go to the Netflix page for testing it.
Conclusions
As you see, it is easy to start creating some extensions that make life easier. The most important part is to understand Google Chrome Extension divided architecture and communication between components. This subtitles styler is only a simple demo of what you can do with the Chrome Extension API.
As you see, it is easy to start creating some extensions that make life easier. The most important part is to understand the Google Chrome Extension divided architecture and communication between components. This subtitles styler is only a simple demo of what you can do with the Chrome Extension API.
Imagine enhancing your viewing experience on platforms like Netflix with the power of extensions. For instance, the popular Netflix Party allows users to watch content together in sync. An extension icon in your toolbar can provide you quick access to control features when streaming Netflix. The idea behind tools like Netflix Extended is to add more functionality directly into the Netflix app through the Netflix extension integrated within Chrome. When browsing the Netflix website, such extensions can assist users in navigating through Netflix categories more efficiently or even discovering hidden Netflix content that they might not come across otherwise.
Furthermore, imagine being able to add a custom profile picture to your Netflix profile or having more control over the subtitles and playback speed. All of this can be accessible directly from the Chrome web browser, making the overall Netflix experience more tailored to each user's preferences. Extensions not only enhance the functionalities of websites like Netflix but also elevate the overall user interaction, making it more immersive and user-friendly.
Useful links: