Web Development1 June 2026 · 6 min read

Behind the build: Norraform, a furniture e-comm template that respects the product

Furniture e-commerce sites usually pick a side: IKEA listing dump or art gallery without product. Norraform is the fourth template in a five-site marketplace — a quiet Scandinavian e-comm build that picked neither.

Behind the build: Norraform, a furniture e-comm template that respects the product

Furniture e-commerce is split between two failure modes. On one end, the IKEA-style listing grid — endless tiles, micro-photos, sort-by-price-ascending. On the other end, the design-gallery cosplay — full-bleed hero of a chair on a beach, no price, no add-to-cart for three scrolls. Norraform sits in the gap.

It's the fourth template I shipped this week. The constraints are the same as the other four: no build step, no Vite, no React-Router. Static HTML per page, React 18 UMD, Tailwind via CDN.

The PDP gallery bug

The product detail page (PDP) opens with a stacked gallery: one large hero image, a thumb row below, click a thumb to swap the hero. The component fades the hero from opacity: 0 to opacity: 1 on swap for a soft transition.

It didn't work. Every image stayed visible at once, stacked on top of each other, and the last-added one always won.

The cause was in the shared Img component — a thin wrapper around <img> that handles graceful fallbacks and a "blur-up" reveal:

style={{ opacity: loaded ? 1 : 0, transition: 'opacity 320ms' }}

The wrapper unconditionally set its own opacity to 1 once the image loaded, overriding the parent's opacity: 0 on the inactive slides. Every image in the gallery loaded → every image set itself to opacity 1 → every image was visible.

Fix:

style={{ opacity: loaded ? (style.opacity ?? 1) : 0 }}

The wrapper still owns the blur-up reveal, but if the parent passes an explicit opacity in the style prop, that wins. Two characters of code; one entire feature unbroken.

The hero

The hero is a full-bleed lifestyle photo (warm linen sofa, dried branch in a stoneware vase, soft afternoon light) under a calm overlay. The headline pairs a tight serif ("Built to last,") with an italic phrase ("made to love.") under a hand-drawn gold underline.

Above the headline: a small kicker — "Norraform — Est. Stockholm". Below: a short subtitle ("Furniture made from honest materials, designed to live with you for decades — not seasons. Quietly built in the North.") and two CTAs ("Shop the collection" filled, "Our story" minimal with arrow).

The hero rotates between three lifestyle photos via a slider dot row at the bottom. Each transition is a 700ms cross-fade — no slide, no scale. Cross-fade reads as a magazine spread, slide reads as a carousel ad.

Reveal animation

Originally a 1s rise-up with translateY(2rem) on every section. On desktop it was lovely. On mobile it read as: every section blinks like an eye as you scroll past. Cut to 0.45s and translateY(8px) — same intent, no twitch.

The animation patterns I kept

Marquee mask. The materials ticker at the top of the page is an infinite horizontal scroll. The naked ticker has a hard edge — words appear and disappear at the viewport border. A linear-gradient mask on the container fades the leftmost and rightmost 9% to transparent. Words materialize and dissolve at the edges instead of popping.

Lookbook hotspots. The lookbook page has pulsing dots on a wide lifestyle photo — tap one, a small product card slides up with the product name, price, and add-to-cart. The dot itself is a 1px center with a 6px outer ring that pulses to 12px and 0 opacity. The product card uses backdrop-blur and translate-y-3 opacity-0translate-y-0 opacity-100 on hover/tap.

Quickview modal. Click any product card on the shop page → quickview opens in a 450ms ease modal with opacity-0 translate-y-5 scale-[0.985]opacity-100 translate-y-0 scale-100. Backdrop blurs the page behind. Close on outside-click or escape.

What's the rest of the site

product.html, article.html, account.html, cart.html, checkout.html, wishlist.html, lookbook.html, about.html — every commerce surface a furniture catalog needs. None of it lives behind a SPA router. Each page is its own HTML file that mounts a React tree. Slower to navigate? By maybe 200ms. Easier to debug, deploy, and replace one page without touching the others? Yes.

Live

norraform-template-04.netlify.app — open the shop page, click a product, watch the quickview transition.

DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Senior full-stack developer shipping SaaS MVPs, web apps, and mobile apps 2× faster than agencies using AI-augmented workflows. Live portfolio: BookBed, Callidus, Pizzeria Bestek.