Theme Switch for Framer · free · v1.0

Let them pick the dark.

A light / dark / system switch for Framer and any website. Your Framer color styles follow the visitor's choice instead of only their device, the choice is remembered, and a tiny head snippet means no white flash on load. Try it on the little site here.

Download freeSee every style

Free for personal and commercial projects · Script tag, React and Framer builds

Northwind

Design studio · Oslo

Quiet interfaces for loud ideas.

Your visitors pick light, dark or their device. Color styles follow; the choice is remembered.

See our work

mode: system · theme: light

Styles

Three styles, three sizes, two looks.

Every switch here is live and they share one theme: pick in any of them and they all follow, along with the site above. Each card shows its switch in the theme you picked.

S · M · L: 28, 34 and 42 px tall

Framer setup

Your color styles, on the visitor's terms.

Framer gives every color style a light and a dark value, then lets the visitor's device pick. Theme Switch adds the choice. Two pastes, about two minutes:

  1. 01Paste the head snippetSite Settings → General → Custom Code → Start of <head> tag. It sets the theme before the page paints.
  2. 02Add the componentAssets → Code → New code file named ThemeSwitch, paste framer/ThemeSwitch.tsx from the download.
  3. 03Drop it in your navPick the style, size, look and accent in the properties panel. Leave Storage Key, Attribute and Default as they are.
  4. 04Give colors a dark valueAssets → Colors: each color style has Light and Dark. Use color styles everywhere, publish, and switch on the live site.
