DataTalksClub / DataTalksClub/website
Make dark mode a user setting with a System option
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
User outcome
A member chooses once, in their account settings, whether the site follows their operating
system's appearance (System) or always uses Light or Dark. A visitor with no account gets the
same System-by-default behaviour locally. Nobody who already chose a theme sees it change when
this ships.
This is a bounded P2 slice: one preference, one control, no preferences framework.
Normative links and current contract (verified at origin/main, face8e4808d65afbf0374d1ced7a88079950d663)
_docs/design/design-5a.md— the token table is mirrored onbody.dark-mode; light/dark
pairs are the only sanctioned colour source for design 5a pages.- Storage today: anonymous choice in
localStorage['darkMode']('true'/'false'), read by
the pre-paint bootstrap intemplates/core/_site_shell_head.html:32-47(anonymous only,
key read at line 39) and by the toggle script in
templates/core/_site_shell_foot.html. The Playwright suites seed this key directly
(e.g.playwright_tests/test_legal_footer_125.py:95,127). - Server state today:
CustomUser.dark_modeboolean (accounts/models.py:67, default
False), published asDARK_MODEbycourse_management/context_processors.py:5-12and
rendered into the body element (class="dark dark-mode",data-dark-mode,
data-authenticated,data-toggle-url) by every design 5a page template (e.g.
templates/public/text_page.html:153-165). - Endpoints today (both JSON POST; there is no cookie for dark mode — the foot include's
"cookie endpoint" wording is loose, and no cookie is introduced by this issue):POST /accounts/toggle-dark-mode/(accounts/views/account_toggles.py:13-29): flips the
boolean, returns{"dark_mode": bool}; consumed by the design 5a masthead script and by
the legacycourses/static/dark_mode.js.POST /accounts/settings/toggle/(update_account_toggle,
accounts/views/account_toggles.py:32-64): sets an explicit value for an allowlisted
field (onlydark_modetoday); consumed by the settings page's immediate-toggle row
(course_platform_templates/accounts/account_settings.html:394-401via
courses/static/settings_toggles.js).
- Settings UI today: the account settings page (design 5a already) exposes a "Use dark mode"
checkbox (accounts/forms.py:94,105,124). prefers-color-schemeappears nowhere in repository CSS/JS/templates (verified); the
effective default is light for everyone who never touched the toggle.- Two theme implementations are live during the design 5a porting window: the token table
underbody.dark-mode(templates/core/_design_system.html) and the legacy
courses/static/courses.cssoverrides (137body.darkselectors) with
courses/static/dark_mode.js. Both read the same storage key, body classes, data
attributes, and flip endpoint.
Product decision (settled in grooming)
- Preference model: one three-state preference,
System/Light/Dark, default
Systemfor new accounts and anonymous visitors. - Migration semantics: existing
dark_mode=Truerows becomeDark; existing
dark_mode=Falserows becomeLight. A storedFalsecan be either "never chose" or "may
have deliberately switched off", and mapping it toLightis the only choice that preserves
every current user's rendered outcome at deploy time; nobody's theme flips because this
shipped. - Masthead pill: stays a two-state quick override that writes an explicit
Lightor
Dark(fromSystem, the pill writes the opposite of the currently effective theme).
Returning toSystemhappens in account settings only — no three-state cycling on a
single button. - Server vs browser: the server cannot see the OS preference, so
Systemis resolved in
the browser by the pre-paint bootstrap, which extends from anonymous-only to any visitor
whose resolved preference isSystem.
Scope
- Model/migration: replace
CustomUser.dark_modewith
theme_preference = CharField(choices=[system, light, dark], default="system"); data
migration mapsTrue → dark,False → light; migration-drift check green. One source of
truth — no dual columns. - Context:
course_management/context_processors.pypublishesTHEME_PREFERENCE(raw)
and keeps publishingDARK_MODEas the server-effective boolean (systemresolves to
light server-side), so legacy templates and pages keep rendering exactly as today. - Templates: body element gains
data-theme-preference="{{ THEME_PREFERENCE }}";
server-rendereddark dark-modeclasses remain forlight/dark. The head bootstrap
(_site_shell_head.html) resolvesSystemvia
matchMedia('(prefers-color-scheme: dark)')pre-paint for anonymous and authenticated
alike, and keeps honouring the stored explicit choice first. - Anonymous storage: new key
themePreference(system|light|dark); the legacy
darkModekey is read as an explicitLight/Darkchoice when the new key is absent and
is never written again. Existing visitors and the Playwright suites that seeddarkMode
keep working unchanged. - Endpoints:
update_account_toggleallowlist gainstheme_preference, accepting exactly
system|light|dark(400 otherwise); response includestheme_preferenceand the
effectivedark_modeboolean.toggle_dark_moderemains for legacy un-ported pages: it flipstheme_preference
betweenlightanddarkand keeps returning{"dark_mode": <effective bool>}so
courses/static/dark_mode.jsneeds no change.- The design 5a masthead script switches to the value-setting endpoint and stops
mirroring the preference intolocalStoragefor authenticated users.
- Settings control: replace the "Use dark mode" checkbox with a labelled three-option
radio group (fieldset/legend "Theme"; options System — "Follows your device's appearance",
Light, Dark), keeping the immediate-save row behavior and the accessible help/error
relationships the page already uses. - Accessibility:
aria-pressedon the masthead pill continues to reflect the effective
dark state; declarecolor-scheme: light/color-scheme: darkalongside the existing
body.dark-modetoken table so native controls and scrollbars follow the theme. - Observability:
account.toggle_updatedrecordsfield="theme_preference"and the new
value. - Focused Django tests (endpoints incl. validation and 405/302 boundaries, migration mapping,
context processor, form) and Playwright core coverage for the new behaviour.
Non-goals
- No preferences framework, generic preference store, or per-device preference sync.
- No theming for the Django admin, emails, or rendered course artifacts.
- No live re-evaluation of
prefers-color-schemechanges mid-session:Systemresolves at
page load; reacting to an OS switch without a reload is future work. - No changes to the legacy
courses.cssdark overrides,dark_mode.js, or legacy page
templates beyond keeping their contracts working; porting those pages is the design 5a
port's work, not this issue's. Legacy pages therefore keep server-effective rendering and
do not follow the OS until they are ported. - No cookie is introduced for dark mode (the timezone
browser_timezonecookie pattern is
not adopted here). - No redesign of the masthead pill beyond its label/pressed-state semantics.
Dependencies
- None open. The design 5a shell, account settings page, and toggle endpoints all exist at
main(#179 work landed).
Acceptance criteria
- Migration maps existing rows
dark_mode=True → theme_preference=darkand
False → light; new users default tosystem; migration-drift check passes. - Authenticated
light/darkusers get server-rendered body classes with no
script-dependent flash; authenticatedsystemusers and anonymous visitors resolve
Systemviaprefers-color-schemein the pre-paint bootstrap (no light flash when the
OS is dark). - Anonymous explicit choice persists in
themePreferenceand wins over the OS; a legacy
darkModevalue still applies whenthemePreferenceis absent; nothing writes
darkModeanymore. - The masthead pill writes an explicit
light/darkfor both anonymous and
authenticated visitors, updatesaria-pressedand its label, and persists across
reloads. - Account settings shows the three-option Theme radio group; changing it saves
immediately viaupdate_account_toggle, applies without a full-page reload, and an
invalid value is rejected with 400. -
POST /accounts/toggle-dark-mode/still flips and returns{"dark_mode": bool};
unauthenticated POST still redirects to login (302) and GET still returns 405. -
DARK_MODEremains server-effective for legacy templates; a legacy course page renders
and toggles exactly as before. -
color-schemeis declared for both themes in the design system. - Playwright core covers: OS-dark anonymous first visit renders dark; explicit choice
overrides OS; authenticatedsystemfollows OS; reload shows no light flash; existing
suites that seeddarkModestay green. - Tester screenshots at desktop and mobile: light and dark variants of the homepage or a
content page, and the settings page showing the three-state control, in both themes.
Browser scenarios
- Fresh anonymous visitor, OS set to dark: first design 5a page renders dark with no light
flash; the masthead pill shows the state; reload keeps dark. - Same visitor activates the pill: theme switches to the explicit opposite and survives
reload even after switching the OS to light. - Member with preference Dark (migrated) opens any design 5a page: dark, server-rendered,
no flash; the legacy course platform page they can still reach renders identically to
today. - Member sets Theme to System in account settings with OS dark: next page load is dark;
setting OS to light and reloading renders light. - Keyboard-only user tabs to the Theme group, changes options with arrows, and hears the
saved state announced; the masthead pill's pressed state matches the visible theme. - All of the above at mobile width; nothing overflows or overlaps in the masthead.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with accounts/models.py, accounts/forms.py, accounts/views/account_toggles.py, the context processor, and the design 5a shell templates; trace the existing dark_mode contracts first. Run the focused Django tests and the named Playwright suites, including playwright_tests/test_legal_footer_125.py, then cover the acceptance scenarios. Done means migration, settings, endpoints, no-flash System behavior, legacy compatibility, accessibility, and screenshots pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- django, javascript, playwright, python
- Domain
- accessibility, backend, frontend, full-stack, testing-qa
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100