This project enhances the YouTube viewing experience through a modular, highly customizable browser extension. It is built around isolated feature modules that integrate with the YouTube player lifecycle.
Contributions are welcome from developers interested in:
- Frontend architecture
- React + TypeScript
- Complex state and lifecycle management
Non-developers can contribute too — see Internationalization for translations.
YouTube Enhancer supports multiple languages to provide a more inclusive experience for users around the world. We use Crowdin for managing translations.
We welcome contributions to improve translations and make the extension accessible to a wider audience:
- Visit our Crowdin project
- Select your language and start translating
- If your language is not listed, feel free to request its addition
All contributions must be based on the dev branch — not main.
devis the active development branch. All new features and bug fixes land here first.mainis reserved for stable releases. It is updated only whendevis merged into it as part of cutting a new release.
feature/your-feature ──► dev ──► main (release only)
What this means for you:
- Fork/clone the repo and check out
dev:git checkout dev - Create your working branch from
dev:git checkout -b feature/your-feature-name - Open your PR targeting
dev, nevermain
PRs opened against main will be asked to retarget dev.
-
Check out
devand branch from it:git checkout dev && git checkout -b feature/your-feature-name -
Install dependencies:
npm install -
Start dev server:
npm run dev -
Create a feature in
src/features/MyNewFeature/:index.metadata.ts→ schema + settings UI + i18nindex.ts→ lifecycle + logic
-
Add translations in
public/locales/ -
Manually test on multiple YouTube pages
-
Submit a PR targeting
dev, using Conventional Commits
- Prerequisites: Node.js and npm/yarn installed
- Install dependencies:
npm install - Run dev server:
npm run dev(hot reload enabled) - Testing: Automated testing is planned. For now, all testing is manual verification. Your feature must be validated across multiple YouTube states (watch, live, navigation, etc.).
Before new code gets merged into the repository, automated lint tests verify the format of the code.
It is recommended to test your code before committing by running:
- Lint check:
npm run lint - Fix lint errors:
npm run lint:fix
You won't need to do this if you use a supported editor, as the process is automated.
Extension Core
│
├── Feature Registry (auto-discovers features)
│
├── Content Script (runs on YouTube pages)
│
├── Popup/UI (options page)
│
└── Feature Modules (src/features/*)
│
├── index.metadata.ts (config + schema + UI definition)
└── index.ts (lifecycle + behavior)
Every feature must follow this contract to ensure consistency and stability.
src/features/MyNewFeature/
index.metadata.ts→ configuration, schema, settings UIindex.ts→ feature logic and lifecycle
- Open
src/types/index.ts - Add your feature:
myNewFeature: {
enabled: boolean;
}Why: Ensures the feature is globally recognized and configurable.
- Create
index.metadata.ts - Use
createFeatureMetadata()
defaultsmust includeenabled: falseidmust match the feature folder nameschemaInputmust validate all settingssettingsdefines UI (required)
All user-facing text must use the t function.
Why: Prevents hardcoded UI and enables localization.
import { z } from "zod/v4-mini";
import { createFeatureMetadata } from "@/src/features/_registry/createFeatureMetadata";
export const metadata = createFeatureMetadata({
defaults: {
enabled: false,
someSetting: "defaultValue",
anotherSetting: 5
},
id: "myNewFeature",
schemaInput: {
enabled: z.boolean(),
someSetting: z.string(),
anotherSetting: z.number().min(1).max(100)
},
sectionTitle: (t) => t((tr) => tr.settings.sections.myNewFeature.title),
settings: [
{
section: "myNewFeature",
type: "group",
children: [
{
component: "checkbox",
id: "enabled",
label: (t) => t((tr) => tr.settings.sections.myNewFeature.enable.label),
title: (t) => t((tr) => tr.settings.sections.myNewFeature.enable.title)
},
{
component: "input",
id: "someSetting",
label: (t) => t((tr) => tr.settings.sections.myNewFeature.settings.someSetting.label),
title: (t) => t((tr) => tr.settings.sections.myNewFeature.settings.someSetting.title)
},
{
component: "number",
id: "anotherSetting",
label: (t) => t((tr) => tr.settings.sections.myNewFeature.settings.anotherSetting.label),
title: (t) => t((tr) => tr.settings.sections.myNewFeature.settings.anotherSetting.title),
min: 1,
max: 100,
step: 1
}
]
}
]📌 Add translations in:
public/locales/en-US.json
- Create
index.ts - Import the metadata from "./index.metadata"
- Use
createFeature()wrapper, spreading the metadata and adding lifecycle methods - Export default:
import { createFeature } from "@/src/features/_registry/createFeature";
import { metadata } from "./index.metadata";
export default createFeature({
...metadata,
// Add lifecycle methods here (onEnable, onDisable, onConfigChange, etc.)
onEnable: (config) => {
// Enable logic
},
onDisable: (config) => {
// Disable/cleanup logic
},
onConfigChange: (config) => {
// Handle config updates
}
});| Method | When it runs | Purpose | Notes |
|---|---|---|---|
onInit |
Once per lifecycle (extension load) | Initial setup | Use for observers or lightweight initialization |
onEnable |
When feature is enabled | Activate behavior | Must be idempotent |
onDisable |
When feature is disabled | Cleanup | Remove DOM, listeners, timers, observers |
onConfigChange |
When settings change | React to config updates | Prefer incremental updates |
onNavigate |
YouTube SPA navigation | Re-sync behavior | DOM resets on navigation |
- Missing i18n (hardcoded strings)
- Forgetting
enabled: falsein defaults - Missing cleanup in
onDisable - Direct state mutation instead of
stateAPI - Ignoring SPA navigation (
onNavigate) - Hardcoded UI placement instead of config-based rendering
- Forgetting to fully define
settingsarray contents
- Feature initializes correctly
- Enable/disable works
- Settings apply immediately
- Watch page
- Live streams
- Search results
- Channel pages
- Works across SPA navigation
- No duplicate DOM elements
- Buttons render correctly
- No layout glitches
- Interactions work as expected
- Persists correctly (if enabled)
- Survives reloads (if configured)
- No noticeable lag
- No excessive re-renders
- Define in
FeatureState - Use
FeatureBaseWithState<K> - Access via
stateAPI
Never mutate state directly.
Interaction between the content script, popups, or background services must use the specific functions exported from src/utils/message. Do not assume a general messaging bus.
-
Content
$\to$ Content/Background: UsesendContentMessage(type, action, data?)orsendContentToBackgroundMessage(type, data?)for directed communication. -
Extension
$\to$ Content: UsesendExtensionMessage(type, action, data?)orsendExtensionOnlyMessage(type, data)when communicating from the extension side. -
Listening: Use
waitForSpecificMessageto reliably listen for expected responses or events from other parts of the system.
Do not implement custom messaging systems.
Buttons are defined in the buttons array passed to createFeature(). Each button object must include:
name: A unique string identifier for the button.add: An async function that receives the feature's configuration. It should calladdFeatureButton(id, placement, label, icon, onClick, isToggle?)to add the button and set up any necessary event listeners.remove: An async function that receives the button's placement. It should callremoveFeatureButton(id, placement)to remove the button and clean up listeners.shouldRender(optional): A function that receives the feature's configuration and returns a boolean indicating whether the button should be rendered.- Critical: Always clean up event listeners and remove buttons in the
removefunction to prevent memory leaks. - Example Pattern:
buttons: [
{
name: "myFeatureButton",
add: async (config) => {
await addFeatureButton(
"myFeatureButton", // Button ID
config.button.placement, // Placement from config
window.i18nextInstance.t((t) => t.pages.content.features.myFeatureButton.button.label), // Initial label
getFeatureIcon("myFeatureButton", config.button.placement), // Icon function
(checked) => {
// Handle click/toggle
},
true // Optional: set to true for toggle buttons
);
// Add event listeners here if needed
},
remove: async (placement) => {
await removeFeatureButton("myFeatureButton", placement);
// Remove event listeners here
},
shouldRender: (config) => {
// Return true/false based on config
return config.mode === "global";
}
}
];Use helpers like:
createStyledElement(...),conditionalStyles(...),createSVGElement(...),createTooltip(...),modifyElementClassList(...),modifyElementsClassList(...),waitForElement(...),waitForAllElements(...)
Do not manually manipulate the DOM where utilities exist.
- Features must be fully encapsulated
- Shared logic goes in
src/utils/ - Always clean up in
onDisable - Follow existing patterns (don’t introduce new ones unnecessarily)
- Avoid heavy logic in navigation/event loops
- DRY principle
- Rule of three
- Single source of truth
When creating Bug Report issues, follow the template and explain the issue in a clear and straightforward manner.
- Targets the
devbranch (notmain) - Meaningful, descriptive title — Conventional Commits format is preferred for PR titles, though not strictly enforced
- Description briefly explains the goal of the PR and the changes it brings to the codebase
- Follows Feature Contract
- Fully localized (no hardcoded strings)
- No console errors/warnings
- Manual testing completed
- No memory leaks
- Consistent with project structure
Follow a strict workflow cycle:
- Branching: Always branch from
dev— never frommain.devholds the latest features and fixes;mainis only updated whendevis merged in for a release. Run:git checkout dev && git checkout -b feature/your-feature-name. - Development Cycle: Implement small, focused changes. When designing logic, adopt the Red-Green-Refactor methodology.
- Modification: After implementing logic, first explore existing subdirectories in
src/utils/(e.g., dom, format, logging, math, messaging, plugins, style, color, deep-dark-theme) to see if your utility fits into an existing category. If so, place it there. Otherwise, updatesrc/utils/utilities.tsfor general-purpose utilities. - Committing: Commit only when the feature is functionally complete and manually verified.
- Conventional Commits: All commits must follow the Conventional Commits specification (
type(scope): message), with messages that meaningfully describe the change itself. Because our release CI/CD workflow is automated, we rely on this convention for semantic versioning (why).- feat: For new features.
- fix: For bug fixes.
- refactor: For code restructuring without adding features or fixing bugs.
- chore: For build scripts or tooling changes.
- Pull Request: Open your PR against the
devbranch. PRs targetingmainwill be redirected todev, asmainis reserved for release merges.