Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VivaOcean, a fictional deep-work suite whose site is one window held in open water, rendered entirely from this theme's tokens, scene, components and shell

A site that lives in one window, held in open water. Deep blue falling away below, sunbeams coming down through it, schools of fish drifting past behind the page. This is a shell archetype rather than a visual register: what the repository packages is the composition, and the register it wears is swappable. PostHog's current era is the defining commercial exemplar of the form, one centred chrome window over a scene with icon rails beside it, and this build adopts that composition and nothing else from it. Every shape in the water here is original work, drawn from scratch in this repository; none of the exemplar's artwork, characters or concepts are used or imitated.

The water is the point. It is CSS animation on inline SVG, it never stops, it costs a little over two percent of one core at idle, and it holds sixty frames a second at both viewports. The numbers are further down, with the control that makes them mean something.

The demo

The screenshot above is demo/index.html, the home page of VivaOcean, a fictional deep-work suite for teams. Depth is the product's metaphor and the site's ground at the same time: you start a dive at a depth, your team sees how deep you are on the Shoal, and Currents are the blocks where everyone goes down together. That is the honesty test for a scene this loud. A seascape behind a project-tracker would be a screensaver; behind a product whose whole vocabulary is depth, the ground is describing the thing.

Open it live, or clone the repo and open the file. There is no build step, no framework, no node_modules, and no server to start.

Nine pages: home, features, pricing, three documentation pages, about, changelog and contact. Every one of them is a real static HTML document. None of them declares a color of its own.

There is a second page worth opening: components/index.html renders the eight in-window components on an ordinary page with no shell and no water behind them, which is its own point.

The navigation architecture, because it is the interesting part

Every page is a real static document wearing the whole shell. Not a route, not a template, not a shell that fetches content into an empty frame. Nine documents, each with the scene, both rails, the window and its chrome already in it. So with JavaScript off, navigation is ordinary links and ordinary page loads, every URL is real, linkable and crawlable, and a deep link into the third documentation page arrives complete.

shell.js makes that faster without changing what it is. A click on a link to another page of the site fetches that page, lifts its window body out of the response, and puts it into the window already on screen. The water never reloads, so it never blinks, which is the entire reason the swap exists. The history entry is pushed, popstate is honored, the document title changes, aria-current moves, focus goes into the new content and a live region says what arrived, because none of those things happen by themselves when the browser is not the one navigating.

And it hands the click back the moment it cannot. Over file:// a fetch is blocked, so the script checks the protocol once at boot and does not install the interceptor at all: from disk, the demo is the same site by ordinary page loads. Any other failure, a 404 or a bad response, falls through to location.href. The fallback is not a degraded mode, it is the site.

Both modes are verified rather than asserted. With scripts off: all nine pages loaded, every rail link and every menu link clicked, every in-window link followed, aria-current correct on each arrival. With scripts on: four swaps, a four-step back walk plus a forward, two more swaps driven from the menu bar, a load counter proving zero document loads across the whole journey, and a cold deep link into docs-shoal.html. The full walk is in the pull request.

The window closes, which is how you get to look at the thing

The same site with its window closed: open water from edge to edge, both icon rails still in it, and one plate at the top left reading Open VivaOcean

The title bar has a close control, and closing puts the shell in its second state: desk-clear, the scene with nothing in front of it. At 1024 and up the rails stay exactly where they were, because they are drawn on the water rather than on the window. Below it there are no rails to keep, and the launcher at the top left is the whole surface.

This is the one interaction on the theme that is about the theme. Everything else here is a site being a site over a nice background; this is the background being the point, and a repository whose stated thesis is that the ground is the product should be able to show you the ground.

Three things bring the window back, and each means what its own surface has always meant.

  • A rail icon opens the window at that page. Not a new behavior invented for the state — it is what a rail item has always done. The only difference is that the window it lands in was not on the screen a moment ago. Verified per icon rather than in general: seven closes, seven clicks, seven assertions that the window came back around the right page with aria-current moved and focus inside it.
  • The launcher opens the window at home, because it is a launcher and a launcher opens an application at its front door.
  • Anything that delivers content opens the window, including Back pressed while the desk is clear. Content arriving into something the reader cannot see is the one outcome none of these paths is allowed to produce, so the rule lives in the function that applies content rather than at each caller.

