← The library

Kuroa

One-page site for a creative studio

Full template Members only Open the demo Unlock the library
Preview of the Kuroa template: the white hero panel over two columns of fashion photographs, with the giant Kuroa Studio wordmark across the bottom.

What it is

A complete site for a creative studio: a hero panel that shrinks from full screen to a card while two columns of photographs scroll behind it, a portfolio with a pinned heading, a bento about the studio, a “vision” section where five sentences take turns across eight screens, a showreel mosaic that zooms out and settles, pricing, an FAQ, four testimonials that drop and stack, the numbers, a journal, a contact block and the footer. One page, one HTML file, 30 596 px tall.

What makes this template is that four of its sections are driven by the scroll position and none of them uses a library. A tall section holds a position: sticky screen; one scroll listener and one requestAnimationFrame turn the travel into a number between 0 and 1, and the stylesheet does the rest through custom properties — --panel-scale, --zoom, --open, --y. No animation-timeline, which only Chrome implements.

The design comes from a Framer template for a creative studio of which not one image remains: thirty-seven photographs were regenerated at the aspect ratios measured on the original, and the ten background videos — 14 MB — became one 596 kB reel, cut from those same stills. Every line of code was rewritten: the React bundle, the animation runtime, the smooth-scroll library, the editor badges.

That is the point: this template is not here to be admired, it is here to be read, broken and put back together.

What you change first

The colours — the accent is three shades

Right at the top of assets/css/style.css:

--ink:           #000;     /* text, buttons, the dark sections */
--paper:         #fff;     /* the page */
--paper-warm:    #f5f5f5;  /* portfolio and testimonials */
--night:         #141414;  /* hero backdrop, dark cards */
--accent:        #ff5a1f;  /* pastilles, large text — 3.11:1 on white */
--accent-strong: #d63f0c;  /* hover and small text — 4.62:1 on white */
--accent-light:  #ff8a5c;  /* accent over photography — 6.72:1 on #141414 */

The palette is monochrome and the accent only ever marks a state. But there are three accents, not one. The solid one passes on a white background at large sizes; as small text it would drop below 3:1, so the darker one takes over; over a photograph, the lightened one. Change the accent, change all three.

The name

Search for “Kuroa” in index.html: the logo, the title, the headings, the testimonials and the footer. No CSS class contains the brand — a find-and-replace cannot break anything. The studio mark is a conic gradient in CSS, repeated in assets/img/favicon.svg.

The sizes

The type scale is declared once, then re-declared at each breakpoint (1200 and 810 px, the ones the source design uses). A heading therefore never carries its own media query: change --display-size and the hero follows — 176 px on a large screen, then 92 and 75 px on the way down. The values are the ones measured on the original at 1440, 1024 and 390 px.

The scroll tracks

The four pinned sections are as long as they are tall: 300svh for the hero, 800svh for the vision, 400svh for the showreel, 500svh for the testimonials. Shorten them and the page gets shorter; nothing else moves.

The thirteen effects, and how they’re built

They were not watched, they were measured: the transform of each moving element was read at twelve scroll positions, which gives the law it follows rather than an impression of it. The hero panel, for instance, is a straight line from scale(1) to scale(0.4) across 2 700 px — and it keeps shrinking after the sticky screen lets go, which is why its progress is measured on the whole section and not on the pinned part.

EffectOriginal settingsHow
Shrinking hero panelscale 1 → 0.4, linear, over 2 700 pxsticky screen, --panel-scale written per frame
Photo columnstwo strips looping in opposite directionslist written twice, translateY(-50%) over 46 s
Progress barthin white bar at the bottom of the herowidth: calc(var(--p) * 100%)
Flipping partner logoseight tiles, staggered, rotating on the horizontal axisrotateX keyframes, 7 s, 0.45 s apart
Pinned portfolio headingsticky heading, four cases scrolling pastsame, position: sticky
Chat bubblescascade, loopingrevealed on entry, 0.45 s apart
Five sentencesone at a time over 7 200 px, letters staggered70 % of the track for the text, 30 % for the photo; 22 ms per letter
Opening photographwidth and opacity tied to the scrollwidth: calc(60% + var(--open) * 40%), radius closing as it opens
Service lines lighting upgrey → black as they passIntersectionObserver, 0.5 s
Settling showreelscale 3.25 → 1.1 and opacity 0.3 → 1, linear, over two thirds of the sectionidentical, --zoom and --fade
Falling testimonialsfour cards dropping, then stacked and tilted−120 % of their own height, rotations 0 / −2.5 / 3 / −1.5°, counter in role="status"
Flipping stat cardsfront and back on hoveropacity cross-fade, 0.6 s
Revealsfired on the first visible pixelcorrected: 15 % from the bottom of the screen, measured on the viewport

What you should know

No external request, no build step. Open index.html in a browser and it works. Self-hosted fonts, local photographs, no CDN, no tracker.

Without JavaScript, nothing is hidden. The hidden state is written .js [data-reveal], and enhance.js is the only thing that sets that class. Checked with JavaScript off: 12 headings out of 12 visible, 34 animated blocks out of 34 too, the five sentences of the vision section stack up and the four testimonials read one after another. The four pinned sections become ordinary sections — the page then measures 19 633 px instead of 30 596.

The reveal threshold is 15 % from the bottom of the screen, not the first visible pixel as in the original. The margin is measured on the viewport, not on the element — a threshold given as a share of the element would leave anything taller than the screen hidden for ever. A safety net reveals whatever is left when the bottom of the page is reached.

Four sections pin. A tall section holds a position: sticky screen, and one scroll listener writes the progress as a custom property. Which is why the root uses overflow-x: clip and not hidden — the latter would turn the root into a scroll container and unstick every one of them.

Under a strict security policy (script-src 'self'; style-src 'self'): zero violations, checked by serving the folder with the real header. No inline style, no inline script, no onclick — the progress values go through style.setProperty(), which the policy allows.

No horizontal overflow at 390, 1440 and 2560 px, on load and after a full scroll. Accessibility: skip link, aria-expanded kept truthful on the menu, native <dialog> for the menu and the reel, <details name="faq"> for the accordion, a single visible focus ring, and prefers-reduced-motion that zeroes the delays as well.

What isn’t perfect

The showreel is a montage of stills, not a film. The original played ten videos, 14 MB between them, which are not ours to redistribute. Six generated shots, pushed in slowly and cross-faded, make a 596 kB reel that is attached to the player on click — never at page load.

The animated logo is a conic gradient where the original plays a 2.4 MB video of a spinning iridescent sphere. Close enough to read as the same mark, for no weight at all.

The page is 30 596 px tall against 30 813 px for the reference (−0.7 %) at 1440 px. Section by section, the four pinned ones land to the pixel and eight of the twelve are within 2 %. The widest gap is the numbers section at −5.4 % (2 204 px against 2 329), where the original lets its cards breathe more. Nothing overlaps.

The original was a multi-page site — studio, work, news, contact, each with its own variants. Everything is folded into anchors on the single page rather than left pointing at 404s.

The newsletter form posts nowhere. It checks the address and says so plainly. Wire it to your own service.

A WebM would not have been lighter. Re-encoded at the size it is displayed: 658 kB in VP9 against 610 kB in H.264. Only the mp4 ships — serving both would hand the worse file to the browsers that prefer WebM.

It’s waiting for you in the library.

Kuroa comes with the subscription, alongside the rest of the library and the full course.

Immediate access · No commitment · Cancel in one click