Skip to content

Case study · Studio Ekatva

A motion-heavy studio site where every animation loads after the page does

An architecture and interior design studio working across architecture, interiors and landscape — from 2D floor plans to 3D renders and walkthroughs.

Shark Labs built and maintains the website for Studio Ekatva, an architecture and interior design studio in India. It is a statically rendered Next.js 16 site whose smooth scrolling, pinned project strip, custom cursor and hover-image navigation are GSAP behaviours loaded through dynamic imports, so none of them sit in the bundle the first paint waits on. All visitor-facing copy — brand, navigation, services, section text — is typed data in three files rather than a CMS.

Visit Studio Ekatva

3 files
Where every word on the site lives
0 static imports
Of GSAP or Swiper — every animation loads after mount
4 sources
Hero video variants — a phone never fetches the desktop file

The brief

Studio Ekatva designs buildings, interiors and the landscape around them, and its entire public presence was an Instagram grid. Instagram is a good place to be seen and a bad place to be judged: it crops the work square, it puts a competitor's post underneath yours, and it cannot tell anyone what the practice actually offers or how to commission it.

The site had to do the two things the grid can't. Carry architectural photography at the size the work deserves, with the kind of motion a design practice is judged on — and still be a site, with services, a studio page and a way to make contact.

Those two goals fight each other. Scroll smoothing, pinned sections, a custom cursor and an image layer that trails the pointer are exactly the things that make a page arrive slowly on a phone. Most of the engineering here is about refusing that trade.

No animation code in the bundle the first paint waits on

GSAP, its ScrollTrigger and ScrollSmoother plugins, and Swiper are the heaviest dependencies in the project. Not one of them is imported at the top of a module. Every single import is dynamic, inside the effect that needs it, so it is fetched after the component has mounted and the page has already painted.

useEffect(() => {
  const initPinnedStrip = async () => {
    const [{ gsap }, { ScrollTrigger }] = await Promise.all([
      import("gsap"),
      import("gsap/ScrollTrigger"),
    ]);
    if (isCancelled || !section.current) return;
    // …pin the section and translate the strip xPercent: -100
  };

  const isDesktop = window.innerWidth >= DESKTOP_MIN_WIDTH;
  (isDesktop ? initPinnedStrip() : initCarousel()).catch(() => {
    // An animation that fails to load must not take the section with it:
    // both markups are already in the DOM and readable without JavaScript.
  });
}, []);
The pinned project strip. The library arrives after mount, and a failure to load it leaves the section readable rather than blank.

The catch is the part that matters more than the split. Every one of these behaviours is decoration over markup that is already in the document, so a chunk that fails to arrive costs the visitor an effect, never the content. A studio's project photographs do not disappear because a CDN had a bad minute.

One breakpoint, and everything gated on it agrees

Three separate behaviours are desktop-only: the smooth scroller, the custom cursor, and the pinned horizontal project strip. All three are gated at 1024px, and each one is gated at 1024px for its own reason rather than by copying a number around.

  • Smooth scrolling is wrapped in gsap.matchMedia(), so the media query owns the teardown as well as the gate — the smoother cannot survive a resize down into mobile.
  • The custom cursor is gated by matchMedia to the same query as the stylesheet that hides the native pointer. If those two ever disagreed, desktop visitors would have no cursor at all — the CSS hides one the JavaScript never draws.
  • The projects section ships both markups and initialises exactly one: a looping Swiper carousel below the breakpoint, the pinned scroll-driven strip at and above it. Two presentations of one array, and only the one you can see costs anything to run.

ScrollSmoother doesn't scroll the page; it transforms the content inside a fixed wrapper. The browser's native jump to #services therefore lands in the wrong place — the document position it scrolls to no longer corresponds to where that section is drawn.

One delegated click handler intercepts in-page anchors, hands them to smoother.scrollTo with a rest position that matches the scroll-margin-top used below the breakpoint, and pushes the hash into history itself so the URL still updates and the link is still shareable. Below 1024px the handler finds no smoother and returns immediately, leaving the browser to do the job it already does correctly.

The hero paints before the video exists

A full-bleed background video is the obvious opening for a design studio and the obvious way to lose the largest paint. Here a poster image is rendered with priority and stays behind the video permanently, so the first meaningful paint is an image decode rather than a video negotiation.

The video source itself is chosen after mount from the viewport width across four breakpoints, which does two things at once: the server and the client render the same markup, so hydration has nothing to reconcile, and a phone never downloads the desktop-sized file. Nothing about the video is decided during server rendering, so nothing about it can block it.

Every word on the site is typed data

Content as typed modules, not a CMS

All visitor-facing copy on the Studio Ekatva site — brand, navigation, service list, section text, image alt text — lives in three TypeScript modules that the components read. Components hold no strings of their own, so a copy change is a data change and never a markup change.

The practical payoff is that one edit lands everywhere it belongs. Adding a service to services.ts makes it appear in the desktop menu panel and in the services section at the same moment, because both render the same array through the same component — the hover-image list is used twice and knows nothing about which context it is in.

FileHolds
site.tsBrand, logo paths, navigation, social links, contact details, canonical origin
services.tsThe five offerings, read by both the menu panel and the services section
home.tsHomepage section copy and imagery
Three files, and the rule for which one a string belongs in.

This is a real trade and worth naming. A CMS would let the studio edit copy without a developer; typed data does not. For a five-service practice that revises its wording a few times a year, a licence, a hosting bill, an upgrade path and an editor to train is a lot of machinery to carry — and the version history, review and rollback come free from the repository instead. If the studio starts publishing project write-ups weekly, that calculation changes, and the content layer is already the right shape to swap behind.

The same principle covers the details the studio hasn't supplied yet. The contact block renders each entry only once it has a value, so the site is complete rather than provisional in the meantime, and filling in a phone number is an edit rather than a build.

A logo that is half image and half text

The studio supplied one lockup file, black on white. A single script derives the mark, the light-ground lockup and the square app icon from it, so the brand assets are reproducible from the source the studio actually gave us rather than being three files someone once exported by hand.

In the header and footer only the isometric mark is a bitmap. The wordmark beside it is live text, which means it stays sharp at any size, takes its colour from currentColor so it works on the site's dark ground, is readable by search engines and screen readers, and scales from a single font-size per breakpoint instead of from a set of exported sizes.

What we're not claiming

There are no traffic figures, enquiry counts or conversion numbers on this page. The site is newly live, we weren't given them, and a case study with invented numbers is worth less than one without any — it just takes longer to find out.

What is true and checkable: the site is live, it is statically rendered, no animation library is in the bundle the first paint waits on, and every string a visitor can read is typed data in three files. When there are outcome figures we're allowed to publish, they'll be added here with the studio's name against them.

Built with

  • Next.js 16
  • React 19
  • TypeScript
  • GSAP
  • Swiper
  • Vercel
  • Biome
  • Bun

Keep reading

The services this drew on

Service

Web development

Fast, accessible, search-visible sites and web apps on Next.js — built so your team can edit them without calling us.

Service

Retainers

Monthly engineering after launch: monitoring, patching, incident response and new features, at a price published on the page.

Article

What a software maintenance retainer actually costs

Most agencies won't put a number on a retainer. Here are ours, the industry ranges we checked them against, and the arithmetic for deciding when hiring someone is cheaper.

Next step

Got something like this?

Tell us what you're running and what isn't working. The first call is free and usually narrows the problem more than it widens it.

Start a projectSee pricing