There is no minimize, and the close control is the reason to say so rather than the reason to stop. Minimize means "keep this window, put it somewhere I can get it back from, in the state I left it", and that sentence needs a restore surface to be true. This shell has no taskbar and no window list, because it has one window and a list of one is a label. Without that surface, minimize and close would leave the reader on the same screen and be undone by the same gesture: two controls, one behavior, which is a dead control wearing a second icon.

That argument has a consequence and the consequence is shipped rather than hidden. The launcher deliberately does not restore the page you closed — it opens at home, and a reopened window is at the top of its page rather than wherever it was scrolled to. A control that gave you back exactly what you had would be the minimize this shell just argued its way out of.

Closing costs nothing and proves it. Every path back is measured with a document-load counter, including the one the swap navigator declines to handle — a rail icon naming the page you are already on, which without a branch for it would reload the whole document and take the ocean down with it. Zero loads, on every path, at both viewports, and over file:// as well as HTTP.

And the reopened window is the same window, at the pixel. The matrix renders home, closes it, opens it again from the launcher, and asserts that the result hashes identically to a cold page load. It does the same for a rail icon opening the window at Pricing against a cold pricing.html. Two states that ought to be indistinguishable, proved indistinguishable, rather than inspected and believed.

The launcher

It is a first-class control, and the reason that phrase is doing work here is the 390 column of the accounting below: at phone widths, on a clear desk, it is the only control on the screen. So it is first in the tab order, it takes focus the moment the window closes, it clears the 44px target floor, and its label says what will happen.

That it is the only control is a ruling rather than an accident, and here is the ruling. At 1024 and up the desk keeps the rails, so closing the window leaves seven ways back plus the launcher. Below 1024 there are no rails, and the question is whether the desk should carry a second control — a hamburger that opens the rails panel over the water — or whether the launcher should be the whole surface. The launcher is the whole surface, and the hamburger's function is folded into it: not by the launcher doing the hamburger's job, but by the launcher restoring the chrome the hamburger lives in. The rejected alternative loses three times over. The panel is positioned from the title bar, so on a desk with no title bar it would need geometry invented for one state. It would put two controls on a state whose name is desk-clear. And it would rebuild, on the desk, a piece of the chrome the reader has just closed — which is the move class decision 46 names when it says an empty state must not re-create the navigation it lost. What the reader gets instead is one press, the window back at home, and the whole of the shell's navigation on the screen with it.

It is drawn as a piece of the window rather than as a button on the water — a plate in the window's own surface carrying the title bar's own accent hairline, with a bubble mark, because bubbles are what this ocean would use for something on its way up. The alternative concept, ink and a glyph drawn straight onto the water in the rails' visual language, is argued and rejected in the pull request, and the measurement is what decides it: under the lagoon re-skin this repository ships as a failure, ground ink on the lightest water falls to 1.76:1 and the rail labels are gone, while this label is still at 15.63:1, because an opaque plate does not care what is behind it.

The copy went through the same test. "Start Here" is the onboarding register and it is not true: this state is only ever reached by closing something, so starting is the one thing the reader did not do. "Surface" is register-true and funny and nobody can predict what it does. "Open VivaOcean" names the application and the outcome, and the name is read from the brand link in the title bar rather than written into the script, so a consuming site gets its own.

Two ways through, on purpose

The rails are the metaphor's navigation, and they are the part people will like. They are also icons in the water down the sides of the screen, which is a thing some readers will not read as navigation at all.

So there is a second way, and it is deliberately the most ordinary thing in the repository: a menu bar in the window chrome, directly under the title bar, carrying every page as a plain text link. Register-true, because that is where an application window has kept one for forty years, and specifically not a site navbar floating above the scene, which would put a second undiegetic layer over the water and break the composition the theme exists for.

It carries nine links where the rails carry seven. The rails are curated into two named groups and leave the two documentation sub-pages to be found inside the docs page; a guarantee layer that skipped two of nine pages would not be one.

It is not an enhancement and it is not scoped under the live class. They are plain anchors to real documents, so they behave identically with the script and without it.