Head snippet · Start of <head> tag
<script>(()=>{function l(r){let i=window,n=i.__pfts||(i.__pfts=[]),c=s=>{for(let t of s)t.cssRules&&(/color-scheme/.test(t.media?.mediaText)&&!n.some(o=>o[0]==t)&&n.push([t,t.media.mediaText]),c(t.cssRules))};for(let s of document.styleSheets)try{c(s.cssRules)}catch{}for(let[s,t]of n)s.media.mediaText=r?t.replace(/\(prefers-color-scheme: (\w+)\)/g,(o,e)=>e==r?"(min-width:0px)":"(max-width:-1px)"):t}function u([r,i,n,c,s]){let t=document,o=t.documentElement,e;try{e=localStorage.getItem(r)}catch{}/^(light|dark|system)$/.test(e)||(e=c);let a=e=="system"?matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light":e;if(i&&o.setAttribute(i,a),n&&o.classList.toggle(n,a=="dark"),o.style.colorScheme=a,window.__pftsSync=l,s&&e!="system"){let d=window,p=d.__pftb=()=>{m.disconnect(),d.__pftb=0},f=()=>d.__pftb&&l(a),m=new MutationObserver(f);f(),m.observe(t,{childList:!0,subtree:!0}),t.addEventListener("load",f,!0),t.addEventListener("DOMContentLoaded",p)}}u(["theme","data-theme","dark","system",1]);})()</script>

What follows the switch

  • Every fill, text color, border, shadow and gradient that uses a color style with Light and Dark values
  • Other CSS on the page written for prefers-color-scheme, including styles that load later
  • Your own Custom Code CSS keyed off html[data-theme="dark"] or html.dark
  • Scrollbars and form controls (color-scheme)

What doesn't

  • Colors typed in as plain hex values instead of color styles
  • Images: Framer has no dark variants. Two images and a line of Custom Code CSS (layer names) do it; the README shows how
  • Component variants, and the editor canvas (it has its own light/dark preview)
  • Embeds and iframes (YouTube, maps, forms): they follow the device

Code

Script tag, React or Framer.

The same switch and the same settings in all three. The head snippet goes first in every case: it's what stops the flash.

<!-- 1. first thing in <head> (head-snippet.html) -->
<script>(()=>{function l(r){let i=window,n=i.__pfts||(i.__pfts=[]),c=s=>{for(let t of s)t.cssRules&&(/color-scheme/.test(t.media?.mediaText)&&!n.some(o=>o[0]==t)&&n.push([t,t.media.mediaText]),c(t.cssRules))};for(let s of document.styleSheets)try{c(s.cssRules)}catch{}for(let[s,t]of n)s.media.mediaText=r?t.replace(/\(prefers-color-scheme: (\w+)\)/g,(o,e)=>e==r?"(min-width:0px)":"(max-width:-1px)"):t}function u([r,i,n,c,s]){let t=document,o=t.documentElement,e;try{e=localStorage.getItem(r)}catch{}/^(light|dark|system)$/.test(e)||(e=c);let a=e=="system"?matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light":e;if(i&&o.setAttribute(i,a),n&&o.classList.toggle(n,a=="dark"),o.style.colorScheme=a,window.__pftsSync=l,s&&e!="system"){let d=window,p=d.__pftb=()=>{m.disconnect(),d.__pftb=0},f=()=>d.__pftb&&l(a),m=new MutationObserver(f);f(),m.observe(t,{childList:!0,subtree:!0}),t.addEventListener("load",f,!0),t.addEventListener("DOMContentLoaded",p)}}u(["theme","data-theme","dark","system",1]);})()</script>

<!-- 2. your dark colors -->
<style>
  :root { --bg: #fafaf9; --text: #1c1917; }
  html[data-theme="dark"] { --bg: #0f0e0d; --text: #f5f5f4; }
</style>

<!-- 3. the switch, anywhere, and the script once -->
<pf-theme-switch variant="segmented" look="copper"></pf-theme-switch>
<script src="theme-switch.min.js" defer></script>

Works with

  • FramerHead snippet in Custom Code + a code component with a properties panel
  • WebflowSnippet in Head code, script in Footer code, <pf-theme-switch> in an Embed
  • Plain HTML / any siteSnippet in <head>, one script, <pf-theme-switch> anywhere
  • WordPressSnippet in the header, a Custom HTML block for the switch
  • ShopifySnippet in theme.liquid, the script as a theme asset
  • Squarespace, WixCode Injection / Custom Code (plan must allow custom code)
  • React, Next.js<ThemeScript /> in <head>, <ThemeSwitch />, useTheme()

What it does

  • Made for Framer. Framer switches your color styles' dark values only when the visitor's device is in dark mode. Theme Switch lets the visitor choose, and every fill, text color and border that uses a color style follows.
  • No flash. A head snippet (1,020 bytes) sets the theme before the page paints, so a visitor who picked dark never sees a white page first, on a Framer site, in Webflow or in Next.js.
  • Three styles. A segmented System / Light / Dark control, an icon button that cycles, or a switch. Three sizes, a neutral or copper look, your accent color.
  • Remembers, follows, syncs. The choice is saved in the browser. On System it follows the device as it changes. Other tabs switch too. Every switch on the page stays in step.
  • Works with your CSS. It sets data-theme and the dark class on <html> (Tailwind's class strategy works as is) plus color-scheme, and makes CSS written for prefers-color-scheme follow along.
  • Accessible. Native radio buttons in a named group, a real switch, a pressed toggle; arrow keys, Space and Enter; a visible focus ring; labels for screen readers. Reduce Motion turns the fade off.
  • Small. 2.95 KB gzipped for the script build, no dependencies. A cross-fade with the View Transitions API where the browser has it, nothing where it doesn't.

Theme Switch v1.0

Free for personal and commercial projects

Download free

A free download. No account needed. Use it on as many sites as you like, including client work; just don't resell the component itself.

  • head-snippet.html: the no-flash snippet (1,020 bytes)
  • theme-switch.min.js: <pf-theme-switch> + data-attribute buttons for any website (2.95 KB gzipped), and an inline copy for site builders
  • theme-switch.esm.js + TypeScript types, for bundlers
  • ThemeSwitch.tsx for React and Next.js: <ThemeSwitch>, useTheme(), <ThemeScript>
  • ThemeSwitch.tsx for Framer: a code component with a properties panel
  • demo.html, a README with quick starts for nine platforms, LICENSE.md

Parallel Studio All-Access Pass

Like it? Get all 8 of our web effects for $49.

Particle Forms, Shader Gradients, Liquid Cursor, Kinetic Type and the rest: script tag, React and Framer builds, one purchase ($92 separately, save $43).

See the All-Access Pass →

FAQ

Is it really free?

Yes: free for unlimited personal and commercial projects, including client sites. The only thing you can't do is sell or give away the component itself (as a code component, plugin, template add-on and so on). LICENSE.md in the download has the details.

Framer already has dark mode. What does this add?

Framer's color styles switch with the visitor's device setting, and there's no built-in way for visitors to choose. Theme Switch adds the choice (System, Light or Dark), remembers it, and makes your color styles follow it: it rewrites the page's prefers-color-scheme rules while a theme is picked.

Why do I need the head snippet?

The switch loads with the page's scripts, which is too late for the first paint: a visitor who picked dark would see a light flash on every page load. The snippet (about 1 KB, no requests) sets the theme first. On Framer it's also what makes the script build sync your color styles; the Framer component can sync without it but can't prevent the flash.

What doesn't change with the switch?

Colors that aren't color styles, images (Framer has no dark variants, but two images and one CSS line do it), component variants, embeds and iframes, and anything that asks the device directly. The README lists it all.

Does it work with Tailwind or next-themes?

Yes. It sets the dark class and data-theme on <html> (both configurable), so Tailwind's class strategy works as is, and it uses the same localStorage key and values as next-themes, so visitors keep their choice if you switch.

Is it accessible?

The segmented control is a group of native radio buttons with a name, the switch is a real switch, the icon button says what's on; all work with the keyboard and show a focus ring. With Reduce Motion there's no fade.

Where do I get it?

Press Download free. It opens a free Polar checkout: no payment, and the download is on the confirmation page.

Something else?

Email hi@gunnargray.com.

Hear about updates

An email when Theme Switch gets an update, then the occasional new tool. No more than that.