Skip to content

feat(studio): a light and dark theme toggle in the header that remembers the choice - #4839

Merged
miguel-heygen merged 8 commits into
mainfrom
feat/studio-theme-toggle
Oct 1, 2026
Merged

miguel-heygen merged 8 commits into
mainfrom
feat/studio-theme-toggle

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

What changes for a person using Studio

A sun and moon button in Studio's header flips between light and dark, with a circular reveal from the button, the same control as hyperframes.dev's theme toggle. Studio opens light the first time; after that the saved choice comes back on the next load from the first frame. The player stays dark in both. With reduced motion on, the theme switches without the reveal.

How

  • utils/studioTheme.ts owns the rule for Studio's own app: the saved choice (theme in Studio's existing hf-studio-ui-preferences), else light. Light is data-theme="paper" on the document element, the hook theme.css keys on; dark is no attribute.
  • A short inline script in index.html applies the same rule before the first paint, so the saved theme paints from the first frame. studioTheme.test.tsx reads that script out of the parsed index.html and runs it against every saved value (including junk, null and unparsable storage) and holds it to the owner.
  • The button shows the theme that is on screen, read from the document, so it stays right when something else changes the theme (another tab's save, a host).
  • ThemeToggle is the header's ghost icon button with a tooltip, before Window. While its own transition runs it marks the document element, and only then is the browser's default crossfade switched off, so other view transitions on the page keep theirs.
  • Hosts own their theme. The header shows the toggle only inside ShowThemeToggle (a context that defaults off), and only Studio's own main.tsx provides it. A host that embeds StudioApp gets no toggle, and Studio never writes data-theme for it. App.tsx is unchanged.

Before

Before: header has no theme control

After

The repo's product-promo example at 1280x800, 1440x900 and 1920x1080.

After: header, first run opens light, moon button
After: header, light, hovered
After: mid-reveal, dark growing from the button (captured with the animation slowed 4x)
After: header, dark after one click, sun button
After: 1280x800, first run, light
After: 1280x800, after one click, dark
After: 1440x900, first run, light
After: 1440x900, after one click, dark
After: 1920x1080, first run, light
After: 1920x1080, after one click, dark
After: dark after a reload, the choice kept

Verification

  • studioTheme.test.tsx: the boot script equals the owner for saved light, dark, nothing, junk, null, unparsable and non-object storage; first run is light; boot runs before the app module; a click flips and saves, a second click flips back; the button follows a theme set by someone else. The reveal tests stub view transitions: a keyboard press grows the circle from the button's centre to the far corner, the document is marked while the reveal runs and cleared after, a second click mid-reveal keeps the marker until its own reveal ends, and reduced motion switches with no transition. Making the button read storage, ignoring reduced motion, using the click point for a keyboard press, never setting or never clearing the marker, or letting an earlier reveal clear a later one's marker each fails its test.
  • StudioHeader.dom.test.tsx: a header mounted with no provider (as a host mounts it) shows no toggle and leaves a host's data-theme alone; inside the provider it shows the toggle. Defaulting the context to on fails the first.
  • 39 tests across those two files and studioUiPreferences.test.ts pass. tsc --noEmit on packages/studio: no errors. oxlint and oxfmt clean on the changed files.
  • In the built Studio (headless Chromium 147, the repo's product-promo example): the first load is light, a click reveals dark from the button (the only animation is the circle's clip-path; the crossfade is off while the marker is set, and the marker is gone after) and removes data-theme, and a reload boots straight into dark.

@miguel-heygen
miguel-heygen force-pushed the feat/studio-theme-toggle branch 4 times, most recently from 8fe20db to 1b974d2 Compare October 1, 2026 12:51
Base automatically changed from feat/studio-graphite to main October 1, 2026 14:21
@miguel-heygen
miguel-heygen force-pushed the feat/studio-theme-toggle branch from 33ef964 to 10b8115 Compare October 1, 2026 14:22
@miguel-heygen
miguel-heygen marked this pull request as ready for review October 1, 2026 14:23
Comment thread packages/studio/src/utils/studioTheme.test.tsx Fixed
…ers the choice

The sun and moon button flips Studio between Graphite light and dark with a
circular reveal, saves the choice, and follows the system until one is made.
…ows the theme on screen

A host that embeds StudioApp gets no toggle; the button reads the document's theme, the reveal drops the default crossfade, and reduced motion skips it.
…e default for the toggle

The crossfade rule applies only while the toggle's own transition runs, StudioApp passes the header's single default through, and the reveal's reduced-motion and keyboard paths are tested.
Only the latest reveal clears the document marker; tests pin the marker while a reveal runs and the circle's radius.
… plain functions

Simplify pass: one icon props object for the sun and moon, no useCallback around a handler nothing memoises, cannotReveal named for what it answers, the test reuses the theme reader.
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Edit accuracy: 557 passing here, 557 on the base branch

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

…xt; tests parse index.html

App.tsx stays at main's size: main.tsx provides ShowThemeToggle and the header reads it, default off for hosts. The boot-script tests read index.html through DOMParser instead of an HTML regex.
@miguel-heygen
miguel-heygen force-pushed the feat/studio-theme-toggle branch from 10b8115 to 1c4be8a Compare October 1, 2026 14:58
@miguel-heygen
miguel-heygen merged commit 9f4d9e6 into main Oct 1, 2026
119 checks passed
@miguel-heygen
miguel-heygen deleted the feat/studio-theme-toggle branch October 1, 2026 15:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants