Back to Case Files
BNR-CF-05Restaurant · Motion Web·Hospitality·Sousse, Tunisia·2026

Zitouna

A French-language site for a contemporary Tunisian restaurant in the medina of Sousse, built on Next.js 16 and GSAP, with a pinned horizontal signature gallery and a five-language switcher including Arabic RTL.

Visit live site
Zitouna — Restaurant · Motion Web homepage built by BreakNRoot

05

Locales

07

Routes

02

API routes

Zitouna is a contemporary Tunisian table at 14 rue de Paris, inside the medina of Sousse. The brief was a showcase site that carries the weight of the room and the produce behind the cooking: the fish auction, the central market, the charcoal, the plating. The site is authored in French as its primary language, with the full interface and every piece of dish, season and service copy also carried in English, Arabic, Russian and German.

The build is a Next.js 16 App Router project on React 19 and Tailwind CSS v4, animated entirely in 2D with GSAP. There is no WebGL layer. The scroll surface is a Lenis instance driven from the GSAP ticker, so smooth scrolling and every ScrollTrigger read off the same clock rather than fighting each other. The signature dishes live in a pinned section that converts vertical scroll into a horizontal rail, with each panel running its own containerAnimation-scoped parallax on the photo and its ghost numeral. Below it, a scrubbed vertical timeline fills a saffron rail while each step's photo unmasks with a directional clip-path sweep.

Every ornament on the page is hand-authored SVG rather than an image asset: a tiled zellige field used as a background motif, a zellige divider and diamond bullet, an olive branch that parallaxes in the hero, a stylised clickable map of Sousse recoloured to the house palette that deep-links into Google Maps directions, plus per-dish and per-season line illustrations. The whole motion system is gated behind prefers-reduced-motion, at the component level and again in globals.css, so a reduced-motion visitor gets a correctly framed static site instead of a degraded animated one.

// challenge

The Challenge

A restaurant in a UNESCO-listed medina sells a room and a provenance story as much as a menu, and neither survives a conventional card grid. The site had to move like a walk through the souk while staying a fast, indexable, bookable page for a mixed audience of local diners and international visitors, which meant the same content had to render in French, English, Arabic, Russian and German with correct text direction. On top of that, a restaurant menu is structured data that changes: five categories, a full à la carte list, five signature plates and four seasonal tasting menus each with its own accent colour and course sequence, all of which had to be modelled once and reused across routes, metadata and JSON-LD.

// solution

What We Built

Everything is driven from typed data modules. Dishes, categories, signatures, seasons, services, photos and site facts are declared once in src/data, each translatable string held as a Localized record and resolved at render through a pick() helper against the active locale. A client LanguageProvider holds the locale in React context, persists it to localStorage, and writes lang and dir onto the document element so Arabic flips the whole layout to RTL. The four seasonal menus are statically generated from the same data via generateStaticParams, each page tinting its zellige field, header glow and iconography from that season's accent hex. Reservation and contact both post to Next.js route handlers that re-validate the payload server side, reject past dates and Mondays when the house is closed, cap the party size, and return a ZIT-prefixed booking reference the form then animates into a confirmation state. The homepage emits Restaurant JSON-LD assembled from the same hours table the footer renders, alongside a generated sitemap that includes every season route and a robots policy that keeps the API paths out of the index.

// outcome

The Outcome

The site is live on Vercel and ships six primary routes plus four statically generated seasonal menu pages, a custom 404, a generated sitemap and robots policy, and Restaurant structured data derived from the real opening hours. The kitchen can move a dish, reprice a season or add a service by editing a single typed data file, and the translation for every locale sits inline next to the French source, so no string can drift out of sync between languages. Accessibility was treated as part of the motion work rather than a pass afterwards: a skip link, an in-house lightbox with focus trapping, Escape and arrow-key navigation and scroll locking, aria-pressed filter controls with a live count, and a complete reduced-motion path that disables Lenis and every ScrollTrigger while leaving the layout intact.

Engineering log
[01]

Pinned horizontal signature gallery

src/components/home/SignatureGallery.tsx converts vertical scroll into a horizontal rail via a pinned ScrollTrigger, then hangs per-panel triggers off that tween with containerAnimation so each plate's artwork, over-zoomed photo and ghost numeral parallax independently as the panel crosses the viewport. A gsap.matchMedia swaps to a staggered vertical stack below 1024px and to a static layout under reduced motion.

[02]

Scrubbed medina-to-plate timeline

src/components/home/RecipeTimeline.tsx scrubs a saffron rail to full height as the list passes, lights each diamond marker with a back-ease colour change on a reversible toggleActions trigger, and unmasks each step photo with a clip-path sweep whose direction follows the alternating left and right sides of the rail.

[03]

Five-language runtime i18n with Arabic RTL

src/i18n holds a locale config, a nested dictionary of roughly two thousand lines covering French, English, Arabic, Russian and German, and a context provider that persists the choice to localStorage and sets document lang and dir so Arabic renders right to left. Data-level strings use a Localized type resolved by pick(), with French as the guaranteed fallback.

[04]

Validated reservation flow with booking reference

src/components/forms/ReservationForm.tsx validates client side, focuses the first offending field, and posts to src/app/api/reservation/route.ts, which independently re-checks the phone pattern, date format, past dates, the Monday closure, service period, time slot membership and a party size between one and twelve before returning a ZIT-dated reference that the form animates into a confirmation panel.

[05]

Seasonal menus with per-season theming

src/data/seasons.ts models four tasting menus with their own slug, months, price, pairing, motif and accent hex. src/app/notre-carte-des-saisons/[slug]/page.tsx statically generates each one through generateStaticParams, tints the zellige field, header glow and icons from that accent, builds its own metadata, and wires circular previous and next navigation between seasons.

[06]

Filterable menu explorer

src/components/menu/MenuExplorer.tsx renders the à la carte list across five categories with a sticky filter bar, a vegetarian-only toggle, signature, vegetarian and spicy tags exposed as aria labels, and a live-region result count. Rows and category banners reveal through ScrollTrigger.batch, re-run on every filter change.

Stack
Framework
Next.js 16React 19TypeScript
Motion
GSAPScrollTriggerSplitText@gsap/reactLenis
Styling & infra
Tailwind CSS v4Lucide ReactVercel
See it in production

Live in production

Zitouna is running in production today. Open it, then tell us what you want built — BreakNRoot engineers systems like this one from first prototype to production.

Adjacent records
>_ BREAKNROOT//SYSTEM.ONLINE