Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/mr-sunset/zen/llms.txt

Use this file to discover all available pages before exploring further.

Despite its calm surface, Zen is made of a handful of carefully composed files working together: index.html defines the structure and embeds the audio, style.css handles layout, theming, and animations, and script.js wires the two interactive controls. This page traces each of those moving parts from first page load to the moment the welcome screen disappears and the scene takes over.

Welcome screen flow

When the page loads, the browser renders a #welcome-container — a frosted-glass card centered on screen. It holds a heading, a short description, two tip paragraphs, a Controls checkbox, and an Enter Zen button. The card sits at z-index: 1000, floating above the island scene that is already rendered behind it. Two event listeners drive all of the welcome screen’s interactive behavior:
// Hide welcome container when button clicked and hide cursor
wEnter.addEventListener('click', () => {
    welcomeContainer.classList.add('fade-out');
})

// Black noise control logic 
wControlToggle.addEventListener('change', (event) => {
    blackNoise.controls = event.target.checked;
})
1

User checks the Controls toggle

The change event fires on #w-controls-toggle. The handler reads event.target.checked and writes it directly to blackNoise.controls — the boolean property of the <audio> element that shows or hides the browser’s native playback bar.
2

User clicks Enter Zen

The click event fires on #w-enter. The handler adds the .fade-out class to #welcome-container. CSS takes over from this point — no further JavaScript is involved.
3

Welcome screen disappears

The .fade-out animation runs for 120 ms, reducing opacity from 1 to 0. Once finished, the forwards fill mode holds the element at opacity: 0 and pointer-events: none ensures it cannot be accidentally re-interacted with.
All DOM queries and event bindings are wrapped in a DOMContentLoaded listener, so the script is safe to place at the end of <body> without a defer attribute.

Fade animation

The entire welcome screen dismissal is handled by a single CSS keyframe declared at the top of style.css:
@keyframes fade-out {
    0% { opacity: 1; }
    50% { opacity: 0.5; }
    100% { opacity: 0; }
}

.fade-out {
    animation: fade-out 0.12s forwards;
    pointer-events: none;
}
Two details are worth calling out:
  • forwards fill mode — without it, the element would snap back to opacity: 1 the moment the animation finished. The forwards keyword tells the browser to keep the computed style from the final (100%) keyframe in place indefinitely, effectively hiding the element without removing it from the DOM.
  • pointer-events: none — this is applied at the same time as the animation begins, not after it ends. That means clicks and touches pass straight through the (still technically present) container to the scene underneath from the very first frame of the fade. There is no window during the 120 ms animation where a mis-click could re-trigger a button.
The 0.12 s duration is intentionally short. A longer fade would feel sluggish on a page whose entire purpose is effortless calm. If you fork Zen and want a slower dissolve, increase the duration value in both the animation shorthand and any transition properties on child elements.

Scene composition

Behind the welcome card, three visual layers build the island scene. They are rendered in document order inside #main-container and positioned with CSS:
ElementCSS strategyRole
#sandposition: fixed; bottom: 0; width: 100dvwFull-width SVG anchored to the bottom edge of the viewport
#group-treeposition: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 5Palm tree group centered horizontally, sitting just above the sand
#moonposition: absolute; top: 200px; right: 100pxCSS-drawn circle with a yellow glow, placed in the upper-right area
The moon is not an image file — it is a plain <div> styled entirely with CSS:
#moon {
    height: 50px;
    width: 50px;
    background-color: yellow;
    box-shadow: 0 0 12px rgb(255, 255, 0, 0.5);
    border-radius: 50%;
    position: absolute;
    top: 200px;
    right: 100px;
}
In dark mode the moon shifts to a muted grey tone and its glow follows suit, preserving the night-time mood without any JavaScript:
@media (prefers-color-scheme: dark) {
    #moon {
        background-color: rgb(60, 60, 60);
        box-shadow: 0 0 12px rgb(60, 60, 60, 0.5);
    }

    #group-tree { filter: brightness(0.4); }
    #sand { filter: brightness(0.2); }
}
100dvw and 100dvh (dynamic viewport units) are used instead of 100vw / 100vh so the layout accounts for retractable browser toolbars on iOS and Android. This prevents the sand strip from appearing to float above the bottom of the screen on mobile devices.

Audio element

The black noise track is declared as a plain HTML <audio> element at the very top of <body>, before any visual content:
<audio src="assets/black-noise.ogg" id="black-noise" autoplay loop></audio>
Three attributes do all the work:
  • src — points to a local .ogg file in the assets/ folder. Using .ogg (Vorbis) keeps the file size small while maintaining broad browser support across Chrome, Firefox, Safari, and mobile browsers.
  • autoplay — asks the browser to begin playback as soon as the audio is ready, without waiting for a user gesture.
  • loop — restarts the track the instant it finishes, creating an uninterrupted ambient backdrop with no audible gap.
Most modern browsers block autoplay for audio unless the user has previously interacted with the domain or the tab. If you open Zen in a fresh browser session, the audio may be silently blocked. The workaround built into the welcome screen is to enable the Controls checkbox — doing so registers a user interaction that satisfies the browser’s autoplay policy — then disable it again if you prefer a clean UI.
The #black-noise audio element is also positioned on screen (top: 20px, max-width: 90vw) so that when controls is true, the native browser audio bar appears neatly at the top of the page rather than in an unexpected location.

Build docs developers (and LLMs) love