Greater than a yr of rebuilding, refining, and rethinking to get it good. What went stay on March 23, 2026 was not one clear thought from begin to end, however the results of pushing the work till the visible path, movement, construction, and identification lastly clicked. Constructed round the concept that doubt is dear, the portfolio was formed to make the usual apparent early, so the dialog can transfer straight to suit, scope, and execution.
Numerous late nights for one thing that needed to really feel proper
I began designing younger and went by extra names than I’d prefer to admit. Falconcept, Paramor, and lots of different instructions all bought shut in their very own approach, however none of them totally carried the presence I used to be after. They both felt too secure, too indifferent, or an excessive amount of like I used to be hiding behind one thing that didn’t actually match my model.
R—Okay was the primary path that felt totally private whereas nonetheless carrying sufficient weight to talk to each manufacturers and companies. Getting there was messy. Numerous late nights, juggling different work, remodeling layouts, typography, pacing, and model language again and again, and loads of moments of considering **** this, this nonetheless isn’t it. However that was the method. I saved pushing till it stopped feeling like separate concepts and eventually began to really feel like one factor.
Someplace in all that remodeling, R—Okay stopped feeling like one other path and began feeling just like the path. The identification bought sharper, the layouts extra deliberate, and the entire thing lastly started to hold the presence I had been after for a very long time. It began to really feel extra resonant, extra refined, much less sure by what a portfolio is meant to appear to be, and far nearer to the sort of readability that solely comes from actually understanding what the work must be. That first impression mattered lots, which is strictly what formed the hero.
Technical Overview
Beneath that sits my very own framework, developed from native Shopper-First by Finsweet. I additionally used Osmo Provide’s scaling system and Barba boilerplate as a base, then folded each into my very own setup so I can preserve constructing on it in future initiatives.
The location could be very visible, however the factor that retains it manageable is construction. I exploit attributes for principally all the things. Each JavaScript file begins with notes explaining precisely which attributes it depends on, so I at all times know what talks to what. That issues whenever you work in Webflow and preserve touching HTML/CSS. I by no means need some random visible edit to quietly break logic some place else.
Right here is an effective instance of that, the construction and choices are outlined straight within the HTML, whereas the code reads them and wires up the behaviour:
const MENU = {
wrap: "[data-menu-wrap]",
toggle: "[data-menu-toggle]",
panel: "[data-menu-panel]",
gadgets: "[data-mi]"
}
const wrap = doc.querySelector(MENU.wrap)
const toggle = doc.querySelector(MENU.toggle)
const panel = doc.querySelector(MENU.panel)
const gadgets = panel?.querySelectorAll(MENU.gadgets)
const config = "0.7",
closeDuration: wrap?.getAttribute("data-menu-close-duration")
Constructing the middle stage of the web site, the Unicorn Studio visible
The hero needed to do lots, quick. It needed to pull folks in earlier than the location actually explains something. The distorted portrait, the typography, the pacing, and the WebGL background all needed to work collectively with none one in every of them overpowering the remaining.
For the background, I used Unicorn Studio, a device to create WebGL movement and interplay in minutes, with none code. The aim was to get the proper visible weight and environment with out making the construct extra advanced than it wanted to be. I self-host the Unicorn venture file and mount it into the web page utilizing bunny.web
const SELECTOR = "[data-us-project], [data-us-project-src]";
const KEY_ATTR = "data-us-key";
async perform createSceneForElement(el, key) {
ensureElementId(el, key);
const cfg = buildSceneConfigFromElement(el);
const scene = await window.UnicornStudio.addScene(cfg);
sceneMap.set(key, scene);
mountedElementMap.set(key, el);
requestAnimationFrame(() => {
if (scene && typeof scene.resize === "perform") scene.resize();
});
return scene;
}
That alone is just not what makes the hero work, although. What makes it work is the way it sits inside all the things else. It isn’t some free visible on the high of the web page. It’s a part of the location’s rhythm from the primary second on.
What retains all of it managed
Numerous the location comes all the way down to grid. I exploit a 12-column structure all through, which supplies me sufficient construction to push compositions round whereas retaining all the things managed. For many full-screen sections, I often suppose in six rows on a 100vh part, which helps place issues with intention with out making the layouts really feel stiff.
That carries throughout the entire website. The homepage stays clear whereas nonetheless feeling authored, the venture pages lean into presentation, and the perception pages really feel extra editorial, nearer to spreads than normal weblog posts. The aim was to make every web page really feel associated, however not repetitive.
To maintain the hero steady on cell, I added a viewport-height helper. Cellular browsers consistently change the seen viewport when you scroll, which may make 100vh-based layouts resize unexpectedly. The helper writes a steadier worth to –vh which I then use in CSS for screen-based sizing.
perform setVh() {
const h = window.visualViewport?.top ?? window.innerHeight
const px = h * 0.01
if (minVhPx === null) minVhPx = px
minVhPx = Math.min(minVhPx, px)
doc.documentElement.fashion.setProperty("--vh", `${minVhPx}px`)
}
In follow, meaning I can outline screen-based sizing in CSS like this:
--vh: 1vh;
--near-screen-height: calc(var(--vh) * 80);
--full-screen-height: calc(var(--vh) * 100);
--overflow-screen-height: calc(var(--vh) * 120);
--double-screen-height: calc(var(--vh) * 200);
Sound, however solely if you would like it
Sound was one thing I needed within the website pretty early, as a result of it provides a bit extra weight to transitions, hover states, and menu interactions. On the identical time, I understand how annoying it’s when a website simply begins taking part in music at you for no motive, so I saved it totally optionally available. Nothing begins on first click on. You need to allow it your self.
That gave me one of the best of each worlds: an additional layer for individuals who need it, with out forcing it into the expertise for everybody else.
perform bindSoundTargets(root = doc) {
root.querySelectorAll("[data-sound-hover]").forEach(bindHoverElement);
root.querySelectorAll("[data-sound-click]").forEach(bindClickElement);
root.querySelectorAll('[data-sound="mute"]').forEach(bindMuteElement);
}
perform bindHoverElement(el) el._rkHoverBound) return;
if (!el.hasAttribute("data-sound-hover")) return;
el.addEventListener("pointerenter", () => startHoverTimer(el));
el.addEventListener("pointerleave", () => clearHoverTimer(el));
el._rkHoverBound = true;
perform bindClickElement(el) {
if (!el || el._rkClickBound) return;
if (!el.hasAttribute("data-sound-click")) return;
el.addEventListener("click on", () => {
const soundName = el.getAttribute("data-sound-click");
playNamedSound(el, soundName);
});
el._rkClickBound = true;
}
Then the mute logic makes positive the entire thing stays user-controlled relatively than automated:
perform toggleMute() {
const wasUnlocked = audioUnlocked
if (!audioUnlocked) {
unlockAudio()
}
if (!wasUnlocked && audioUnlocked) {
isMuted = false
Howler.mute(false)
playBGM({ fromStart: !bgmEverStarted })
syncMuteUI(doc)
emitState()
return
}
isMuted = !isMuted
Howler.mute(isMuted)
if (isMuted) pauseBGM()
else playBGM({ fromStart: false })
syncMuteUI(doc)
emitState()
}
Preloader to set the stage
I didn’t need the preloader to really feel separate from the location. It needed to already really feel like a part of the expertise. As an alternative of simply overlaying the web page whereas issues load, it makes use of an SVG clip path with an even-odd cutout, which lets me carve a gap out of a full-screen layer and develop that opening over time. That cutout turns into the reveal itself.
The timing is cut up into distinct phases. First, the emblem masks reveal in with a parallax ease. From there, the cutout timeline takes over, regrouping the emblem, resizing the wrappers, after which beginning the precise cutout sequence. The cutout begins as a small sq. within the centre, expands right into a wider rectangle, after which settles right into a full-screen opening. These steps are timed at 0.3s, 1.1s, and 0.8s, all utilizing the identical parallax ease, with a slight overlap between the rectangle and full-screen section so the movement by no means feels too segmented.
On the identical time, the Unicorn scene is triggered simply after the preliminary emblem reveal, so it has sufficient time to get up beneath the masks earlier than the opening totally clears. That mixture is what makes the preloader really feel much less like a loading state and extra like a part of the hero itself.
perform buildEvenOddPath(w, h, holeW, holeH) {
const outer = `M0 0H${w}V${h}H0Z`;
if (!(holeW > 0 && holeH > 0)) return outer;
const x = (w - holeW) * 0.5;
const y = (h - holeH) * 0.5;
const internal = `M${x} ${y}H${x + holeW}V${y + holeH}H${x}Z`;
return `${outer}${internal}`;
}
This leads to:
+----------------------+
| |
| +--------+ |
| | gap | |
| +--------+ |
| |
+----------------------+
Barba web page transitions that tie all of it collectively
The transitions took a silly quantity of iteration. At one level, I had a a lot heavier WebGL model the place the web page virtually peeled up and rotated away. It sounded nice in idea, however in follow it was an excessive amount of — too heavy, too awkward, and too simple to overdo.
I ultimately moved to a clip-path based mostly transition as a substitute. It nonetheless gave me the sensation I needed, nevertheless it was cleaner, simpler to tune, and far simpler to sync with all the things else occurring on the web page.
That shift mattered, as a result of the transitions in R—Okay are usually not simply there to brighten navigation. They carry rhythm by the location. They make the entire thing really feel like one steady world as a substitute of a stack of disconnected screens. The textual content reveals, masks, and enter animations all needed to land on the proper second — not simply vaguely after the brand new web page appeared.
const CLIP_1 = "polygon(0% 100%, 100% 40%, 100% 100%, 0% 100%)"
const CLIP_2 = "polygon(0% 60%, 100% 0%, 100% 100%, 0% 100%)"
const CLIP_3 = "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)"
tl.set(subsequent, { clipPath: CLIP_1, webkitClipPath: CLIP_1 }, "startEnter")
tl.to(subsequent, {
clipPath: CLIP_2,
webkitClipPath: CLIP_2,
length: 0.22,
ease: "parallax"
}, "startEnter")
tl.to(subsequent, {
clipPath: CLIP_3,
webkitClipPath: CLIP_3,
length: 0.76,
ease: "parallax"
}, "startEnter+=0.22")
Insights that really feel like actual journal spreads
This didn’t require a lot code. It was extra about taking part in with structure, composition, and pacing, however I needed the Insights to really feel like precise journal spreads relatively than normal articles. The aim was to make each really feel like a designed editorial web page, as should you have been transferring from unfold to unfold, not simply scrolling by content material.
Smaller particulars matter
Numerous what makes R—Okay really feel like R—Okay lives in smaller selections. The dock as a substitute of a typical navbar. The best way it disappears across the hero and footer. The best way the footer nonetheless feels alive as a substitute of merely changing into the top of the web page. The best way attributes preserve all the things structured beneath, so I can preserve pushing the visuals with out the entire thing changing into fragile.
One in all my favorite smaller touches is the animated swords within the footer. Not as a result of they’re some enormous technical flex, however as a result of they are saying lots about how I take into consideration ending a website. The footer ought to nonetheless really feel authored.
gsap.set(L, { transformOrigin: "0% 100%" })
gsap.set(R, { transformOrigin: "100% 100%" })
struggle.to(R, { x: 1.05, y: 0.1, rotation: 4.5, length: 0.4 }, 0)
struggle.to(L, { x: 0.15, y: -0.05, rotation: 1.2, length: 0.4 }, 0)
struggle.to(R, { x: -1.85, y: -0.55, rotation: -8, length: 0.34 }, ">-0.05")
struggle.to(L, { x: -0.55, y: 0.2, rotation: 7, length: 0.34 }, "<")
It’s a small factor, nevertheless it provides the footer a little bit of life as a substitute of constructing it really feel like the hassle stopped there.
Closing ideas
None of those items carry the location on their very own. Not the WebGL. Not the transitions. Not the preloader. Not the sound. Not the smaller particulars. The work was in getting them to help one another with out letting the entire thing collapse into noise.
R—Okay was by no means constructed to show what number of results I might match right into a portfolio. It was constructed to really feel thought-about from the primary body to the final — sharp, memorable, and clear sufficient to set the usual instantly.
Constructed for presence. Not quantity.