It is one line, at every width, and below 1024 it scrolls sideways. Nine links are 529px of content in a 390px window, so wrapping there is not a risk that might arrive with a tenth page, it is what happens today, and it costs three rows of a phone's chrome. One scrolling row costs 35px and stays 35px however long the site gets. That is class decision 45's shape for a chrome strip carrying a list that grows with the site, and this repository owes that rule two notes, both in shell.css: the focus-ring reservation is not owed here because the ring is drawn inside the control at a negative offset, which is the log's stated retro-desktop exception arriving by a second route; and the rule's permission to wrap in the no-script document is deliberately declined, because being identical in both modes is the whole design of this element.

The scrollbar is hidden and the scrolling is not. A classic scrollbar is 15px of a 35px row and reads as a defect under nine words; what tells you the row continues is that it is cut mid-item at 390 rather than at a tidy boundary. The matrix carries a strip-scrolled-narrow cell so "it scrolls" is a hash rather than a claim.

This is WCAG 2.4.5 Multiple Ways satisfied on purpose rather than by luck: the metaphor is the delight layer and it is never the only path.

Which is a claim about a state, not about a repository

A shell is a machine with states, and a navigation surface that lives in chrome is only there in the states where that chrome is. Adding a second state made that worth measuring instead of assuming, so it was measured: every state, both viewports, counting anchors that are rendered, carry visible text, and contain no <svg>. Rendered means it has boxes and is not under visibility: hidden, which matters more than it sounds — without the second half the audit passes surfaces that were deliberately hidden.

State 1280 390
window-open menu bar, 9 of 9 pages menu bar, 9 of 9 pages
desk-clear none in-state; the launcher returns you to window-open none in-state; the launcher, and it is the only control
no-JS menu bar, 9 of 9 pages menu bar, 9 of 9 pages

The 390 column used to read "none", in both modes, and that is why this section exists. The menu bar was display: none below 1024, on the stated reasoning that the menu button was "the same guarantee at that width". It was not, in two ways at once: the button opens a panel of seven icon-and-label links, which decision 44 counts as an icon surface, and it is missing two of the nine pages. So this shell had no text-link navigation surface at all on a phone, in either mode, while its compliance was assessed at 1280 and reported for the whole repository. A per-state audit exists to stop exactly that sentence.

That second half is worth stating on its own, because neither surface makes it obvious: the rails carry seven of the nine pages, at every width, in both modes. docs-dives.html and docs-shoal.html are on no rail anywhere; they are reached from the menu bar and from inside the docs page. At 1280 that is a curation choice and it costs nothing, because the rails are ambient and the strip is right there above the content. On a phone it made the hidden strip the only complete index the shell had, so hiding it did not remove a redundant convenience — it removed two pages from the only surface that reached all nine.

Both surfaces stay at 390, and the button that opens the second one was renamed. The rails could have been dropped once the strip carried all nine: they are a seven-of-nine subset in icon form, and a render of the open panel shows it covering the complete text index with an incomplete picture of it. They stay anyway, because this is the same redundancy the shell already ships at 1280, where the rails and the menu bar sit side by side and that coexistence is decision 44. Deleting the metaphor's navigation at the width most readers use, because a safety net arrived, would leave a phone with the safety net and no theme. What the render did settle is the label: a button called Menu directly above a visible menu bar is a question the reader has to answer before either surface works, so it is now Browse by section, which names what the rails have that the strip does not.

The desk-clear cells are a different kind of empty. A state with no chrome in it cannot inherit a guarantee that lives in chrome, and pretending otherwise by rebuilding a menu on the desk would be re-creating the thing the reader just closed. What that state owes instead is one first-class affordance back to a state where the guarantee exists, which is the launcher, and which is why every requirement in the section above is load-bearing rather than polish. That rule is class decision 46.

From a clear desk on a phone, every page is now two taps and no page is more. Walked, nine times, each attempt from a genuine 390 desk-clear entered from another page: home is one tap on the launcher, and the other eight are the launcher and then a link in the strip. The two documentation sub-pages used to be four — launcher, hamburger, docs, then a link in the prose — because they are the two the rails do not carry.

Position map

