This browser extension is an independent, community-developed project and is in no way affiliated with, endorsed by, or connected to Miacademy.co or its parent company. This is an unofficial extension created for the convenience of users.
The problem
My kids use Miacademy.co for their schoolwork, and the platform itself is great. But the interface is busy — a navigation bar, a "Tutorial Videos" dropdown, a floating help button, sidebar links to challenges they haven't gotten to yet — a lot of surface area competing for a young student's attention. None of it is wrong, it's just more chrome than a kid who's trying to finish a lesson needs to see.
I didn't want to fork or theme the site itself — I just wanted a way to selectively hide pieces of the UI, per element, without touching Miacademy's code. That's a browser extension problem, so I built one: miacademy-browser-extension.
What it does
The extension adds a toolbar popup with checkboxes grouped by section — Navigation, Left Navigation, Header, Footer, and Global elements like the floating help icon. Toggle a box and the corresponding element disappears from the page immediately, no reload required. Preferences are saved via chrome.storage.sync, so they follow the same signed-in browser profile across devices.
- Hide navigation items ("All Lessons", "My World"), left-nav panels ("On your Learning Path", "Tell me more", the solved-challenges link), the tutorial videos dropdown, the footer, and the floating help icon
- Select All / Deselect All for bulk changes
- Instant apply — the popup pushes changes straight to the page
- Domain-restricted to
miacademy.coandwww.miacademy.co— it won't run anywhere else, including subdomains
Architecture
It's a deliberately small Manifest V3 extension — five files doing the real work:
manifest.json # MV3 manifest, permissions, host restrictions
popup.html/js # Toolbar popup UI and storage handling
content.js # Injected into Miacademy.co pages, does the hiding
styles.css # Popup styling
Content script + selector map. content.js keeps a plain object mapping a checkbox key to a CSS selector (or a function, for elements that need real DOM traversal instead of a clean selector):
const elementSelectors = {
'navigation-all-lessons': 'li.mia-Navbar-item:nth-child(2)',
'navigation-my-world': 'li.mia-Navbar-item:nth-child(3)',
'left-nav-learning-path': '.mia-VideoPanel',
'left-nav-tell-me-more': '.mia-ExplanationButton',
'left-nav-your-challenges':
'#contentcontent__ctl4 > .mia-sidebar > div:nth-child(1) > div > div.panel-body > div > ul > li:nth-child(2)',
'header-tutorial-videos': '.mia-TutorialVideosDropdown',
'footer-footer': 'body > div.mia-pageWrapper > footer',
'global-help-icon': '.mia-FloatingHelpButton > div:nth-child(1)'
};
Elements are hidden by setting display: none rather than removing them from the DOM, and their original display value is cached in a Map first so unhiding restores exactly what was there — important on a page where some of those values aren't just the default block.
Message passing between popup and content script. The popup never touches the DOM directly. It writes the preference to chrome.storage.sync, then sends a chrome.tabs.sendMessage with an updateVisibility action to the active tab's content script. If that fails — usually because the content script hasn't loaded yet on a tab that predates the extension being installed — the popup falls back to chrome.scripting.executeScript to inject content.js on demand and retries the message. That fallback path turned out to matter more than I expected; without it, "install extension, don't reload the tab, open the popup" was a dead end.
MutationObserver for dynamic content. Miacademy is a single-page-app-style site — lessons load in without full navigations, so a selector that matched on page load can go stale the moment new content streams in. A MutationObserver on document.body re-runs updateVisibility() whenever the DOM changes, so hidden elements stay hidden as the student moves between lessons.
Domain restriction as a security boundary, not just a matches pattern. The manifest scopes host_permissions and content_scripts.matches to https://miacademy.co/* and https://www.miacademy.co/*, but the content script also checks window.location.hostname at runtime and refuses to do anything if it isn't an exact match — deliberately excluding subdomains. Manifest globs are usually enough, but doubling up with a runtime check makes the domain boundary explicit and auditable in one place, which mattered to me since this is an extension I'm comfortable handing to other parents.
Why Manifest V3
Chrome (and now Firefox) have converged on Manifest V3, and building against it from the start meant not having to migrate later. The tradeoffs that show up in this codebase specifically:
- No persistent background page — the popup and content script communicate directly via messaging instead of routing through a long-lived background context
scripting.executeScriptinstead of the oldtabs.executeScript, used purely as the fallback injection path described abovestorage.syncoverstorage.localso preferences travel with the browser profile instead of being tied to one machine
What I'd extend next
The selector map is the extension's only real maintenance liability — if Miacademy changes a class name or restructures a panel, that entry silently stops matching. The setSelectors message handler in content.js already exists as a hook for pushing selector updates without shipping a new extension version; the next step would be wiring that to a small remote config so selector drift doesn't require a store update to fix.
Try it
The source, install instructions, and full docs are on GitHub: github.com/camyers/miacademy-browser-extension. It's unpacked-load only for now (no Chrome Web Store listing), so installation is "enable developer mode, load unpacked, point it at the folder" — details are in the repo's installation guide.