Skip to content
All work
  • #web
  • #design-system
  • #webgl
  • #i18n

leafranger.dev

This site: a bilingual portfolio in Next.js 16 with its own design system, a particle field that draws my dragon as a low-poly mesh, and accessibility and SEO checked by tests on every change.

Website
Role
Solo designer and developer: the design system, the code and the copy in both languages.
Outcome
Lighthouse 100 for accessibility, best practices and SEO on mobile, with no layout shift. Every page in both languages is checked in CI: status, SEO tags, structured data, axe in both themes, phone width.
Year
2026
Status
in progress
Stack
  • Next.js 16
  • React 19
  • TypeScript
  • Tailwind 4
  • three.js
  • React Three Fiber
  • Radix
  • MDX
  • Playwright
  • Vercel
  1. The start

    A first version that did the job

    The first version of this site went up in June 2025, with a page for my Roblox work and one for my side projects. It did the job, but I wanted more from it: a site that told how each project went instead of just listing it, and one that looked like mine.

  2. The goal

    One site for three kinds of visitor

    Clients looking for a bot or an automation, Roblox studios looking for sound, interfaces or scripts, and employers reviewing my work: all of them should find what they need and a clear way to write to me. In Italian and in English, with the same substance. And it had to feel like mine, with my dragon and my colours, not like a template.

  3. The adventure

    Rebuilt from the ground up

    I rebuilt it piece by piece. First a visual language with its own rules: purple for anything you can press, lime for what's live, nearly square corners, and every button, menu and player designed once and reused everywhere. Then every page in two languages, the case studies you can read by tabs, and the Roblox pages where every sound plays and every interface opens up.

    The Roblox page, made for studios: my name as a wall of bricks you can brush and blast.

    The part I'm proudest of is the dragon on the home page. It's made of particles that leave the background and gather into my avatar, then turn to follow your pointer. Click the background and it bursts apart in slow motion and puts itself back together.

  4. The end

    A site that checks itself

    It loads fast, works with a keyboard and a screen reader, and stays calm for anyone who prefers less motion. Every time I change something, every page in both languages is checked automatically before it goes live. It's still in progress: the redesign ships page by page, and this case study grows with it.

Lighthouse accessibility, best practices and SEO
100
Lighthouse accessibility, best practices and SEO
Languages, same substance
2
Languages, same substance
Layout shift while loading
0
Layout shift while loading

What's next

  • Even faster

    The effects take some loading before the first screen appears on a phone. Loading more of them later is the next step.

  • More of my own media

    Real screenshots and clips for every project, and my own 3D models.

  • Its own design system

    Colour roles (purple to press, lime for what is on), fluid type tokens, three corner radii and a UI kit of buttons, forms, menus, media and dialogs, all shown in a lab page.

  • A dragon made of particles

    The home page draws my avatar as a low-poly mesh that turns toward the pointer, with a click that blows it apart and puts it back together.

  • Two languages, one site

    Every page in Italian and English. Switching language keeps the page running and sweeps the new text in behind a lime scan line.

  • Accessible by default

    Real headings and text behind every effect, a visible focus ring, a skip link, contrast checked in both themes, and a still version for reduced motion.

  • Works without the effects

    The 3D desk on the About page has a list of buttons beside it, so it works by keyboard and without WebGL, where the list is the whole thing.

  • Consent done properly

    Nothing from Google loads before a choice. Decline and Allow are the same size, and Cookie settings in the footer changes it at any time.

  • Readable by search and AI

    Canonical and hreflang tags, share images built at build time, one Person in the structured data on every page, and llms.txt files generated from the project data.

  • Checked on every change

    CI runs lint, the type check, a production build and smoke tests across every page in the sitemap.

Two themes

Dark is the default and light is one click away, revealed as a circle that grows from the toggle. The light theme isn't the dark one inverted: panels lift on a soft shadow, field outlines are darker so they stay visible, and lime text switches to a deeper shade, because the bright one is too pale to read on white.

Roblox, discipline by discipline

Every discipline has its own page, with its workspace as the backdrop and every game as a panel of sounds, images, videos or scripts.

The Roblox page links to one page per discipline. Each game opens as a panel, grouped the way the work was made, with players for every sound and a lightbox for every image. A link can point straight at a game or a group, and a Commission me button follows you once the title scrolls away.

Work you can filter

The topics shared by at least two projects become filters. The choice is saved in the address, so a link like the home page's "See this kind of work" opens the list already filtered.

On a phone

On phones the dragon gets its own band under the header and the copy starts below it, so nothing covers it. The buttons go full width, the navigation becomes a full-screen menu, and the language switch closes it so the scan line plays over the page.

When a page doesn't exist

Unknown paths stay inside the site: the particles draw the 404, and both exits are one click away.

The stack

  • Framework

    Next.js 16, React 19

    App Router with Turbopack, server components by default, and Next 16's proxy for the locale redirect. Deployed on Vercel, with API routes for the parts that need a server secret.

  • Styling

    Tailwind 4

    CSS-first configuration: the colour, type, radius and easing tokens are CSS variables in one design-system stylesheet, switched by the theme class.

  • 3D

    three.js, React Three Fiber

    The Roblox voxel hero and the About page's desk, lazy-loaded, paused when off screen, and replaced by a poster without WebGL2.

  • Content

    MDX

    Case studies and game pages are MDX per language, with blocks like the journey, stats and challenges usable without imports.

The particle field

Two canvases sit behind the page: the main layer with the plexus and the hero shape, and a far layer with three blurred depth layers.

  1. Sample

    A Web Worker reads the avatar's pixels into outline, glasses, teeth, mouth, tongue and fill points.
  2. Mesh

    The points are triangulated with Delaunator; only triangles on ink with short sides are kept.
  3. Recruit

    While the hero is on screen, background particles leave home and land on the shape's points.
  4. Shade

    Facets are batched by colour and lit with a sheen that turns with the dragon toward the pointer.

Every background particle has a home from a low-discrepancy sequence, so any prefix of the list is evenly spread: recruiting from the end leaves the rest of the field still even. The depth layers move with 1/z parallax, and their blur is drawn with gaussian sprites rather than CSS filters.

Languages

Every page lives under /en or /it. A path without a locale gets a 307 to the language saved in a cookie, else the best match in the browser's languages, else English. Interface strings live in two dictionaries, and the Italian one is typed against the English one, so a missing key fails the type check. Long copy is MDX per language.

There is one root layout above the locales, so a language switch is a soft navigation: the particle field, the cursor and the sound keep running, and a named view transition pairs the old and new page under the scan line.

Challenges

Problem

A blocked main thread

Sampling the dragon took about 850 ms on a mid-range phone, frozen in the middle of the first visit.

Solution

The sampling moved to a Web Worker. Its launcher lives outside the shapes module, because a worker URL inside the module the worker imports sent Turbopack's build into a loop.

Problem

Blur that cost the frame rate

Three CSS-blurred canvases ran at 3-4 fps in software rendering.

Solution

The blur is drawn instead: dots are gaussian sprites and links a wide faint stroke under a thin one, on a canvas at 0.6 resolution.

Problem

A late consent banner

Rendered after hydration, the banner became the largest paint on every first visit.

Solution

It ships in the server HTML, hidden by CSS until an inline script, run before first paint, marks the page as asking.

Problem

A hydration mismatch

With the proxy in front, the pathname could differ between the server HTML and hydration, and the header's current-page marker didn't match.

Solution

The header reads its route from the rendered layout segments instead, which are part of the tree, so server and client always agree.

Quality checks

  • Smoke tests

    Playwright

    Every page in the sitemap: status, one h1, canonical, hreflang, share image, structured data with the Person, no console errors, no sideways scrolling at phone width.

  • Accessibility

    axe

    The key pages in both themes, with no serious or critical violations, plus the consent flow, the localized 404 and reduced motion.

  • Security

    Headers

    A content security policy and the other security headers on every route, and API routes that validate their input and only serve an allowlist.

  • CI

    GitHub Actions

    Lint, type check, a production build and the smoke tests on every push.

Is the source code public?

No, the repository is private. I'm happy to walk through any part of it on a call.

Does it track me?

Only if you allow analytics. Until then nothing from Google loads, and the privacy policy lists every service your browser talks to.

Why is the plexus only on the home page?

It's the site's signature, but behind reading content it was too much. Every other page sits on a solid background.

What happens without WebGL or with reduced motion?

The 3D scenes fall back to a poster or a list of buttons, and with reduced motion the particles stand still, the shapes snap into place and the intro is skipped.

Can you build something like this for me?

Yes. Tell me what it should do and when you need it, on the contact page.

Need something like this?

Tell me what it should do, where it runs and when you need it.

Get in touch