All articles
06 · Design & interaction · 2026.10 · 6 min

x³: keeping the Mauve hidden mode discoverable and cheap

Click the x in the devilyxrd wordmark in the footer three times and the site switches to Mauve mode. In this note I explain how the secret gives itself away, what changes when it turns on, and how I kept all of it cheap, with a class and a few variables, while respecting reduced motion.

01A secret that can be found

A hidden feature nobody finds does not really exist. So I layered the hints. The x in the big footer wordmark is actually a button, and a small exponent sits next to it: x³. Until someone touches it, the x wobbles now and then on a seven-second loop (xwob). Anyone who opens the console sees a coloured x³ and a short note telling them to tap the x below three times; consoleHint prints it after the app mounts. There is also a shortcut: an action in the command palette that toggles Mauve mode.

The x button has an aria-label (toggle Mauve mode). So for someone using a screen reader it is not even a secret, just a plain button. The surprise stays on the visual side, not on the access side.

The command palette action is part of the search index too. searchIndex.ts registers it with its label in both languages and keywords such as mauve, iris, secret and gizli; someone who opens the palette and types secret or mauve lands directly on it. It is a shortcut for someone who knows the secret but does not want to scroll down to the footer. Like the header badge, the palette action calls toggleSecret, so it flips the mode without waiting for three clicks and plays the celebration from the centre of the screen.

02Three clicks

The counter is a single number in the ui store: secret. It goes up with every click, and whether the mode is on is derived from it. The exponent in the footer shows ui.secret % 3 || 3; in turn x¹, x², x³. With each click the x turns another 30 degrees and a short toast says x¹ or x². The third click flips the mode.

src/stores/ui.ts, src/composables/useSecretMode.ts
// src/stores/ui.ts
export const mauve = computed(() => Math.floor(ui.secret / 3) % 2 === 1)

// src/composables/useSecretMode.ts
export function clickSecret(x: number, y: number, xEl?: HTMLElement) {
  ui.secret++
  const step = ui.secret % 3
  if (xEl && !isReduced()) gsap.to(xEl, { rotate: step * 30, duration: 0.5, ease: 'back.out(3)' })
  document.documentElement.classList.add('x-found')
  if (step === 0) celebrate(x, y)
  else showToast(step === 1 ? 'x¹' : 'x²', 1200)
}

On the first click the html element gets the x-found class and the wobble stops. The hint has done its job and no longer needs to draw attention. Deriving the state from a single number also means turning off works with the same arithmetic: the sixth click turns the mode off, the ninth turns it back on.

03What happens when it turns on

The celebrate function first saves the state and shows a toast. Then, if reduced motion is off, two animations play. The first is the shockwave: a clip-path circle that grows from the click point to the farthest corner of the screen. The element is appended to body, inverts the page beneath it with mix-blend-mode: difference, and is removed when the GSAP timeline finishes. The second is the scatter: the letters of the footer wordmark spring back into place from random positions and angles with an elastic ease.

Everything that changes while the mode is on lives in CSS, under .app.mv. The Iris variable is set to Mauve (#C7A8BE), so everything using the accent colour changes together. The cursor variables switch to x-shaped SVGs. .app.mv::after lays thin horizontal lines over the whole screen without blocking clicks. In the hero the chaos level goes from 1 to 2; the rotation and random offsets of the crosses are multiplied by that number, and the rings turn further.

tokens.css, shell.css
/* tokens.css */
.app.mv { --iris: var(--mauve); }

/* shell.css */
.app.mv::after { content: ''; position: fixed; inset: 0; z-index: 300; pointer-events: none;
  background: repeating-linear-gradient(0deg, rgba(199, 168, 190, .045) 0 1px, transparent 1px 3px); }
html:not(.x-found) .ftw .xbtn { animation: xwob 7s var(--e) 3s infinite; }
@media (prefers-reduced-motion: reduce) { .ftw .xbtn { animation: none !important; } }
A secret is worth the small surprise of finding it; every visitor should not have to pay for it.

04Persistence and the way out

The mode lasts for the session. persist writes 1 or 0 to sessionStorage under the key dvx-secret; main.ts calls restoreSecret after mount, and if the value is 1, ui.secret becomes 3. I put this after mount on purpose: the prerendered HTML is always in normal mode, and the client's first render has to produce the same markup. I chose sessionStorage because this is a game; I wanted the site to return to normal once the tab is closed. If storage is blocked, a try/catch makes the mode simply start off.

While the mode is on, a badge with a dashed Mauve border appears in the header: Mauve ×. Clicking it runs toggleSecret, which jumps the counter to the next multiple of three and plays the same celebration from the click point. The badge is there so someone who turned the mode on by accident can leave without scrolling back down to the footer.

05Reduced motion and cost

With prefers-reduced-motion on, the x does not turn, the shockwave and scatter do not play, and the wobble animation is disabled in CSS. The mode still turns on and the toast still appears; there is just no motion. The click pulse in the hero is never created in that case either.

On the cost side, almost nothing runs while the mode is off: a number, a computed and one CSS animation on the x. The shockwave element exists only at the moment of the click and is removed when it is done. When the mode turns on, the hero watches the mauve value and redraws all of its cells once (repaintAll), then everything returns to its normal loop. There is no separate theme file and no separate component tree.

Related articles
  1. 07Language lives in the URL: Turkish at the root, English under /enInterface2026.106 min
  2. 08From an XSLT-styled sitemap.xml to a real HTML sitemap pageSearch & GEO2026.105 min