A visual style is a set of coordinates, not a mood. This theme sits at one point in the creative direction framework, which sets brand direction on four axes. Here is where it lands and what each choice pays for.

Axis Position What the position buys
Tone register Conversational The demo talks like a person with an opinion and no slogan. The wit is in the argument rather than in the jokes, which is what stops a site this atmospheric from tipping into a mood board.
Aesthetic philosophy Polished Standard Under the water it is an ordinary, well-made document: a title bar, a content region, a comparison table with real rows. Nobody has to learn anything, which is what frees the ground to be beautiful.
Audience relationship Peer The composition goes unexplained. Rail items get names, not a tutorial, and the reader is assumed to have used a website before.
Sensory ambition Resonant The scene is engineered to produce one specific feeling, calm, and it is the reason to open the page at all.

Those four position names are the exact strings the framework uses.

The sensory position moved, and it should be argued rather than announced. The desktop pilot in this class sits at Considered and says so carefully: its metaphor is structure rather than atmosphere, there is no wallpaper photograph and no boot sequence, and reduced motion collapses its one animation. Every one of those sentences is false here. This repository's ground is a moving picture whose only job is how it makes you feel, it runs from the first frame, and the reduced-motion state is not an absence of the effect but a composed version of it. The framework's own test for Resonant is whether the reader is meant to carry a feeling away from the page; that is precisely what the water is for.

What keeps it from being melodrama, which is the failure mode the position warns about: everything is slow. The fastest thing in the scene takes fifty-four seconds to cross the frame. Nothing pulses, nothing greets you, nothing waits for you to notice it. The engineering is meant to be invisible, and the way you can tell it is trying to be is that the register has exactly one warm color in it and spends it on the focus ring.

The tone register also moved, from the pilot's Playful to Conversational, and for a reason worth naming: the pilot's shell is the joke, and a site that answers "where is pricing" with a window called Pricing is being funny on purpose. This one is not being funny. A calm ocean and a comic voice would be two registers fighting, and the water would lose.

Quick start

Clone once, then pick the path that matches what you came for. This repository has three separable things in it, so the three grabs are stated separately.

git clone --depth 1 https://github.com/rampstackco/vivaocean-theme

Grab the whole thing. Tokens, water, components, chrome, behavior. Your content goes in the vo-window-body, your navigation in vo-rail-item anchors; the demo is the reference markup and every page of it has the identical shell region.

<link rel="stylesheet" href="/styles/tokens/tokens.css" />
<link rel="stylesheet" href="/styles/scene/scene.css" />
<link rel="stylesheet" href="/styles/components/components.css" />
<link rel="stylesheet" href="/styles/shell/shell.css" />
<script src="/styles/shell/shell.js" defer></script>

Grab the water only. tokens/ and scene/ are self-contained. Drop the scene markup into any page, add class="vo-water" to the body and data-scene="ocean", and you have the ocean behind whatever you already had. Nothing in scene/ knows the shell exists.

<link rel="stylesheet" href="/styles/tokens/tokens.css" />
<link rel="stylesheet" href="/styles/scene/scene.css" />

Grab the register only. Skip shell/ and scene/ and you have a cool, calm, light theme for ordinary pages: tokens plus eight components, no window and no water. This is the grab where this repo behaves exactly like the register themes in the collection.

<link rel="stylesheet" href="/styles/tokens/tokens.css" />
<link rel="stylesheet" href="/styles/components/components.css" />

Tailwind v4. One import. theme.css pulls in tokens.css and maps it onto Tailwind's theme namespaces, so you get bg-vo-water-deep, shadow-vo-window, rounded-vo-window, h-vo-titlebar. The shell, the scene and the script are plain CSS and JS either way; the adapter covers the tokens.

@import "tailwindcss";
@import "./styles/tokens/theme.css";

Tailwind v3. Load the tokens in your stylesheet, then register the preset.

@import "./styles/tokens/tokens.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
// tailwind.config.js
module.exports = {
  presets: [require("./styles/tokens/preset.js")],
  content: ["./src/**/*.{html,js,jsx,ts,tsx}"],
};

What the ocean costs

