smoothly transition colors when changing theme

Hunter
Wednesday, July 29th, 2026 at 4:44 PM

index.html+1-1
resources/style.css+40-1
resources/theme.js+12-2

index.html
 7  	<title>memori</title>
 8  	<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>✂️</text></svg>">
 9  	<link rel="stylesheet" href="resources/style.css">
10 +	<script src="resources/theme.js"></script>
11  </head>
12  <body>
13  	<div class="page">

26  	<script src="resources/pointer.js"></script>
27  	<script src="resources/input.js"></script>
28  	<script src="resources/print.js"></script>
   -	<script src="resources/theme.js"></script>
29  	<script src="resources/app.js"></script>
30  	<script src="resources/pdf.js"></script>
31  </body>
resources/style.css
  1 +/* Registered so theme swaps can be transitioned — plain custom properties don't animate.
  2 +   Everything downstream (borders, color-mix, scrollbars) follows for free. */
  3 +@property --desk {
  4 +	syntax: '<color>';
  5 +	inherits: true;
  6 +	initial-value: #7a8520;
  7 +}
  8 +
  9 +@property --page {
 10 +	syntax: '<color>';
 11 +	inherits: true;
 12 +	initial-value: #fefef4;
 13 +}
 14 +
 15 +@property --grid-line {
 16 +	syntax: '<color>';
 17 +	inherits: true;
 18 +	initial-value: #a9cf51;
 19 +}
 20 +
 21 +@property --accent {
 22 +	syntax: '<color>';
 23 +	inherits: true;
 24 +	initial-value: #a81d8f;
 25 +}
 26 +
 27  :root {
 28  	/* Default colors */
 29  	--white: white;

 95  
 96  	/* Set font */
 97  	font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
 98 +
 99 +	--theme-transition: 250ms ease;
100 +	transition:
101 +		--desk var(--theme-transition),
102 +		--page var(--theme-transition),
103 +		--grid-line var(--theme-transition),
104 +		--accent var(--theme-transition);
105 +}
106 +
107 +@media (prefers-reduced-motion: reduce) {
108 +	:root {
109 +		transition: none;
110 +	}
111  }
112  
113  /* Theme definitions */

665  	-webkit-tap-highlight-color: transparent;
666  	user-select: none;
667  	-webkit-user-select: none;
    -	transition: transform 0.1s ease, box-shadow 0.1s ease;
668 +	transition: transform 0.1s ease, box-shadow 0.1s ease, color var(--theme-transition);
669  }
670  
671  .add-images-btn:active {
resources/theme.js
 5  const DEFAULT_THEME = 'guac';
 6  const DEFAULT_DARK_THEME = 'pantry';
 7  
   -function setTheme(theme) {
   -	document.documentElement.setAttribute('data-theme', theme);
 8 +function syncThemeColorMeta() {
 9  	const backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--desk').trim();
10  	document.querySelector('meta[name="theme-color"]').setAttribute('content', backgroundColor);
11  }
12  
13 +function setTheme(theme) {
14 +	document.documentElement.setAttribute('data-theme', theme);
15 +	syncThemeColorMeta();
16 +}
17 +
18 +document.documentElement.addEventListener('transitionend', (e) => {
19 +	if (e.propertyName === '--desk') {
20 +		syncThemeColorMeta();
21 +	}
22 +});
23 +
24  function cycleTheme() {
25  	currentThemeIndex = (currentThemeIndex + 1) % themes.length;
26  	const newTheme = themes[currentThemeIndex];