A free, open-source Chrome extension that does what Better Canvas / BetterCampus does — dark mode, custom themes, dashboard grades, a real planner, GPA and what-if calculators — with nothing behind a paywall, no account, and no server.
CanvasMax is an independent, clean-room implementation. It shares no code with any other Canvas extension.
Better Canvas was a free extension. It rebranded to BetterCampus and moved its themes, full planner and other tools behind a $120/year Pro tier. That is a lot of money for students, and it is charged for something that turns out not to need a paid backend at all.
Everything those features display comes from data the student's browser is already allowed to read: Canvas exposes a complete REST API at /api/v1/*, authenticated by the same session cookie that renders the pages. An extension running on a Canvas tab can call it directly.
So CanvasMax has:
- no account system
- no license server
- no backend of any kind
- no analytics, telemetry, or third-party requests
Which means there is nothing to charge for. See How it works.
| Feature | BetterCampus | CanvasMax |
|---|---|---|
| Dark mode | Free | Free |
| Built-in themes | Limited set free | 12 built in |
| Custom themes | Pro | Free, unlimited |
| Theme editor with live preview | Pro | Free |
| Theme import / export / sharing | Pro | Free, plain JSON files |
| Scheduled + system-following dark mode | Free | Free |
| Dashboard card colors and images | Free | Free |
| Card gradients, condensed and compact layouts | Free | Free |
| Hide, rename and reorder courses | Partly Pro | Free |
| Grades on dashboard cards | Free | Free, with a fallback that computes hidden totals |
| Full planner / assignments-due list | Pro | Free |
| Check items off from the planner | Pro | Free, writes back to Canvas |
| Dashboard notes | Free | Free |
| GPA calculator (4.0, 4.3, weighted HS) | Free | Free |
| What-if grade calculator | Pro | Free |
| "What do I need on the final?" | Pro | Free |
| Assignment preview without navigating | Free | Free (alt-click) |
| Custom fonts and text scaling | Free | Free |
| Separate fonts per role (UI / headings / content / code) | Not offered | Free |
| Google Fonts import | Pro | Free, downloaded once and stored locally |
| Custom background image | Pro | Free, upload or link |
| Interface tweaks (hide logo, nav items, sidebar) | Free | Free |
| Settings sync across devices | Pro | Free, via Chrome profile sync |
| Due-date reminders | Pro | Free |
| Custom CSS | Pro | Free |
| Self-hosted Canvas domains | Varies | Free, per-domain opt-in |
CanvasMax is not on the Chrome Web Store yet. Load it unpacked:
git clone https://github.com/EmberGuild-Labs/CanvasMax.git- Open
chrome://extensions - Turn on Developer mode (top right)
- Click Load unpacked and choose the cloned folder
- Open your Canvas site and sign in
To build an uploadable zip instead:
npm run package # runs the static checks, then writes dist/canvasmax-<version>.zipWorks in any Chromium browser with Manifest V3 support: Chrome, Edge, Brave, Opera, Arc, Vivaldi.
Most universities self-host Canvas at something like canvas.myschool.edu rather than myschool.instructure.com. An extension cannot request access to every possible domain without asking for access to your entire browsing history, which CanvasMax will not do.
Instead: open CanvasMax settings → Canvas sites, type your domain, and click Grant access. Chrome will prompt for that one site. CanvasMax then registers its scripts for that domain only.
Twelve built-in themes — eight dark (Midnight, Carbon, True Black, Grape, Frost, Deep Tide, Moss, Ember) and four light (Canvas Light, Paper, Clear Sky, Sepia) — plus a full editor for your own.
A theme is nine colors. The engine derives everything else (hover states, raised surfaces, borders, shadows, translucent overlays, readable text on each background) so you are not hand-picking forty values to get a coherent result.
Dark mode can follow your system setting, stay on, stay off, or run on a schedule. Scheduled mode wakes at the exact boundary rather than polling, and handles windows that cross midnight.
No flash of white. Reading settings from extension storage is asynchronous, which normally means a white flash on every page load. CanvasMax mirrors the compiled palette into the Canvas origin's localStorage, which a content script can read synchronously at document_start, and applies it before the first paint. Only the very first page load after installing flashes.
The theme editor shows a live preview and a WCAG contrast audit as you pick colors, so you find out that your muted text is unreadable before you commit to it. All twelve built-in themes pass their contrast targets, and there is a test that keeps it that way.
A theme is a small JSON object. Export yours to a file, send it to anyone, and they can import it. There is no gallery to sign into and no server in the middle. See docs/THEMES.md for the format.
Recolor, rename, re-image, reorder and hide course cards. Three densities: default, condensed (the header image collapses to an accent bar) and compact (also drops the action row). Optional gradient headers. Configurable quick links under each card — Assignments, Grades, Announcements, Discussions, Modules, Files, Syllabus, People, Quizzes, Pages.
Card colors default to whatever you already set in Canvas, so the dashboard does not change out from under you.
Reads the score Canvas already computes for each enrollment, preferring the current grading period when your institution uses them.
When a course is configured to hide totals from students, Canvas omits the score. CanvasMax then computes it from that course's assignment groups — the same arithmetic the Grades page would run — for up to eight such courses per load, so a student with many hidden courses does not trigger a request storm.
Built from /api/v1/planner/items, which returns assignments, quizzes, discussions, calendar events and your own planner notes in one pass.
Groups by date or by course, separates overdue work into its own section, flags missing and late submissions, and lets you check items off — which writes a real planner override back to Canvas, so the change shows up in Canvas's own UI and on your phone.
Computes your GPA from live Canvas grades on three scales:
- 4.0 — standard US unweighted, A and A+ both 4.0
- 4.3 — A+ worth 4.3
- High school weighted — 4.0 base plus Honors (+0.5) and AP/IB/Dual-enrollment (+1.0)
Set credit hours once per course. Courses with no posted grade are skipped, not counted as zero. Failing a weighted class earns no rigor bonus. Where a course defines its own grading standard, that standard is used rather than an assumed one.
On any course's Grades page, CanvasMax adds a panel showing the weighted breakdown Canvas hides, and lets you type hypothetical scores and watch the course grade move. Nothing is sent to Canvas — hypotheticals stay in the page.
Enter a target percentage and each row shows the score you would need on that assignment to reach it, or tells you the target is already met or no longer reachable. The grade is piecewise-linear in any single score, so this is solved directly from two probes rather than by searching.
The math mirrors Canvas's own, including the part people get wrong: in a weighted course, a group with no graded work yet is dropped and the remaining weights renormalised, not counted as zero. Otherwise every student would show single digits in week one.
Optional desktop notifications before work is due, at lead times you choose (1 week through 30 minutes). The background worker checks every 30 minutes and never notifies twice for the same deadline. The toolbar badge shows how many things are due in the next 24 hours.
Set a page background from an uploaded image or an https:// address. Uploads
are downscaled to at most 1920px and re-encoded before storage, so a phone photo
does not sit in browser storage at full size.
The image is painted on two fixed layers behind the page — the picture, then a
scrim over it — rather than onto body. That way the blur and dim apply only to
the photograph and never to the text on top of it, and the image stays put while
the page scrolls. The dim slider is not decoration: text over a photograph is a
legibility problem, and the scrim is the fix.
Typography is split into four roles, so a display face on headings does not end up on a wall of reading:
| Role | Styles |
|---|---|
| Interface | Navigation, buttons, menus, and everything CanvasMax adds |
| Headings | Page titles, section headers, dashboard card names |
| Course content | Assignment descriptions, pages, discussion posts |
| Code | Code blocks and preformatted text |
Any font installed on the machine works. For anything else, import a Google Font by name. CanvasMax downloads the family in the background, inlines the woff2 files as data URIs, and stores the result on the device — so your Canvas pages never contact Google, and the font is immune to whatever content security policy your school's Canvas sets. Access to Google's domains is requested at the moment you import your first font, not at install time.
Hide the institution logo, breadcrumbs, right sidebar, dashboard greeting, or individual global-navigation items. Full-window width. Auto-expand modules. Custom fonts and text scaling. And a custom CSS box that can reference the CanvasMax theme variables, so your rules follow whichever theme is active.
When you are signed in to Canvas, your browser holds a session cookie. Canvas's REST API accepts that same cookie:
fetch('/api/v1/courses?include[]=total_scores', { credentials: 'same-origin' })A content script running on a Canvas tab is same-origin with Canvas, so this just works. No OAuth app, no developer key, no API token, no server relaying anything. Every number CanvasMax displays is fetched by your own browser, from your own school, using your own session — and never leaves the machine.
Two Canvas-specific details the client handles:
Accept: application/json+canvas-string-ids. Canvas IDs can exceedNumber.MAX_SAFE_INTEGER. Without this header they silently round to the wrong number.- The
while(1);prefix. Canvas prepends it to JSON bodies as an anti-hijacking guard; it must be stripped before parsing.
Pagination follows the RFC 5988 Link header's rel="next" rather than incrementing page=N, because Canvas uses bookmarked pagination on several endpoints where page numbers break.
Dark mode was originally a stylesheet full of Canvas selectors. That approach
fails in a specific and ugly way: it forces the dark palette's text colours
everywhere, but can only recolour backgrounds it has a selector for. Canvas's
newer screens are built from components whose class names are generated per
build (css-1a2b3c-view), so there is nothing stable to target — those panels
stayed white while the text on them went light, and headings became invisible.
Writing more selectors does not fix that; the next Canvas release breaks it again. So CanvasMax reads the computed background of each element and recolours the ones that are actually light, in two passes:
- Find opaque, near-neutral, light backgrounds and darken them. Only near-neutral colours qualify — a saturated light background is nearly always a status chip or a brand accent carrying meaning, and repainting those loses information.
- Inside each darkened panel, lighten only the text that would now be unreadable. Any descendant that paints its own background is skipped along with its subtree, because its text sits on that background, not on the panel that changed.
The walk is scoped to the main content containers, debounced, run in idle time,
and capped at 2500 elements per pass, since getComputedStyle forces style
resolution. It can be turned off under Settings → Appearance → Readability.
| Endpoint | Powers |
|---|---|
GET /api/v1/users/self/profile |
session health check |
GET /api/v1/courses (total_scores, current_grading_period_scores, term) |
course list, card grades, GPA |
GET /api/v1/users/self/colors |
existing Canvas card colors |
PUT /api/v1/users/self/colors/:asset |
writing a card color back to Canvas |
GET /api/v1/planner/items |
the planner panel and reminders |
POST / PUT /api/v1/planner/overrides |
checking planner items off |
GET /api/v1/courses/:id/assignment_groups (assignments, submission) |
what-if calculator, hidden-total fallback |
GET /api/v1/courses/:id/assignments/:id |
assignment preview |
GET /api/v1/users/self/course_nicknames |
course nicknames |
Every file is a classic script that attaches to one CanvasMax global. There is no bundler, no transpiler, no node_modules. What you read in this repository is byte-for-byte what runs in the browser — which matters for an extension that reads your grades, because you can audit it without trusting a build pipeline.
The same files load three ways: ordered js arrays for content scripts, <script> tags for the options and popup pages, and importScripts() in the service worker.
manifest.json MV3 manifest
src/
lib/
util.js DOM, color, and date helpers
storage.js settings schema, deep-merge, migrations
canvas-api.js REST client: pagination, caching, CSRF
themes.js palettes, CSS-variable compiler, contrast audit
grades.js weighted/unweighted grade math, what-if solving
gpa.js GPA scales and computation
fonts.js typographic roles, Google Fonts, CSS compilation
content/
early.js document_start, flash-free theme application
boot.js page detection and feature dispatch
theme.css Canvas restyling, entirely variable-driven
overlay.css styles for CanvasMax's own injected UI
features/ one file per feature, each self-registering
background/
service-worker.js alarms, reminders, badge, dynamic registration
options/ settings page and theme editor
popup/ toolbar popup
tools/
make-icons.js generates the PNG icons (no dependencies)
check.js static checks
package.js builds the store zip
tests/ 132 unit tests, node:test
Each file in src/content/features/ pushes a descriptor onto CanvasMax.features:
CanvasMax.features.push({
id: 'card-grades',
matches: (page) => page.type === 'dashboard',
async init(ctx) { /* first run on this page */ },
update(ctx) { /* settings changed */ },
});boot.js classifies the URL, runs the features that apply, and re-runs them when settings change or Canvas navigates client-side. A feature that throws is logged and skipped — one broken feature never takes down the rest.
- No network requests to anywhere but your own Canvas site. There is no CanvasMax server.
- No analytics, telemetry, or error reporting.
- No account. Nothing to sign up for.
- Settings live in
chrome.storage.sync(your Chrome profile, which is how they follow you between devices for free). Notes and caches live inchrome.storage.localand never leave the device. - Grades, assignments and course data are fetched on demand, held in memory for the page's lifetime, and never written anywhere persistent.
Permissions, and why each is needed:
| Permission | Why |
|---|---|
storage |
saving your settings |
alarms |
the 30-minute reminder check |
notifications |
due-date reminders |
scripting |
registering content scripts for custom Canvas domains |
*://*.instructure.com/* |
the Canvas sites the extension is for |
optional_host_permissions |
self-hosted Canvas domains, requested one at a time, only when you ask |
CanvasMax never requests broad host access up front. The optional permission exists so you can grant canvas.yourschool.edu specifically.
npm test # 155 unit tests (node:test, no dependencies)
npm run check # syntax, manifest integrity, CSS variable coverage
npm run icons # regenerate icons/*.png
npm run package # check, then build dist/canvasmax-<version>.zipThere is nothing to install — the test suite uses Node's built-in runner, and the extension has no dependencies.
The tests cover the pure logic, which is where the bugs that matter live: grade computation (weighted, unweighted, excused, omitted, what-if, target-solving), GPA across all three scales, theme validation and contrast, Link-header pagination, the API client's caching and request de-duplication, settings merge and migration, planner normalisation and grouping, URL classification, the colour
heuristics behind the surface sweep, and font-name sanitisation.
Two of those deserve calling out, because they are security or accessibility boundaries rather than ordinary logic:
- Font names are rejected, not sanitised. A family name arrives from a text
field, so anything containing a character a font family cannot have is refused
outright. Stripping the bad characters and accepting what is left turned
url(evil)into the plausible-looking familyurlevil— harmless, but silently not what anyone asked for. - Every built-in theme is contrast-audited by a test, including the colour a filled button's label will actually be given. That check is what caught the Clear Sky accent failing WCAG AA at 4.28:1.
npm run check additionally catches things unit tests cannot: a syntax error in a file nothing imports, a manifest entry pointing at a renamed file, a CSS variable no theme defines, and drift between the manifest's content-script list and the service worker's dynamic-registration copy of it.
- Create
src/content/features/your-feature.jsand push a descriptor ontoCanvasMax.features. - Add the path to
manifest.jsonand toCONTENT_SCRIPTSinsrc/background/service-worker.js—npm run checkfails if you forget the second one. - Add settings to
DEFAULTSinsrc/lib/storage.js. - Add a UI entry to
SECTIONSinsrc/options/options.js.
The mark is an artist's palette in pixel art — a nod to "Canvas", and to the
unlimited custom themes that are the headline free feature. tools/make-icons.js
draws it and writes all four PNGs.
It is drawn as two masters, because Chrome's four sizes do not share one clean scale factor:
| Master | Ships as |
|---|---|
paletteLarge() — 32×32 |
32 (1×) and 128 (4×) |
paletteSmall() — 16×16 |
16 (1×) and 48 (3×) |
Every size is an integer upscale of a master, so nothing is ever resampled and the pixels stay hard-edged. If you change the art, change both masters: pixel art has to be redrawn at each size rather than resized, since detail that reads at 32px turns to mud at 16px. That is also why the small master's body is written out row by row while the large one is rasterised from an ellipse — an ellipse rasterised at 16px comes out visibly lumpy.
Add an entry to BUILTIN_THEMES in src/lib/themes.js with the nine required colors. The contrast test will tell you if any pairing is unreadable before it ships.
- Tested on one real Canvas install, not many. The first release was written against Canvas's API documentation with no live instance to check against, and it showed: on a real school Canvas the newer dashboard rendered light text on panels that stayed white, making headings invisible. That is fixed — the runtime surface sweep described above replaced the selector guessing — but only one institution's Canvas has been exercised end to end. Other installs may turn up surfaces the sweep misses. The toggle under Settings → Appearance → Readability is the escape hatch, and a screenshot in an issue is the fastest way to get it fixed.
- Firefox is not supported yet. The code is close — Firefox supports MV3 — but
chrome.*calls need abrowser.*polyfill and the manifest needs abrowser_specific_settingskey. - Reminders require Chrome to be running. They are driven by
chrome.alarms, not a push service. - The hidden-grade fallback is capped at eight courses per dashboard load, to avoid hammering Canvas.
Issues and pull requests are welcome. Two rules:
- No code from other Canvas extensions. BetterCanvas is AGPL with additional non-commercial and no-redistribution restrictions. CanvasMax is a clean-room MIT implementation and must stay that way — contribute your own work only.
- No telemetry, no accounts, no paid tiers. That is the point.
MIT — see LICENSE.
CanvasMax is not affiliated with, endorsed by, or connected to Instructure, Canvas LMS, Better Canvas, or BetterCampus. "Canvas" and "Instructure" are trademarks of Instructure, Inc.