A ground that animates forever owes you a number, so here are the numbers, measured on this machine over ten seconds of an idle page with nothing else happening. Both states are measured, because the desk-clear number is not derivable from the window-open one — closing the window hands the scene the part of the screen the window was covering, which is 59% of it at 1280 and 92% of it at 390.

window-open 1280 desk-clear 1280 window-open 390 desk-clear 390
Ground actually on screen 41% 100% 8% 100%
Frames in 10s 601 600–601 601 601
p95 frame time 16.7ms 16.7–16.8ms 16.7–16.8ms 16.7–16.8ms
Layouts / recalcs in 10s 601 / 601 601–602 / 601–602 601 / 601 601–602 / 601–602
Main-thread task time 0.317–0.326s 0.323–0.397s 0.284–0.322s 0.309–0.343s
The ocean's own share 2.3–2.5% 2.4–3.1% 2.1–2.4% 2.3–2.6%

The guarantee strip costs nothing here and it is worth saying why rather than only that. It animates nothing — document.getAnimations() filtered to the strip's subtree returns zero in every cell — and it is static chrome, so it does not participate in a per-frame anything. The four animations, one per scene layer, are the same four in every row of the table above, and the idle numbers are inside the run-to-run spread they were in before the strip existed.

Only transform is animated. Four animations, one per layer, in both states, and the keyframes touch nothing but transform. No opacity churn, no filters, no layout properties, no canvas, no script running per frame.

The control is what makes the number mean something. The same page with the water switched to calm, so there is no scene at all, spends 0.076s to 0.089s of main-thread time over the same ten seconds, which is what the measuring loop itself costs; the ocean's own share is that subtracted out. Ranges are quoted rather than single figures because that is the run-to-run spread on this machine, and rounding it away would be inventing precision.

The interesting number is the one that barely moves. Between the two states the painted area of the animation goes up 2.4 times at 1280 and 12.5 times at 390, and the cost goes up by about a tenth to a quarter. The layout and recalculation counts do not move at all: 601 of each per ten seconds in every cell. That is the earlier finding stated from the other end — this is not a compositor animation, it costs one style recalculation and one layout per frame, and neither of those depends on how much of the result is visible. Only rasterisation scales with area, and rasterisation is not on this thread.

One claim narrowed. This section used to say "not one dropped frame at either size, in any run". Across four measured runs, two cells showed frames over 20ms — one frame in desk-clear at 1280 in one run, four in window-open at 1280 in another. Different cells each time, and the cell with no window in front of the ocean was the clean one in the run where the other was not, which makes it this machine rather than this state. The honest claim is the p95, which is a frame budget met in every cell of every run.

One honest correction to the obvious claim. This is not a compositor animation. Chromium does not promote an animated SVG group the way it promotes an HTML element, so the scene costs one style recalculation and one layout per frame, and will-change: transform was measured and removed because it bought nothing and would have set a trap for the next person who wanted a position: fixed element inside the scene. The animation is cheap; it is cheap on the main thread, and calling it GPU-cheap would have been a nicer sentence and a false one.

Reduced motion is not a slower version of this. It is zero animations, zero recalculations, zero layouts, and a composed still life.

Accessibility over a moving background

