Quick Start
- Upload the whole folder to any static host. No build step.
- Open
index.htmland replace the studio name, nav links, copy, project names and footer with your own. - Edit the token block at the top of
assets/style.cssto reskin (see Palette). - Keep the three CDN
<script>tags andassets/script.jsbefore</body>.
The page works with no JavaScript and with reduced motion; see Accessibility.
Structure
The page is one document, section by section, each independent. Delete any section and the rest still runs, because every section is guarded in the script.
| Section | id | What it is |
|---|---|---|
| Nav | .nav |
Sticky bar, one accent action |
| Hero | #top [data-hero] |
Editorial statement in a masked serif, then a finished-room photograph |
| Selected work | #work |
Four drawn poster tiles |
| Before & after | #transform |
The signature slider, photographed rooms behind tabs |
| Approach | #approach |
Four stages plus a services list |
| Materials | .materials |
Five drawn material swatches |
| Testimonial | #studio |
One pulled quote |
| Enquire | #enquire |
Intro, contact details, a plain form |
| Footer | .foot |
Columns on the ink band |
The before/after slider
Each room is a [data-reveal-stage] holding two stacked photographs, a --before
(bare, cold, empty) and an --after (finished, warm, furnished), a divider, two tags,
and a visually hidden <input type="range">.
- One custom property drives everything. The range's
inputevent writes--pos(a percentage) onto the stage. The finished scene is clipped withclip-path: inset(0 0 0 var(--pos)), and the divider sits atleft: var(--pos), so the two always line up. Before is on the left of the divider, after on the right. - No pointer library. The range input is the drag surface. Keyboard, touch and
mouse all work for free;
touch-action: pan-yon the stage lets a vertical swipe scroll the page while a horizontal drag moves the divider. - The photos register because they are cover-cropped identically. Both
<img>scenes areobject-fit: coverin the same stage, so the before and after stay in the same frame at every width and the wipe reads as one room changing rather than a crossfade. For the effect to convince, shoot (or generate) the before and after from the same camera position and framing — only the decor should change.
Adding or changing a room
- Copy a
.room-panelblock. Give it a uniquedata-room-panelname, a matchingid, and point a new.room-tabat it with the samedata-room-tabname andaria-controls. Pairing is by name, not order, so you can reorder or delete freely. - Point its two
<img class="reveal-scene">tags at your own before/after photos (see Images). Use a matched pair (same room, same viewpoint) so the wipe lines up. - Give the new range a unique
idand its<label for="...">a unique target.
To change the resting split, set style="--pos:50%" on the stage and value="50" on
the range to the same number.
Per-section options
- Poster tiles (
.tile--a....tile--d): each tile's gradient is one rule in the CSS. Change the two colours, the name, the kind and the year. - Approach steps: plain
<li class="step" data-reveal>items; add or remove them, the grid reflows. - Material swatches: each chip is a
.swatch-chip--*gradient. Swap the gradient and the two lines of copy. - Reveal animation: any element with
data-revealfades and rises on scroll; add the attribute to opt a block in, remove it to keep it static. - Lenis smooth scroll: remove the Lenis
<script>tag and theLenisblock at the top ofinitStudio()to drop it. Everything else still works.
Palette
Three colours carry the whole site: a warm greige ground, near-black ink, and one
burnt-sienna accent. Everything else is one of those at another value. Every
translucent value is mixed from an --*-rgb triple, so a reskin is the token block at
the top of assets/style.css and nothing else.
Code snippet omitted: it ships with the download.
Measured contrast (as used, WCAG 2.1)
Every pair is measured composited over the surface it actually lands on.
| Text | On | Ratio | Note |
|---|---|---|---|
--ink |
--ground |
10.7:1 | body and headings |
--ink |
--cream |
12.8:1 | copy inside cards |
--ink |
--ground-deep |
9.5:1 | the alt band |
--dim |
--ground |
5.6:1 | secondary copy, labels |
--dim |
--cream |
6.7:1 | |
--dim |
--ground-deep |
5.0:1 | |
--link |
--ground |
5.4:1 | small inline links |
--link |
--cream |
6.4:1 | |
#FFFFFF |
--accent |
4.8:1 | solid-button label |
Burnt sienna --accent as text measures only 3.4:1 on the ground, so it is used for
type only at heading size (the accented hero line and the step numbers, all well
over 24px) or on non-text marks (the divider tag, the list bullets). Small inline links
use the darker --link instead. Lightening --ink past about #3A342E, or --dim
past #5A5049, drops body copy below the 4.5:1 floor.
To reskin, keep those relationships: pick a ground, measure your ink on it to at least
4.5:1 for body copy, and if your accent is lighter than a heading needs, keep a darkened
--link variant for inline text.
Accessibility
- The slider carries a real
<label>andaria-label, reportsaria-valuenowas it moves, and is fully keyboard operable (arrow keys, Home, End) because it is a native range input. - The room tabs are a proper tablist:
role="tab"/tabpanel,aria-selected, rovingtabindex, and arrow-key navigation. - No JavaScript: the tab strip is hidden and every room stacks as a readable
document. The slider renders at its 50% default (CSS holds
--pos: 50%), so both states of every room are visible. The hero and all copy show in place. Nothing is hidden behind a control that cannot work. - Reduced motion: a
prefers-reduced-motionmedia query and agsap.matchMediabranch both produce the complete static page. The load and scroll animations are skipped, the hero shows in place, and the before/after sits at the 50% split with both rooms readable. The slider still works, because it is an interaction, not an animation. - Contrast is measured as used; see Palette.
- Focus is always visible (a two-pixel accent outline).
Dependencies
- GSAP 3.15.0 core + ScrollTrigger (CDN)
- Lenis 1.3.17 smooth scroll (CDN, optional)
- Google Fonts: Cormorant Garamond + Golos Text
All loaded from CDNs in index.html. No build step, no bundler. The only local assets
are the bundled photographs in assets/img/ (see Images).
Images
The hero and the two before/after rooms use bundled, optimised WebP photographs in
assets/img/ (five files, well under 1 MB total). They are art-directed derivatives,
baked to the studio's warm grade, not raw originals.
- Swap a photo by replacing the matching file in
assets/img/, or, to keep the grade consistent, drop a new source intoassets/img-src/, updateassets/img-manifest.json, and re-runbun scripts/build-template-assets.ts interior-design-studio-template --force. - Before/after pairs must share a camera position and framing (same room, only the decor changes) or the wipe will not line up. Generate the "after" first, then derive the "before" from it so the geometry matches.
- Credits: the placeholder photography is AI-generated, so there is nothing to attribute. Replace it with your own project photos before you launch.
Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.