Every remote worker knows this ritual: you're deep in a doc, the dog starts barking, and you frantically Ctrl+Tab through 30 tabs to find the Meet window and hit mute. By the time you get there, your entire team has heard everything.
I got tired of it, so I built MeetMute — a Chrome extension that puts a floating mute button on every tab during a Google Meet call. Click it from anywhere, or press Ctrl+Space, and you're muted. No tab switching.
What it does
- Floating button on every tab — A small circle sits in the corner. Red = muted, green = live. Drag it to any screen edge; it remembers where you put it.
- Keyboard shortcut (Ctrl+Space) — Mute mid-sentence in a doc without lifting your hands off the keyboard.
- Push-to-talk — Hold the button or shortcut to unmute, release to auto-mute. Walkie-talkie style for noisy environments.
- Toolbar icon — Green, red, or gray so you know your mic state at a glance.
- Site exclusions — Right-click the float to hide it on specific sites.
The tech stack
I used WXT (a framework for building web extensions) with TypeScript and Chrome Manifest V3. Here's the architecture:
background.ts (service worker) — central hub, manages state
meet.content.ts — injected into Meet tabs, detects call/mute state
float.content.ts — injected into all tabs, renders the floating button
popup/ — toolbar popup UI
options/ — settings page
WXT made the developer experience significantly better than raw MV3. Hot reloading, auto-generated manifest, TypeScript support out of the box — it let me focus on the actual extension logic instead of fighting with build tooling.
Interesting problems I solved
Detecting mute state without an API
Google Meet doesn't expose a public API for microphone state. The content script on the Meet tab watches the DOM — specifically the mute button's data-is-muted attribute and aria labels. It's fragile (Google can change it anytime), but it works reliably and I haven't had to patch it since launch.
Floating button on every tab with Shadow DOM
The float button is injected as a custom element (<meetmute-float>) using Shadow DOM so it's completely CSS-isolated from the host page. No matter what styles a website uses, the button looks the same. I inline the CSS at build time to avoid FOUC.
Service worker keep-alive
MV3 service workers get killed after 30 seconds of inactivity. During an active call, I use chrome.alarms to keep it alive so mute state updates don't get dropped. When the call ends, the alarm stops and the worker is free to sleep.
Push-to-talk with pointer events
PTT uses pointerdown/pointerup on the float button and keydown/keyup via chrome.commands for the keyboard shortcut. The tricky part was handling the intent delay — a quick click when PTT is enabled should still toggle (not start a PTT session), so there's a short delay before entering "hold" mode.
Handling extension reloads gracefully
When the extension updates or reloads, old content scripts become orphaned but their DOM elements stick around. On injection, the float script removes any stale <meetmute-float> elements before creating a new one — otherwise you'd get duplicate buttons stacking up.
Things I'd do differently
Start with optional_host_permissions from day one. I initially declared *://*/* as a required host permission, which triggered Chrome Web Store's "Broad Host Permissions" review. Switching to optional permissions with a runtime prompt was better UX anyway — users grant access when they first see the float prompt, and it's less scary than a wall of permissions at install.
Test the DOM selectors against Meet's UI updates. This is the biggest maintenance risk. Google Meet's DOM changes without warning. I'd set up a periodic smoke test if I were doing this again.
Privacy
No data leaves your browser. No analytics, no tracking, no accounts, no external servers. Your preferences (button position, settings) are stored in chrome.storage.local and that's it. The source code is open.
Try it out
Chrome Web Store: MeetMute - Mute Control for Google Meet
If you work remotely and use Google Meet, give it a shot. It's one of those tools where the value is hard to explain until you're mid-call reaching for the mute button and it's just... already there.
Feedback welcome — I'm still actively developing it. Happy to answer questions about WXT, MV3 development, or anything else in the comments.

Top comments (0)