Text on top of something that moves is the hard problem this theme has, and it is solved by construction rather than by hoping.

  • The scene has a measured ceiling. The lightest paint the water can ever produce is a sunbeam core over the deep fish fill. Ground ink on it measures 4.94:1, so a rail label clears AA over every frame of the animation, including the one where a beam is crossing it. The arithmetic and the two construction rules that keep it true are in tokens.css and scene/scene.css.
  • The focus ring is two colors, and it has to be. The same rail item sits over lit surface water at the top of the screen and near-black abyss at the bottom, and no single color clears 3:1 against both. So the ring is a warm light band with a deep halo drawn tight around it, and the guarantee is that at least one of the two clears the 3:1 floor against every paint in the theme. Eight backdrops measured, no column without a pass; the table is in tokens.css.
  • Page ink and ground ink are separate tokens and are not interchangeable. The element that proves it is the rail, which appears on water at 1280 and on a light panel at 390 and changes ink, hover direction and current-page marker when it moves between them.
  • The full Tab cycle was walked and recorded, in both states, at both viewports, in both modes, by listing every rendered focusable element in document order rather than by counting keystrokes. Window-open at 1280, 25 stops: skip link, seven rail items, the brand, the water switch, the close control, nine menu-bar links, the content region, four in-content links. Window-open at 390, 19: skip link, brand, the section button, water switch, close, nine menu-bar links, content region, four links. Desk-clear at 1280, 8: the launcher and the seven rail items. Desk-clear at 390, 1. With no script, 22 at both viewports. No trap anywhere, and every surface that leaves the tab order does it by leaving the render tree rather than by ARIA bookkeeping.
  • The strip is before the window content in the keyboard order at both widths, which is the order the markup already had: it is window chrome, and chrome comes before the thing it frames.
  • Two of those numbers are corrections to the previous record, which said twenty-six at 1280 and eleven at 390. Re-counting the shipped build the same way gives twenty-four and nine, so the figures were two high at both viewports and the extra two are not reproducible. Counted honestly, this branch adds one stop at 1280 (the close control) and ten at 390 (the close control and the nine strip links). The method is stated here so the count is checkable rather than remembered.
  • The two navigation surfaces never disagree about where you are. Both carry aria-current in the static markup, and after a swap the script rewrites both in one pass from the URL rather than remembering. On the two documentation sub-pages the rails correctly mark nothing, because they do not carry those pages; the walk asserts that count rather than assuming it.
  • Focus is managed at the moments it can be lost, and the rule is one sentence: focus follows the thing that appeared. A swap moves focus into the new content. Closing the menu returns focus to the button that opened it. Closing the window moves focus to the launcher, which is what appeared and, at 390, is the only thing on the screen. Opening the window moves focus into the window, by the same call the swap uses, so the two cannot drift apart. Nothing else moves focus and nothing is ever trapped.
  • The ring on the launcher is earned the way the others are. Closing with the mouse moves focus to it and draws nothing; closing with Enter moves focus to it and draws the ring. Both directions are asserted against :focus-visible rather than inferred, and the ring is the rails' two-colour ring rather than the window's, because the launcher stands on the water and that is what the two-colour ring is for.
  • The ring on the content region is earned, not decorative. That element is genuinely user-focusable, because it is the scroll region and a keyboard reader has to be able to scroll it, so it draws a ring, and :focus-visible means a keyboard activation shows it and a mouse click does not. Both directions are in the recorded walk.
  • Escape closes the menu if the menu is open, and does nothing otherwise, in both states. The arrival of a close button did not change that. The menu panel is the only transient thing this shell has; the window is not transient, it is the site, and closing it is a labelled control with a name on it rather than a key readers press to get out of things. On a clear desk there is nothing left to dismiss, so Escape is a deliberate no-op rather than an unhandled one, and the walk asserts it as one.
  • The menu panel cannot be open on a clear desk, and that is structural rather than checked. The only control that opens it is the menu button, the menu button lives in the title bar, and the title bar is inside the element the desk-clear state hides. There is no path to the panel that does not go through a control that is not on the screen.
  • All of this ARIA is added by the script at enhance time. The static document has aria-current on one rail item, which is true with no script, and nothing else. The tray is empty in the markup; the menu button, the water switch, the close control, the launcher and the live region are all injected, because a control that does nothing must not exist — and with no script there is no close, so there is no state for a launcher to be the way out of. The nine demo documents are byte-identical to the ones the previous release shipped, which is the strongest available form of that claim: the no-JS reading did not change because nothing in it changed. Counted the same way, a no-script document has the same 22 tab stops at 1280 and 13 at 390 as it did before. There is no aria-modal anywhere and no role that describes behavior the page does not have.
  • prefers-reduced-motion gets the composed still life. prefers-contrast: more gets the calm two-stop field with the scene layer removed entirely, and that is verified at the pixel: the contrast cell hashes identically to the calm cell at both viewports, in both shell states.
  • The chrome is the height the token says, now including its border. --vo-titlebar-height is a min-height on a border-box element, so the bar is 44px until a 44px control goes in it — and WCAG 2.5.8 puts one there. Measured on the longest-titled page at 1280, 390 and 320, the title bar is 45px: 44 of content and its 1px bottom border. The mobile menu panel is positioned by subtracting that height from the viewport, and it was subtracting 44, so it was landing one pixel high and covering the border it hangs from. Both sums now add --vo-border-width and the panel's top sits exactly on the bar's bottom edge. One pixel, invisible, and worth fixing because it is precisely the promise class decision 45 says to check: a calc() that subtracts a chrome token is a claim that the chrome is that tall, and this one was not, before this branch existed.

