What the owner needed
Stitch Bloom is a small accessories business that makes bags, sleeves and smaller pieces from recycled T-shirt yarn. Sustainability is part of the product and the way the owner talks about the business. The site needed to show the work properly, explain the material choices and give new customers a useful introduction to the brand.
The owner had already looked through several website templates. They chose Moss & Stone as the complete reference they wanted me to follow. Having one selected design gave me a clear standard for type, spacing, product photography, controls and motion.
The order process was equally specific. The owner did not want to manage a stock counter after every sale, and availability often needs checking before somebody pays for a handmade item. A normal payment gateway would have created an extra system to maintain while still leaving room for incorrect stock. The agreed flow lets a customer browse the catalogue, keep products in a bag and send the order by email. The owner confirms availability, timing and payment directly.
This was one of my earlier client shops. It became a useful lesson in responsive composition and in fitting software to a small business without giving the owner routine administrative work they had already said they did not want.
Studying Moss & Stone
I could inspect the published Moss & Stone site, though I could not duplicate its Framer project into my account. The browser became the working reference. I used screenshots for broad comparisons, then inspected the live DOM, computed styles, element bounds and transitions at several viewport widths.
The first implementation had inconsistent system-level details. Some controls were rounded, section spacing varied and the product carousel only resembled the original at a glance. I went back through the reference with measurements. I opened its menu, search and cart, added products, resized the viewport and recorded what changed.
The Stitch Bloom variables came out of that pass. Cream #F7F2EA carries most page backgrounds. Terracotta #C4714A is used for primary actions, tags and hover states. Deep brown #2C1A0E handles dark surfaces and active controls. Cormorant Garamond is the display face and Inter is used for utility copy. The wordmark also uses the supplied Boiling Bold and Silver South faces at larger widths, then switches to the logo asset on a small screen.
Spacing follows a 16 pixel base. Wide content is capped at 1400 pixels and shares the same page gutter, which aligns the navbar, hero, grids and footer. The visible controls use square 50 pixel cells with thin outlines. That dimension appears in the segmented navbar, carousel buttons, product actions, quantity controls and the cart close button.
The navbar is a fixed four-part bar for the menu, wordmark, search and bag. The menu panel and search panel attach directly below it, using the same outer edges and border colour. Search matches both product and collection names, focuses its input when opened and closes with the Escape key. The attached panels were important to the Moss & Stone feel. A detached popover would have changed the shape of the whole header interaction.
The same construction appears in the arrow buttons. Two identical arrows sit inside a clipped square. On hover, one exits through an edge while the other enters from the opposite side. Diagonal actions travel from the lower left to the upper right. Carousel controls use horizontal travel. Keeping that motion in a shared button stylesheet stopped each page from acquiring a slightly different arrow.
Rebuilding the diagonal carousel
The main visual feature is the product carousel in the home hero. The active card sits in the centre. Its neighbours step up to the right and down to the left, with a smaller scale and lower opacity. The diagonal remains visible on mobile, where the cards are scaled and repositioned to fit the available width.
At a 1440 pixel viewport, the reference gave me these target measurements.
active card 350 × 574 px
step +390 px across and -100 px up
side scale 0.85
side opacity 0.5
controls 50 × 50 px
I converted the fixed measurements into ratios based on the width of the active card.
const cardW = Math.round(Math.max(180, Math.min(cw * 0.5, 350)))
const cardH = Math.round(cardW * 1.64)
const xStep = Math.round(cardW * 1.114)
const yStep = Math.round(cardW * 0.286)
cw comes from a ResizeObserver on the carousel container. The calculation therefore reacts to the space assigned by its parent as well as a full window resize. On narrow screens the card width can fall to 180 pixels. The diagonal offset scales with it.
Five Najma products run through the hero. Five is enough to expose a common loop error. A plain subtraction of the active index can send a card all the way across the stage when the carousel moves from the last item back to the first. I wrap the relative index around the shortest side of the ring. Only the active card and the two nearest cards on either side are mounted.
The carousel advances every four seconds. A click, an Enter key press on a side card, or either navigation control selects a new item and restarts that interval. The active image links to its product page. The stage height includes both ends of the diagonal and another 72 pixels for the controls, so the upper and lower cards are not clipped.
Mobile required its own measured composition. The heading, support copy, carousel, product description and actions stack around an absolutely positioned diagonal stage. The parent measures the support copy and places the upper neighbouring card 16 pixels below it. It then grows the hero to the measured bottom of the carousel content. The composition stays recognisably the same while its surrounding copy moves into a vertical reading order.
The bug after client-side navigation
The carousel looked correct after a hard refresh and shifted after navigating back to the home page within the app. That made it easy to miss during testing if I always opened the home route first.
The original layout code used getBoundingClientRect() to find the bottom of the copy above the carousel. That copy has a reveal animation built with a CSS transform. A hard load usually measured after the transform had settled. A client-side route change could measure while the copy was still moving, then keep a position that stopped being true a few hundred milliseconds later.
The layout needed the static document position. I replaced the transformed rectangle with an offset walk through the offsetParent chain.
const getStaticOffset = (element, parent) => {
let top = 0
let current = element
while (current && current !== parent && current !== document.body) {
top += current.offsetTop
current = current.offsetParent
}
return { top, bottom: top + element.offsetHeight }
}
The measurement waits for document.fonts.ready, since Cormorant Garamond and its fallback have different metrics. It then waits for one animation frame so the carousel cards have been painted. The same calculation now runs with settled type and static offsets after a hard load, an in-app route change and a resize.
One catalogue for the whole site
Product photography arrived in stages. I moved the catalogue into products.json so the site would not need a component edit for every new product or photograph. The current file contains eight products across the Najma Collection, Accessories and Gadget Sleeves.
Each record can hold its name, price, measurements, weight, description, available colours, customisation status and badge. Images follow a prefix and count convention. A record with and an image count of two resolves to and . Adding another photograph normally means adding the file and increasing the count.