Where the reasoning lives

tokens/tokens.css is the single source of truth. Every literal value in the theme appears there exactly once, and there is not one color literal anywhere else in the repository, which is grepped rather than claimed. Every ratio in its comments is measured. The water is a token group, the shell geometry is a token group, and the scene's four durations are tokens, which is what makes both the re-skin and the reduced-motion behavior one-file concerns.

scene/scene.css draws the ocean from those tokens and is the whole implementation: a depth gradient, five sunbeams built out of three flat quads each so the lateral edge is soft without a blur filter, and three schools on three clocks in two directions. It explains why the loop distance is exactly one view box and not a token, and why the mid school takes the light fill even though it is the middle depth.

shell/shell.css and shell/shell.js draw the window, both rails, the menu and the desk, and run the swap. The shell is deliberately small: one window, no pile, no cascade, no z-order, no minimize, no maximize, and no decorative dots imitating window controls. The pull requests argue each of those deletions instead of assuming them, and argue the one addition — a close — on the same terms.

CUSTOMIZE.md documents retheming as axis moves rather than as a color picker. Its first worked move re-skins the whole ocean to midnight in ten measured tokens, and it ships a second capture of a re-skin that fails, because the failure is the more useful picture: it is a pretty tropical lagoon in which the rail labels measure 1.76:1.

Consuming this from a Claude skill. The design-standards skill asks for a project's design tokens as a required input and offers to define a working set when none exist. Point it at tokens/tokens.css instead. The file covers every category the skill asks for, in the order it asks, and the contrast ratios are already in the comments.

Adjacency: this is not glassmorphism-theme

glassmorphism-theme is a register: frosted translucent surfaces any site can wear. This repo is a shell, and it makes the opposite call about the same temptation. The obvious move for a window floating in water is to frost it, and this window is opaque on purpose, because a translucent window makes every contrast ratio in the theme a function of whatever happens to be drifting behind it that second. If you want the glass, go there. If you want the water, you are in the right repo.

The other shells

This repo is the fifth of a class. All of them ship the same anatomy: a top-level shell/ directory, one root enhancement class, ARIA and controls injected at enhance time, shell geometry as a token group, and a ground axis on the body. They differ in the machine.

  • desktop-os-theme is the pilot, and this repo is the showcase strain of the same class. The pilot is a window manager: multiple windows, cascade placement, raise on click, a taskbar that tracks what is open, on a ground that is a still life. This one keeps a single window and spends everything the pile would have cost on a ground that moves. Its docs/class-decisions.md is the class-decision log both repos file into.
  • retro-desktop-theme is that shell at the other temperature, wearing 1995: beveled silver chrome on teal, a Programs menu, an era-correct window open.
  • phone-launcher-theme is one app at a time, whose native state is 390 and whose desktop is a presented device on a stage.
  • game-console-ui-theme is a board you drive from across the room, with arrow-key spatial navigation and type sized for ten feet.

The class-decision log for all of them lives in the pilot: docs/class-decisions.md. Forty-six numbered decisions with the repository and pull request that filed each one. That count was stale in this file by ten, which is what happens when a number lives in two places; it is now the only count here and the log is the authority for it. This repository's departures from those decisions, and the new items it files, are in its pull requests.

License and questions

MIT. See LICENSE. Use it commercially, fork it, rename the tokens, ship it. No attribution required.

Issues and pull requests are welcome here. For questions, ideas, and anything conversational, use the discussions on the claude-skills repo, which is where all discussion for these repos lives.

About

VivaOcean: a single-window shell theme over a living seascape. Fish schools, light rays, rails to a hamburger, nine-page SaaS demo, keyboard-first. Tailwind and shadcn tokens.

Topics

Resources

Security policy

Stars

1 star

Watchers

1 watching

Forks

Contributors

Languages