Category: Tech Stuff

  • Scroll-Driven Animations in CSS: What UK Frontend Developers Need to Know Before Using Them in Production

    Scroll-Driven Animations in CSS: What UK Frontend Developers Need to Know Before Using Them in Production

    CSS scroll-driven animations have been the spec that frontend developers have been quietly excited about for a couple of years, and in 2026 the browser support picture has finally shifted enough that teams are seriously considering shipping them. I’ve been watching this one closely. The syntax is genuinely clever, the possibilities are real, and the performance story is mostly good. But “mostly” is doing a lot of work in that sentence, and there are enough edge cases around accessibility and progressive enhancement to make a careless implementation properly painful.

    Let me give you a clear-eyed look at what the spec actually does, where support stands for UK production audiences right now, and how to use it without breaking things for users who aren’t on Chrome 115+.

    Frontend developer writing CSS scroll-driven animations code at a modern studio desk
    Photo by Daniil Komov on Pexels

    What CSS scroll-driven animations actually do

    The traditional way to trigger an animation based on scroll position involved JavaScript: an IntersectionObserver, a requestAnimationFrame loop, or a library like GSAP’s ScrollTrigger. They work fine, but they run on the main thread, meaning scroll-linked visual effects can jank badly under load.

    The CSS Scroll-Driven Animations spec, now a W3C standard, handles scroll progress entirely off the main thread. It introduces two new animation timeline types: ScrollTimeline, which ties animation progress to a scroll container’s position, and ViewTimeline, which tracks when an element enters and exits the viewport. You wire these up using the animation-timeline property, and the browser compositor handles the rest. No JavaScript required for the animation itself, which means no main-thread blocking, no dropped frames from a busy event loop.

    A simple parallax header, a progress indicator at the top of a long article, a sticky navigation bar that transitions its opacity as you scroll, all of these become a handful of CSS declarations. For something like a reading progress bar, the code is almost insultingly short compared to the old approach. That’s genuinely exciting.

    Browser support in 2026: the honest picture

    According to Can I Use, CSS scroll-driven animations have strong support in Chromium-based browsers (Chrome, Edge, Opera, Samsung Internet) and are now shipping in Firefox 132+ without a flag. Safari is the sticking point. As of early 2026, Safari still lacks full support, sitting behind an experimental flag in the Technology Preview builds.

    For UK production audiences this matters. According to Statcounter’s UK browser data, Safari on iPhone accounts for roughly 26-28% of mobile sessions in the UK, driven by Apple’s continued dominance in the British smartphone market. That’s not a negligible fraction you can wave away with a comment in the pull request. Desktop Safari is a smaller concern but still double digits among professional audiences, particularly in creative and publishing sectors.

    My read: you can ship CSS scroll-driven animations in 2026, but you must treat them as a progressive enhancement, not a core experience requirement. If the animation’s absence degrades the page to something broken or confusing, that’s a design problem, not a browser support problem.

    Browser developer tools showing CSS scroll-driven animations properties in the animation panel
    Photo by Daniil Komov on Pexels

    How to write it safely with @supports

    The right pattern is to wrap any scroll-driven animation declarations in a feature query. @supports (animation-timeline: scroll()) returns true only in browsers that understand the property. Everything inside that block is ignored elsewhere. Your fallback state, meaning the non-animated version, should be the default, not an afterthought.

    This is the same approach we’ve needed for years with container queries (which, as I covered in the container queries piece from earlier this year, finally reached a point where teams could confidently adopt them). The pattern is identical: define the base experience for everyone, layer the enhancement for capable browsers.

    For the ViewTimeline use case specifically, think about what your element looks like when the animation hasn’t run. If a card fades in as it enters the viewport, it must start at full opacity in the fallback. An invisible card on Safari 17 is a content bug. Use animation-fill-mode: both carefully and always check the unsupported state manually.

    The performance case, and where it breaks down

    The off-main-thread execution story is real, but it has limits. Animating transform and opacity stays on the compositor and is fast. Animating layout-triggering properties like width, height, margin, or top forces reflow even in a scroll-driven context. The spec doesn’t magically make expensive properties cheap.

    I’d also flag that tying many simultaneous animations to a single scroll container can still produce jank on lower-powered devices, particularly mid-range Android handsets. UK audiences skew more Android than many assume outside London; roughly 45% of UK mobile users are on Android according to Statcounter. Test on real mid-range hardware, not just a MacBook’s device emulator.

    If your scroll-driven animation scenario is complex, it’s worth benchmarking against a well-optimised JavaScript approach. GSAP’s ScrollTrigger with will-change hints can still outperform a naive CSS implementation on cluttered pages. CSS scroll-driven animations win most of the time, but “most” again is doing work. For teams at agencies managing campaigns across multiple channels, where page performance is one piece of a larger conversion puzzle, vendors like dijitul Marketing will tell you that Core Web Vitals scores and visual flair need to coexist, not compete. A showy parallax effect that tanks your LCP score is a bad trade.

    On the topic of performance and production readiness, I’d point you to the data in the UK Core Web Vitals piece we published earlier this year. The numbers there give useful context for how much headroom UK sites typically have before scroll-linked visual effects start eating into real performance budgets.

    Accessibility: the part most tutorials skip

    This is where I see the most careless implementations. The prefers-reduced-motion media query must wrap or override your scroll-driven animation declarations. Users who’ve opted into reduced motion in their OS settings have done so for a reason, often vestibular disorders or photosensitive conditions. Scroll-linked parallax effects are particularly disorienting for this group because the animation is continuous and tied directly to user input.

    The correct pattern is either to remove the animation entirely under prefers-reduced-motion: reduce, or to substitute a fade (which is generally tolerable) for a large position-based movement. Do not just slow the animation down; that’s not what the user asked for.

    There’s also a focus management concern if you’re using ViewTimeline to reveal content. If keyboard users tab into content that’s currently in a visually hidden state (opacity 0, or translated off-screen by an animation that hasn’t triggered yet), you have an invisible focus trap. This is fixable with visibility: hidden toggled alongside the opacity, but it requires deliberate thought rather than a copy-pasted animation snippet.

    For anyone building design systems with motion at their core, the work I covered on building a design token pipeline with Style Dictionary applies here too. Motion tokens, durations, easing curves, and reduced-motion overrides can all live in your token system, making consistent and accessible animation much easier to maintain at scale.

    Practical uses worth shipping right now

    Given everything above, where does it make sense to actually use CSS scroll-driven animations today? Reading progress indicators are a no-brainer: they’re purely decorative, invisible absence is harmless, and the CSS is six lines. Sticky header opacity transitions work well. Subtle scale or fade effects on section headers as they enter the viewport add polish with minimal risk, provided the @supports and prefers-reduced-motion guards are in place.

    Avoid: scroll-driven animations that carry meaning (i.e., the animation isn’t decorative but reveals content), complex parallax layers with multiple competing timelines, or anything that requires pixel-perfect timing across browsers, because the Safari gap makes that unreliable for now.

    The spec is solid. The implementation is getting there. Ship it as an enhancement, test it properly, guard it correctly, and it genuinely improves the feel of a page without the JavaScript overhead we’ve been living with for years.

  • Tailwind CSS v4 in Production: What UK Development Teams Actually Found After Six Months

    Tailwind CSS v4 in Production: What UK Development Teams Actually Found After Six Months

    Tailwind CSS v4 landed with a proper bang. The CSS-first configuration model, the new high-performance Rust-based engine (Oxide), the slimmed-down setup story, on paper it looked like the framework had finally grown up. I’ve been watching UK development teams field this across client work and internal product builds for the better part of six months now, and the picture is more textured than the release notes let on. Some of it is genuinely impressive. Some of it will catch you off guard on a deadline.

    Developer working on Tailwind CSS v4 in production at a modern UK office desk
    Photo by Daniil Komov on Pexels

    What CSS-first configuration actually means in practice

    The biggest philosophical shift in Tailwind CSS v4 in production is the move away from tailwind.config.js. Your design tokens, your theme extensions, your custom utilities, all of it now lives in a @theme block inside a CSS file. That’s a meaningful change, and for teams that already think in terms of design tokens, it clicks immediately.

    In practice, I’ve seen two camps. Teams who had already invested in a structured token system (think semantic colour layers, spacing scales, type ramps) found the migration almost pleasant. The CSS custom properties that Oxide generates map directly onto what those teams already had in their heads. For them, the config file was always a slightly awkward intermediary, and removing it felt like cutting out a middleman.

    The other camp, and this is most teams, if I’m honest, had their Tailwind config as a sprawling JavaScript object full of one-off values and plugin hacks accumulated across two or three years of client work. For them, the migration was a proper archaeology project. Not impossible, but not the afternoon job some blog posts implied. If your team is in that position, I’d strongly suggest reading up on building a proper design token pipeline with Style Dictionary before you try to port your config, it’ll save you repeating the same sins in a new format.

    The Oxide engine: where the performance wins are real

    Oxide is fast. That’s not marketing. A Tailwind v3 full build on a medium-complexity Next.js project I looked at was sitting around 1.8 seconds on a reasonably modern MacBook Pro. The same project with v4 and Oxide dropped to under 200 milliseconds. Incremental rebuilds in watch mode feel instantaneous, we’re talking sub-20ms on most saves.

    For developer experience on UK agency projects where you’re sometimes running three or four services locally, that matters. The CPU headroom you get back is real. It’s the kind of change that makes you wonder why the JS ecosystem took so long to reach for Rust tooling, but here we are.

    Code editor showing Tailwind CSS v4 theme configuration in a dark theme environment
    Photo by Stanislav Kondratiev on Pexels

    The output CSS story is also much cleaner. v4 generates far less bloat because the engine has a better understanding of which utilities are actually reachable from your source. On one SaaS dashboard project, the production CSS bundle dropped from 28kB (gzipped) to around 11kB without any additional purging configuration. That has a measurable knock-on effect on Core Web Vitals scores, particularly LCP on slower mobile connections, something UK teams building for diverse audiences across patchy 4G coverage genuinely need to care about.

    Where the workflow still has rough edges

    Right. Here’s the honest part that fewer people are writing about.

    The IntelliSense extension for VS Code is not fully caught up. As of early 2026, autocomplete for custom theme values defined in @theme blocks is inconsistent. You get suggestions for core utilities reliably, but your own spacing tokens or custom colour ramps? Hit or miss depending on how you’ve structured the CSS file. For teams where designers and developers share the same editor setup, this creates friction. Not a dealbreaker, but annoying enough that I’ve seen junior devs quietly reach for inline styles when autocomplete fails them, which rather defeats the point.

    Plugin compatibility is another honest headache. The v3 plugin API is gone. Anything you were pulling in that depended on plugin() or addUtilities() from the old API needs a rewrite or a replacement. Community plugins like @tailwindcss/typography and @tailwindcss/forms have official v4 versions now, but third-party ecosystem plugins are patchy. If your project has bespoke plugins for client-specific utility classes, budget time for a proper rewrite.

    The @apply directive also behaves differently, specifically, it no longer works across component boundaries in certain CSS module setups. This caught several teams using CSS Modules alongside Tailwind for scoped component styles. The workaround exists (using CSS layers manually), but it’s not well documented and you’ll likely find it through Stack Overflow at 11pm rather than through the official docs.

    Real decisions teams are making about adoption

    From what I’ve seen across a handful of UK agencies and product teams, the pragmatic decision looks like this: new projects should start on v4 from day one. The clean-slate experience is good, the performance is genuinely better, and the CSS-first approach encourages better token discipline. The solo developers shipping Micro-SaaS products I’ve spoken to have taken to it particularly well, because they’re not dragging legacy config debt along with them.

    Existing projects are a different calculation. If your v3 codebase is large, has custom plugins, and is actively generating revenue, the migration cost needs to sit in a proper sprint. Not a Friday afternoon. One London-based product team I’m aware of spent three weeks migrating a mid-sized B2B SaaS product, and they found bugs in their own component library that had been masked by v3’s slightly more permissive utility generation. So there’s an accidental quality benefit too, if you’re patient enough to find it.

    For teams doing client delivery work where the project switches between Tailwind versions depending on the client’s stack, the cognitive overhead is real but manageable. The mental model for both versions is similar enough that context-switching is fine once you’ve internalised where your theme configuration now lives.

    One thing that doesn’t get discussed enough: Tailwind v4’s cascade layer architecture makes it much more composable with existing CSS from third-party libraries or design systems. Teams integrating component libraries alongside Tailwind (say, Radix UI primitives with custom Tailwind-based styling) are finding fewer specificity conflicts. That’s a structural improvement that compounds over the lifetime of a project.

    A note on tooling integrations and the broader stack

    Tailwind v4 ships with first-class Vite integration, and if you’re on a Vite-based stack it’s probably the smoothest setup experience the framework has ever had. The Webpack integration exists but feels like a second-class citizen, which, given where the tooling ecosystem is heading, is probably fine. The GOV.UK style guidance aside, there’s no regulatory reason UK teams need to be on Webpack, but plenty of enterprise codebases still are, and those teams will need to factor that into their timeline.

    For teams building on Next.js App Router, v4 works well but requires some care around server component boundaries and how styles are emitted. It’s not a blocker, but the Tailwind docs are still catching up to the App Router mental model. A few developers I know have ended up cross-referencing framework-specific community threads alongside the official docs to get the setup exactly right.

    On the subject of integrations: if your team works with a wider development partner for backend and API work, you’re generally fine, Tailwind’s output is just CSS, so any backend stack is unaffected. Dijitul Developments is a good example of a UK shop building tight business logic integrations where the frontend stack is genuinely irrelevant to what they’re doing on the server side, which is worth remembering when scoping projects with mixed frontend and backend concerns.

    The honest summary: Tailwind CSS v4 in production is a meaningful upgrade, not a cosmetic one. The performance gains are real, the CSS-first approach is architecturally sound, and the rough edges are all fixable with a bit of patience. Just don’t expect a frictionless migration if you’ve been on v3 for years. Plan properly, audit your plugins first, and start your next greenfield project on v4 from line one.

    Frequently Asked Questions

    Is Tailwind CSS v4 backwards compatible with v3 projects?

    Not directly. Tailwind v4 introduces a new CSS-first configuration model and a rewritten plugin API that breaks compatibility with v3 config files and third-party plugins built on the old API. Migrating an existing v3 project requires manually porting your theme configuration into a @theme CSS block and rewriting or replacing any custom plugins.

    How much faster is Tailwind CSS v4's build compared to v3?

    The Oxide engine (Rust-based) makes full builds roughly 5-10x faster in typical projects, with incremental rebuilds in watch mode often dropping to under 20 milliseconds. Real-world results vary with project size, but the improvement is substantial enough to notice immediately during development.

    Does Tailwind CSS v4 work with Next.js App Router?

    Yes, but it requires some configuration care around server component boundaries and CSS emission. The Vite integration is the most polished path; if you’re on Next.js with Webpack, expect a slightly rougher setup experience until tooling documentation catches up.

    What happens to @apply in Tailwind CSS v4?

    @apply still works in v4, but it behaves differently in certain CSS Module setups, specifically, it can fail across component boundaries. Teams using CSS Modules alongside Tailwind for scoped component styles should test this early and be prepared to use manual CSS layer declarations as a workaround.

    Should new UK projects start with Tailwind CSS v4 or stick with v3?

    For new projects, v4 is the right choice: better performance, smaller output CSS, and a cleaner configuration model that encourages proper token discipline from the start. For existing v3 projects actively in production, migration is worthwhile but should be planned as a dedicated sprint rather than a quick upgrade.

  • Edge Functions for Frontend Designers: What Cloudflare Workers and Vercel Edge Actually Mean for UK Web Performance

    Edge Functions for Frontend Designers: What Cloudflare Workers and Vercel Edge Actually Mean for UK Web Performance

    Edge computing sounds like something a backend engineer should explain to you whilst you nod politely and think about font weights. I get it. The infrastructure layer has historically been not-your-problem, and for a long time that was fine. But edge functions have crossed a line. They are now genuinely useful to frontend designers and developers in ways that directly affect the things you spend your days obsessing over: page speed, personalisation, A/B testing, and whether your LCP score makes you want to cry.

    This is my attempt to explain edge functions as a frontend concern, not an ops concern, with real-world patterns UK teams are actually shipping in 2026.

    Frontend developer writing edge functions for performance improvements on a dark-themed code editor
    Photo by Daniil Komov on Pexels

    What edge functions actually are (without the waffle)

    A traditional web request goes: user’s browser, CDN cache, your origin server somewhere in a data centre. The origin might be in London if you’re lucky, Dublin if you’re using AWS eu-west-1, or somewhere entirely unhelpful if you never thought about it. Every uncached request hits that server, adds latency, and makes your Time to First Byte worse.

    Edge functions run code at CDN nodes, physically close to the user. Cloudflare has over 300 data centre locations globally. Vercel’s edge network is similar in concept. When a request comes in, instead of bouncing all the way to your origin, a small piece of JavaScript (or TypeScript) runs at the nearest node and returns a response. We’re talking single-digit millisecond execution in many cases.

    The key constraint worth knowing: edge functions are not Node.js. They run in a V8 isolate environment, which means no file system access, limited APIs, and very short execution windows. That constraint is actually what makes them useful for the frontend layer, because it forces you to keep edge logic small and sharp.

    Why this matters for edge functions frontend performance UK specifically

    UK users suffer more latency than they should from services hosted without European region configuration. If your Next.js app’s API routes are running serverless functions in us-east-1 because nobody changed the default, every dynamic request is crossing the Atlantic. Vercel’s edge runtime and Cloudflare Workers both put execution closer to the UK, and the difference in TTFB can be 200-400ms on uncached requests. That is not trivial when UK Core Web Vitals data shows that LCP and INP failures correlate directly with conversion drops on British e-commerce and SaaS sites.

    The Cloudflare network has a major presence in London (Docklands), Manchester, and Edinburgh. Vercel routes UK traffic through Frankfurt and Amsterdam primarily, with London capacity added in 2024. Either way, you are getting dramatically closer to your users than a single-region origin server.

    Three concrete things you can do with edge functions without a backend engineer

    1. Personalisation without client-side flicker

    The standard approach to personalisation in a React or Next.js app is: render the generic page, load JavaScript, check a cookie or API call, then swap in the personalised content. Users see a flash of the wrong content. It is grim, and it tanks your LCP because the meaningful content arrives late.

    Edge functions fix this by reading cookies and headers before the HTML is assembled. A Vercel Edge Middleware file (literally middleware.ts in your project root) can inspect the incoming request, check a user segment cookie, and rewrite the URL to a different version of the page before the browser sees anything. The user gets personalised HTML on the first response. No flicker, no extra round trip, no bespoke backend logic required.

    A practical example: a UK SaaS product serving both SME and enterprise tiers can serve entirely different homepage layouts based on a cookie set at signup, handled entirely in around 20 lines of TypeScript at the edge. No server, no database call, no delay.

    2. A/B testing that doesn’t torch your Core Web Vitals

    Client-side A/B testing with tools like Google Optimize (now retired) or its replacements typically inject test logic after the page loads. The result is layout shift, delayed LCP, and Cumulative Layout Shift scores that your performance audit will flag immediately. Edge-based A/B testing sidesteps this entirely.

    The pattern is straightforward. Your edge function generates or reads a test bucket (usually a random assignment stored in a cookie), then serves bucket A or bucket B of your HTML before the response leaves the CDN. From the browser’s perspective, there is only ever one version of the page. The Cloudflare Workers documentation has solid examples of this pattern using the HTMLRewriter API, which lets you modify HTML at the edge without reconstructing the entire response.

    I’ve used this pattern on a Next.js project for a UK fintech startup. We ran a button colour test (yes, really, the client insisted) across 50,000 monthly visitors without touching the origin deployment or writing a single line of backend code. The Lighthouse scores stayed identical between variants because the HTML was identical in structure, just different in content.

    3. Geolocation-based content without a postcode lookup API

    Cloudflare Workers and Vercel Edge both expose request geolocation headers automatically. cf-ipcountry in Cloudflare, and x-vercel-ip-country and x-vercel-ip-city in Vercel. You can use these to show UK-specific pricing (£ GBP, VAT-inclusive), route Scottish users to Scottish Gaelic content variants, or surface region-relevant promotions, all without a database call or a paid IP geolocation API.

    This is particularly useful for UK e-commerce sites that need to show duty-included pricing to GB users whilst showing export pricing to EU visitors post-Brexit. Handling that at the edge means the correct price is in the HTML from the first byte. No price flicker, no client-side currency swap, no additional JavaScript bundle weight.

    The Vercel vs Cloudflare choice for UK frontend teams

    If you are already deploying on Vercel, the edge runtime is the path of least resistance. Your middleware.ts file is just part of the project, it deploys automatically, and the routing primitives are well-documented. The limitation is that Vercel’s edge functions are tightly coupled to their platform, so you are buying into that ecosystem.

    Cloudflare Workers are more portable and more powerful. You can run them in front of any origin, including a Vercel deployment, which is a combination I have seen UK agencies use to get the best of both. Workers also have access to Cloudflare’s KV store (a globally replicated key-value database) and Durable Objects for stateful logic, which opens up patterns that Vercel middleware simply cannot do.

    For a solo developer or small UK agency, Vercel’s free tier is generous enough for most projects. Cloudflare Workers’ free tier gives you 100,000 requests per day, which is reasonable for prototyping. The paid tiers for both are reasonable once you are shipping something with real traffic.

    What edge functions cannot do (and why you still need some backend)

    Edge functions are stateless and fast, which means they are the wrong tool for heavy computation, database writes, large payload processing, or anything requiring persistent connections. You still need an actual backend (or a serverless function in a proper runtime) for those things. The edge layer is the routing and transformation layer, not the application logic layer.

    I’d also point out that cold start times, whilst dramatically better than traditional serverless functions, are not zero. Vercel’s edge functions typically have sub-1ms cold starts. Cloudflare Workers are similar. But if your logic becomes complex enough, you will want to profile it. The edge environment rewards small, focused functions.

    One thing this connects to for design teams: if you are building a Micro-SaaS product as a solo developer and you have been putting off personalisation because it felt like infrastructure work, edge middleware genuinely removes that blocker. You do not need a dedicated backend engineer to implement sensible routing logic in 2026.

    Getting started without breaking anything

    My recommendation: start with a read-only middleware. Something that inspects a cookie or a header and adds it to a request header for downstream consumption. This is zero-risk, teaches you the API surface, and gives you something to show in a pull request. Once that is merged and boring, move to URL rewrites. Then cookie setting. Then A/B bucket assignment. Each step is independently reversible.

    The Vercel middleware documentation is genuinely good. The Cloudflare Workers quick-start is also excellent, and the playground at workers.cloudflare.com lets you write and test Workers in the browser with live request inspection. Both platforms have strong TypeScript support, which matters if you are working on a codebase where designer-developer handoff is already type-driven.

    Edge functions are not magic, but they are one of the more genuinely useful infrastructure concepts to land in the frontend toolkit in the last few years. The performance gains are real, the personalisation patterns are practical, and the barrier to entry is low enough that a frontend developer comfortable with TypeScript can be productive within an afternoon. That is a rare combination in infrastructure land.

  • The Real Cost of Ignoring Web Performance: What UK Core Web Vitals Data Actually Tells Us in 2026

    The Real Cost of Ignoring Web Performance: What UK Core Web Vitals Data Actually Tells Us in 2026

    There’s a number sitting inside the Chrome UX Report right now that should make every British frontend developer uncomfortable. Across a sample of UK e-commerce domains analysed through the CrUX dashboard earlier this year, fewer than 55% passed the INP (Interaction to Next Paint) threshold of 200ms. For context, INP became a Core Web Vital in March 2024, replacing FID, and Google has had over two years to let the ranking signal bed in. If your UK Core Web Vitals scores are still red, that’s not a future problem. That’s money leaving your client’s business today.

    Frontend developer analysing UK Core Web Vitals data on performance dashboard monitors
    Photo by ThisIsEngineering on Pexels

    I’ve spent a fair amount of time digging through CrUX field data for UK-hosted sites across retail, SaaS, and publishing verticals. What I keep seeing is a gap between what developers think their site scores and what real users on real networks in Birmingham, Manchester, or rural Scotland are actually experiencing. Lab scores in Lighthouse look fine. Field data is a mess. The delta is always user diversity: 4G on an ageing Android handset, a slow broadband connection in a terraced house with three people streaming simultaneously. That’s your actual audience for a huge chunk of UK traffic.

    What the CrUX data actually shows for UK sites

    Google’s CrUX (Chrome User Experience Report) aggregates real-world performance data from opted-in Chrome users, which in the UK means a reasonably representative slice of actual browsing conditions. You can query it directly via BigQuery, or use the PageSpeed Insights API to pull field data for any URL. I’d recommend doing both.

    A few patterns I keep seeing in 2026 UK data specifically:

    INP is the problem nobody fixed. LCP (Largest Contentful Paint) has improved significantly since 2022 because lazy-loading images and server-side rendering became standard. But INP measures responsiveness across the full page lifetime, not just load. JavaScript-heavy React and Vue SPAs tend to hammer INP because long tasks on the main thread block the browser’s ability to respond to user input. A user clicking a filter on an e-commerce PLP (product listing page) and waiting 600ms for anything to happen is having a bad time, even if the page loaded quickly.

    CLS is still broken on a surprising number of UK news and publishing sites. Cumulative Layout Shift above 0.1 is the threshold, and ad-heavy editorial properties routinely fail it. Late-loading ad slots that push content down, cookie banners that reflow the page, and web fonts swapping on render are the usual culprits.

    LCP on UK SaaS dashboards is frequently image-free but still slow. When the LCP element is a large text block rendered by JavaScript rather than a server-side HTML string, you get LCP scores of 4-6 seconds on mid-range devices. This catches out a lot of React-heavy product teams who never questioned whether their marketing site needed a client-side router at all.

    What sluggish scores are doing to conversions

    The Portent research (now a few years old but consistently replicated) put the penalty at roughly a 4.42% drop in conversion rate per additional second of load time in the 0-5 second window. More recent data from Cloudflare and Google’s own web.dev case studies backs this up across verticals. For UK SaaS with a £50/month average contract value and 10,000 monthly visitors, shaving 1.5 seconds off LCP and bringing INP under 200ms can shift conversion rates by 1-2 percentage points. Run that maths. It’s not a small number.

    E-commerce is even more sensitive. Basket abandonment on mobile is already brutal in the UK, where consumers expect fast experiences after years of conditioning by Amazon and ASOS. A slow PDP (product detail page) on a 4G connection isn’t just annoying; it’s a user who went back to Google, clicked the next result, and bought elsewhere. You don’t see that in your analytics beyond an exit rate and a nagging feeling.

    Laptop screen showing UK Core Web Vitals analytics and conversion rate charts
    Photo by RDNE Stock project on Pexels

    How to actually fix INP on a JavaScript-heavy site

    INP is harder to fix than LCP because it’s not a single moment but a score built from all interactions across a session. Here’s where I’d start.

    Profile long tasks in Chrome DevTools. Open the Performance panel, record a session, and look for red bars in the main thread. Any task over 50ms is a long task; anything over 200ms is the INP killer. You’re looking for event handlers that do too much synchronous work, or third-party scripts that block the main thread during user interaction.

    Yield to the main thread deliberately. The scheduler.yield() API (now available in all modern Chromium-based browsers and with a polyfill for Firefox/Safari) lets you break up long tasks explicitly. Wrapping expensive state updates in scheduler.postTask() with a ‘user-blocking’ priority gives the browser space to respond to input before continuing the heavier computation.

    Audit your event listeners. A React component that fires a synchronous setState inside an onInput handler for a search field, triggering a full re-render of a large list on every keystroke, is a classic INP wrecker. Debounce it, or defer the render using useTransition to mark the update as non-urgent.

    Move work off the main thread. Web Workers are underused in frontend codebases, particularly in UK agency projects where the brief was always “just make it work.” Filtering large datasets, parsing JSON from APIs, or running any sort of calculation that doesn’t touch the DOM is a strong candidate for a Worker.

    LCP: the quick wins that are still being missed

    If your LCP element is a hero image (check using the Performance panel or PerformanceObserver in the browser console), preloading it is the single highest-ROI fix available. A <link rel='preload' as='image'> in the document head, combined with correctly sized srcset attributes and a format like AVIF or WebP, will knock seconds off LCP scores for image-led pages. I’ve seen UK retail sites drop from 4.8 seconds to under 2 seconds LCP on mobile just from this change alone.

    If the LCP element is text, the priority is Time to First Byte (TTFB). Slow server response times, which are common on shared hosting and poorly configured WordPress installs (still ubiquitous in UK small business e-commerce), cascade into every other metric. A CDN with edge caching for static assets and full-page HTML caching where possible is non-negotiable. Cloudflare’s free tier handles this adequately for most small UK sites.

    Font loading is the other LCP trap. Using font-display: swap is not a complete solution; it trades invisible text for layout shift. The better approach is to subset your font files aggressively (only the characters you actually use), preload the critical weights, and keep the number of font families to two at most. I’ve written before about how open source font pairing is an increasingly smart approach for UK designers, partly because the files tend to be smaller and better optimised than some commercial typeface packages.

    Tools worth adding to your workflow right now

    Beyond PageSpeed Insights, the CrUX API is queryable for free and returns 28-day rolling field data. Pair it with Treo’s CrUX visualiser or build your own dashboard; I pointed to a walkthrough of that kind of project in a previous article on building a personal design dashboard using Railway and the Notion API, which is worth reading if you’re the type who’d rather have the data in front of you constantly than check it manually.

    Web Vitals Chrome Extension gives you field and lab data side by side while you browse. It’s the fastest way to gut-check any URL during development. Combine it with Webpagetest.org (run a UK test node, Dulles is not your audience) for waterfall analysis.

    For security-conscious teams handling client sites, anything that touches production performance should be monitored consistently. A compromised script injected into your bundle will tank INP immediately; if you ever notice a sudden unexplained regression in your INP scores, check your third-party scripts first. Services like dijitul Support handle malware removal and script injection issues specifically, which is relevant given how often performance regressions turn out to have a security root cause.

    The uncomfortable truth about UK Core Web Vitals in 2026 is that the tooling to diagnose and fix these issues has never been better, the data has never been more accessible, and yet the median UK site is still underperforming on INP specifically. The gap isn’t knowledge; it’s prioritisation. Performance work doesn’t get a ticket raised because it rarely has a visible face. It shows up as a sluggish feeling, a higher bounce rate, a slightly lower conversion rate than last quarter. Put a number to it, surface it to whoever controls the roadmap, and suddenly it gets prioritised. The CrUX data is the number. Use it.

    If you work across design and build (which is most of us on smaller UK teams), performance is a design decision as much as a technical one. The number of image carousels, the weight of a custom typeface, the choice to render a menu client-side instead of server-side: all of it lands in the field data eventually. Understanding what constrained design systems like GOV.UK get right is partly an object lesson in how limiting your component palette keeps your bundle lean and your users happy.

    Frequently Asked Questions

    What are UK Core Web Vitals and why do they matter for my site?

    Core Web Vitals are three metrics Google uses to measure real-world user experience: LCP (loading), INP (interactivity), and CLS (visual stability). They matter because Google uses them as a ranking signal, and poor scores directly correlate with higher bounce rates and lower conversion rates on UK e-commerce and SaaS sites.

    How do I check my site's Core Web Vitals field data for UK users specifically?

    Use the PageSpeed Insights API or query the Chrome UX Report (CrUX) via Google BigQuery to get 28-day rolling field data. Run Webpagetest.org tests using a London or Manchester server node to simulate realistic UK network conditions rather than US-based test servers.

    What is INP and why is it harder to fix than LCP?

    INP (Interaction to Next Paint) measures how quickly a page responds to user input across the entire session, not just at load. It’s harder to fix than LCP because it depends on all JavaScript running on the main thread throughout the user’s visit, including third-party scripts, event handlers, and framework re-renders.

    How much can improving Core Web Vitals actually improve conversion rates?

    Research consistently shows a 4-5% drop in conversions per additional second of load time. Fixing LCP and INP on a UK SaaS product or retail site can shift conversion rates by 1-2 percentage points, which at meaningful traffic volumes represents significant revenue. Google’s own case studies document similar improvements across e-commerce clients.

  • Pixel Art Is Having a Serious Revival: How UK Indie Game Studios Are Using It as a Deliberate Design Statement in 2026

    Pixel Art Is Having a Serious Revival: How UK Indie Game Studios Are Using It as a Deliberate Design Statement in 2026

    There is something quietly radical happening in UK indie game development right now. Pixel art indie game design, which spent a decade being dismissed as budget shorthand or nostalgia bait, has become one of the most deliberate, technically considered aesthetic choices a studio can make. I have watched this shift happen gradually, and by 2026 it is impossible to ignore. The studios turning up at EGX with pixel-driven titles are not doing it because they cannot afford a 3D artist. They are doing it because the constraint itself is the point.

    Monitor displaying a pixel art indie game design with colourful sprites on screen
    Photo by Yan Krukau on Pexels

    Why British developers specifically are drawn to pixel aesthetics

    Context matters here. The UK has a uniquely dense relationship with early home computing. The ZX Spectrum launched in 1982, the BBC Micro sat in virtually every state school by the mid-eighties, and an entire generation of now thirty-to-fifty-year-old developers grew up watching those machines squeeze colour from limitations you would not accept in a modern spreadsheet. When that generation starts building games professionally, or as a serious side project, the pixel aesthetic is not a random retro choice. It is practically a mother tongue.

    This is not pure nostalgia, though. The commercial evidence suggests younger audiences are responding too. According to BBC reporting on indie game market growth, British players consistently over-index for indie titles relative to AAA blockbusters, and pixel-heavy titles from UK studios like Chucklefish (Wargroove) and Team17 properties have demonstrated that the aesthetic travels globally without losing its identity. The culture supporting this goes well beyond the games themselves. Communities built around retro gaming, comic fandom, and physical collector culture, including enthusiast hubs like supazaar.co.uk, have helped cement the cultural appetite that makes pixel-driven games commercially sensible in 2026 rather than a gamble.

    The tooling behind modern pixel art game design

    Ask most people which tool pixel artists use and they will say Aseprite. They are right, but that barely scratches the surface of the current workflow. Aseprite remains the standard for frame-by-frame animation and sprite sheet management, and its scripting API means studios can automate palette swaps, generate animation previews, and pipe assets directly into their build pipelines. I have spoken to a few UK developers who use it alongside Pyxel Edit specifically for tiled environments, treating each tool as the right instrument for a specific job rather than expecting one application to do everything.

    Godot 4 has made a substantial difference here as well. Its 2D renderer handles pixel art natively without the sub-pixel jitter that used to plague Unity projects unless you spent an afternoon configuring camera snapping settings. The engine’s open-source model also means UK indie developers, many of whom are operating on tight budgets, can ship without licensing overhead eating into already thin margins. That practical economic calculation is part of why the pixel aesthetic has staying power: the toolchain is genuinely excellent and largely free.

    Designer working on pixel art indie game design at a graphics tablet
    Photo by Michael Burrows on Pexels

    Colour palette constraints as a design decision, not a limitation

    Here is where pixel art indie game design gets genuinely interesting from a craft perspective. Working within a restricted palette, say 16 or 32 colours, forces decisions that open-ended digital painting never requires. Every colour has to earn its place. The relationship between the darkest shadow tone and the highlight on a character sprite becomes a conscious compositional choice rather than something you can adjust with a curves layer later.

    The Lospec Palette List has become a reference point for UK developers wrestling with this. It catalogues hundreds of community-created palettes, many derived from real hardware constraints, and the conversation around which palette suits a given game’s emotional register is genuinely sophisticated. A cool, desaturated eight-colour palette reads differently to a warm thirty-two-colour one, and experienced pixel artists in the UK indie scene talk about palette selection the way a film cinematographer talks about colour grading. The constraint produces clarity.

    There is also a legibility argument. I have seen more than one post-mortem from UK game jam participants noting that games with tight palettes performed better on small screens and in noisy streaming thumbnails. When every colour is doing structural work, the visual hierarchy tends to be cleaner than in games where the art direction had no hard limits.

    How this connects to broader UI and UX craft

    The design discipline behind pixel art maps onto broader product thinking in ways that are worth taking seriously. The palette constraint logic is structurally similar to working within a design token system, where every colour value is declared and reused rather than picked afresh each time. If you spend any time thinking about how the GOV.UK Design System uses constraint to produce coherent interfaces, the pixel art parallel is immediately visible: deliberate limitation produces consistency, and consistency builds trust.

    There is a related point about legibility at scale. The same cognitive science that makes pixel art characters instantly readable at 16×16 pixels is behind good icon design and the accessibility principles baked into proper UI work. Incidentally, if you are interested in where product design currently fails users even without these constraints, there is a sharp read on how poor interface decisions are frustrating British app users in 2026 that connects directly to why clarity-first aesthetics resonate.

    EGX and the studio ecosystem making this real

    EGX, which runs annually at ExCeL London, has become a useful barometer for where UK indie game design is actually heading. The Leftfield Collection, which showcases experimental and unusual titles, has featured pixel-driven games in increasing numbers over the past few editions. What strikes me watching these studios present is that the pixel aesthetic is almost never the first thing they lead with in their pitch. The mechanic comes first, the world comes second, and the visual style is described as having emerged from those decisions. That ordering matters. It signals that the look is load-bearing, not decorative.

    Smaller studios like Whitethorn Games’ UK-based partners and outfits publishing through Curve Games have demonstrated there is a viable commercial model here. A well-executed pixel art title with strong mechanic design can build a sustainable audience on Steam and GOG without the marketing budget a 3D equivalent would demand. The visual distinctiveness of a coherent pixel palette makes trailers and screenshots punchy in a crowded storefront, which is a genuinely practical competitive advantage.

    Animation within the constraint: where the real craft lives

    If colour palette is the grammar of pixel art, animation is the prose. The anticipation frames, the squash on landing, the hold on a character’s idle cycle: all of this has to be communicated in far fewer pixels than a modern 3D rig would use for a single joint. UK pixel animators working in this space have talked publicly about studying old ZX Spectrum game sprites not for imitation but for compression logic, understanding how a two-frame walk cycle can read as convincingly as a twelve-frame one if the key positions are chosen correctly.

    This discipline connects directly to the work happening in CSS animation and browser motion design. I wrote recently about how UK freelancers are using CSS transitions to replace heavier animation tooling, and the underlying principle is similar: identify the minimum number of states you need to communicate the motion, then nail those states rather than filling every frame. Economy is the technique.

    Pixel art in UK indie games is not a trend that is about to cycle out. The tooling is mature, the cultural grounding is deep, and the constraint-first design logic it demands produces genuinely better outcomes than unlimited creative freedom often does. British developers who grew up on the Spectrum and BBC Micro built an aesthetic vocabulary from necessity. Their successors are choosing that vocabulary on purpose, and the results are some of the most considered game design coming out of the UK right now.

  • Building a Personal Design Dashboard With Notion API and Railway: A UK Freelancer’s Weekend Project Walkthrough

    Building a Personal Design Dashboard With Notion API and Railway: A UK Freelancer’s Weekend Project Walkthrough

    I built this on a rainy Saturday in February, mostly out of frustration with juggling four browser tabs to answer one client question. A Notion database for projects here, a spreadsheet for invoices in GBP over there, a folder structure that made sense three months ago and now just feels like archaeology. The result was a self-hosted Railway app that pulls everything through the Notion API into a single, genuinely useful dashboard. It took a weekend. Here’s exactly how I did it.

    Before we get into the code, a quick note on why Railway specifically. For UK-based freelancers who want a self-hosted feel without maintaining a VPS at 2am, Railway’s free tier (and its £5/month hobby plan) is a reasonable middle ground. You get persistent deploys, environment variables, and a Postgres instance if you need one. It’s not perfect, but it’s fast to set up and the deployment pipeline is git-push simple. If you want a deeper look at framework choices for self-hosted tooling, the open source alternatives UK indie developers are actually shipping with article on this blog covers the broader landscape well.

    Developer workspace with a Notion API dashboard for freelancers displayed on a laptop screen
    Photo by cottonbro studio on Pexels

    Setting up your Notion API integration

    Log into Notion, head to notion.so/my-integrations, and create a new integration. Give it a sensible name, something like “Freelance Dashboard”, and grab the Internal Integration Token. Store it somewhere safe. You’ll need it as an environment variable in Railway shortly.

    Now, the important bit that trips people up: Notion integrations don’t automatically have access to your databases. You have to share each database with the integration manually. Open the database you want to connect, click the three-dot menu in the top right, go to “Add connections”, and select your integration. Do this for every database you plan to query. I have three: one for client projects, one for invoices, and one for design asset pipelines.

    Your database IDs live in the URL. When you open a full-page database in Notion, the URL looks like notion.so/yourworkspace/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX?v=.... The 32-character string before the query parameter is your database ID. Copy these out and store them alongside your token.

    Structuring your Notion databases for the API

    The Notion API returns data in a fairly verbose JSON structure, so it pays to keep your databases clean before you start querying them. For the client projects database, I use these properties: Client Name (title), Status (select: Active, On Hold, Complete), Start Date (date), Deadline (date), Project Value (number, formatted as GBP), and Brief (rich text). The API will hand all of these back as typed objects, which makes parsing straightforward.

    For invoices, I track: Invoice Number (title), Client (relation to the projects database), Amount (number), VAT (number), Paid (checkbox), Issue Date (date), and Due Date (date). The relation property is particularly useful because it lets you join data across databases in your app layer without building a separate relational database. Notion is doing that join for you, and the API respects it.

    The asset pipeline database is simpler: Asset Name (title), Project (relation), Type (select: Logo, UI Kit, Illustration, Icon Set), Status (select: Draft, Review, Approved, Delivered), and File Link (URL).

    Node.js code for a Notion API dashboard for freelancers shown in a terminal and editor
    Photo by Daniil Komov on Pexels

    Building the Railway app: Node.js + Express

    I went with Node.js and Express here. Nothing exotic. Create a new project directory, run npm init -y, then install your dependencies:

    npm install express @notionhq/client dotenv

    The official @notionhq/client package handles all the auth headers and request formatting. Create a .env file at the root:

    NOTION_TOKEN=secret_yourTokenHere
    PROJECTS_DB_ID=yourProjectsDatabaseId
    INVOICES_DB_ID=yourInvoicesDatabaseId
    ASSETS_DB_ID=yourAssetsDatabaseId
    PORT=3000

    Now the core of the app. Create index.js:

    require('dotenv').config();
    const express = require('express');
    const { Client } = require('@notionhq/client');
    
    const app = express();
    const notion = new Client({ auth: process.env.NOTION_TOKEN });
    
    async function queryDatabase(databaseId, filter = {}) {
      const response = await notion.databases.query({
        database_id: databaseId,
        ...filter
      });
      return response.results;
    }
    
    app.get('/api/projects', async (req, res) => {
      try {
        const pages = await queryDatabase(process.env.PROJECTS_DB_ID);
        const projects = pages.map(page => ({
          id: page.id,
          client: page.properties['Client Name'].title[0]?.plain_text ?? 'Unnamed',
          status: page.properties['Status'].select?.name ?? 'Unknown',
          value: page.properties['Project Value'].number ?? 0,
          deadline: page.properties['Deadline'].date?.start ?? null
        }));
        res.json(projects);
      } catch (err) {
        res.status(500).json({ error: err.message });
      }
    });
    
    app.get('/api/invoices', async (req, res) => {
      try {
        const pages = await queryDatabase(process.env.INVOICES_DB_ID);
        const invoices = pages.map(page => ({
          id: page.id,
          invoiceNumber: page.properties['Invoice Number'].title[0]?.plain_text ?? '',
          amount: page.properties['Amount'].number ?? 0,
          vat: page.properties['VAT'].number ?? 0,
          paid: page.properties['Paid'].checkbox,
          dueDate: page.properties['Due Date'].date?.start ?? null
        }));
        res.json(invoices);
      } catch (err) {
        res.status(500).json({ error: err.message });
      }
    });
    
    app.get('/api/assets', async (req, res) => {
      try {
        const pages = await queryDatabase(process.env.ASSETS_DB_ID);
        const assets = pages.map(page => ({
          id: page.id,
          name: page.properties['Asset Name'].title[0]?.plain_text ?? '',
          type: page.properties['Type'].select?.name ?? 'Other',
          status: page.properties['Status'].select?.name ?? 'Draft'
        }));
        res.json(assets);
      } catch (err) {
        res.status(500).json({ error: err.message });
      }
    });
    
    app.listen(process.env.PORT, () => {
      console.log(`Dashboard API running on port ${process.env.PORT}`);
    });

    Three endpoints, each pulling from a separate Notion database and shaping the response into something clean for the frontend to consume. The optional chaining throughout (?. and ??) is doing a lot of heavy lifting here; Notion properties can be null in ways the documentation doesn’t always warn you about.

    Deploying to Railway

    Initialise a git repository in your project folder and push it to GitHub. Then go to Railway, create a new project, and select “Deploy from GitHub repo”. Point it at your repo. Railway will detect the Node.js project automatically.

    Add your environment variables under the project’s “Variables” tab. Everything from your .env file needs to live there; Railway injects them at runtime. Do not commit your .env file to GitHub. Add it to .gitignore before you push anything.

    Add a start script to your package.json:

    "scripts": {
      "start": "node index.js"
    }

    Railway picks this up automatically. Your first deploy will take about 90 seconds. After that, every git push triggers a new build. Railway gives you a public URL in the format yourproject.up.railway.app. Your three API endpoints are now live at that base URL.

    Building the frontend dashboard

    I kept the frontend deliberately simple: plain HTML, a tiny bit of vanilla JS, and the excellent charting libraries that UK SaaS teams are increasingly reaching for. I used Chart.js specifically for a revenue-by-month bar chart that pulls from the invoices endpoint and groups Amount + VAT totals by the month of the due date.

    The key financial summary card calculates outstanding invoices (where paid === false) and displays total outstanding in GBP. No currency conversion, no dollar signs, just a clean £${total.toFixed(2)} string. Small thing, but it matters when you’re reporting income to HMRC.

    Serve the static frontend from the same Express app using express.static('public') and drop your HTML, CSS, and JS files into a /public folder. One deploy, one URL, everything in one place.

    A few things I learnt the hard way

    Notion’s API rate limit is 3 requests per second. If you’re querying three databases on page load, stagger them or use Promise.all carefully. I cache responses for 60 seconds using a simple in-memory object to avoid hammering the API on every browser refresh.

    Relation properties in the Notion API return page IDs, not the actual related page data. If you want the client name in your invoice response, you need a second API call to fetch that related page. I chose to keep it denormalised in the invoices database instead by adding a plain text “Client Name” field that I fill manually. Not elegant, but it halves the API calls.

    Finally, if you’re dealing with sensitive client data in these databases, think carefully about what you expose through the Railway URL. I added a simple API key check middleware that validates a header against another environment variable. Nothing fancy, but enough to stop anyone who stumbles across the URL from reading your invoice totals. And speaking of due diligence with data, I was reminded recently whilst reviewing a client’s office refurbishment docs that the same careful approach applies to physical compliance paperwork; a colleague mentioned they’d used Asbestos Compliance Solutions for site documentation before any structural work began. Proper records, properly stored. Same principle, different domain.

    This whole setup is genuinely more useful than I expected. The Notion API is well-documented, Railway removes all the ops friction, and the result is a dashboard that actually reflects how I work rather than forcing me to adapt to someone else’s product. If you want to push it further, look at adding webhook listeners to Notion page updates, or wiring in a Telegram bot that pings you when an invoice goes overdue. The foundation is there. Now go build something on a weekend.

    Frequently Asked Questions

    Is Notion's API free to use for personal freelance projects?

    Yes, the Notion API is free to use and included with all Notion plans, including the free tier. You can make up to 3 requests per second, which is plenty for a personal dashboard querying a handful of databases.

    What does Railway cost for hosting a small Node.js app like this?

    Railway’s free tier gives you $5 of credit per month (applied to resource usage), which is usually enough for a low-traffic personal app. Their Hobby plan at around £5/month removes usage caps and adds persistent deployments with no idle spin-down, which is worth it for a dashboard you check daily.

    Can I track invoices in GBP specifically through the Notion API?

    Notion stores number properties without currency formatting at the data level, so your amounts are just numbers in the API response. You apply the GBP formatting in your frontend code using JavaScript’s Intl.NumberFormat with ‘en-GB’ locale and ‘GBP’ currency, which gives you properly formatted pound sterling strings.

  • Designing subscription cancellation flows that don’t violate the UK’s consumer protection regulations

    Designing subscription cancellation flows that don’t violate the UK’s consumer protection regulations

    The CMA published updated subscription trap enforcement guidance in 2024, and the FCA followed with its own Consumer Duty obligations fully bedded in. The combined effect is that UK SaaS companies and consumer app teams can no longer treat cancellation as an afterthought bolted onto the back of their onboarding funnel. Off-boarding UX is now a legal concern, not just a product one. I’ve been watching this space for a while and the number of teams who still haven’t rethought their flows is genuinely alarming.

    Person navigating a subscription cancellation flow on a laptop — UK consumer protection compliance
    Photo by https://kaboompics.com/ on Pexels

    What the CMA and FCA actually say about subscription cancellation UX

    The CMA’s enforcement programme targets what it calls “subscription traps”, patterns designed to make cancellation disproportionately difficult relative to sign-up. The specific behaviours in the crosshairs include hiding cancellation options behind multiple screens, requiring phone calls when sign-up was entirely digital, and using dark patterns to confuse users into staying subscribed. The FCA’s Consumer Duty, which has applied to regulated firms since July 2023, adds the requirement that firms act to deliver good outcomes for customers, and forcing someone through a friction-laden cancellation flow is, in the FCA’s own framing, a foreseeable harm.

    What makes this genuinely interesting from a design perspective is that both regulators are effectively writing UX requirements into law. The CMA’s position is that if you can sign up in two clicks, you must be able to cancel in roughly the same number of steps. That’s not a vague principle, it’s the kind of standard you can test a prototype against.

    The dark patterns that are now explicitly non-compliant

    Before getting into what good looks like, I want to name the specific patterns that teams need to remove. Roach motels, where the entry is easy and the exit is deliberately obstructed, are the CMA’s primary target. In practice that means: cancellation buttons that redirect to a retention page before showing the actual cancel option; multi-step “are you sure?” flows that loop back on themselves; confirmation emails that describe the plan as “paused” when the user clearly tried to cancel; and countdown timers that imply urgency without any genuine deadline.

    Confirmshaming, labelling the cancel button something like “No thanks, I’d rather pay more”, sits in similarly murky territory under the Consumer Duty’s requirement that communications are fair and not misleading. The ASA has already taken action against confirmshaming in advertising contexts; the FCA’s Consumer Duty extends comparable logic to product interfaces.

    The subscription cancellation UX UK consumer protection picture also includes post-cancellation billing, which the CMA considers a trading standards matter. If a user cancels and you bill them anyway because the confirmation state was ambiguous, that’s not just a UX failure, it’s potentially an unlawful charge.

    Wireframe mockup of a compliant subscription cancellation UX flow for UK consumer protection
    Photo by ready made on Pexels

    What compliant, ethical cancellation design actually looks like

    The baseline is simple: the cancellation path must be discoverable, direct, and no more complicated than the sign-up flow. For a typical UK SaaS product, that means a clearly labelled “Cancel subscription” option in account settings, not buried under a “Billing” sub-menu inside a “Plans” section inside “Account preferences”. One level of navigation, maximum two.

    A single retention offer is fine and, I’d argue, reasonable UX. Showing someone a downgrade option or a pause feature before they confirm cancellation is genuinely useful, some users want to reduce cost, not leave. The design problem is when that offer becomes a mandatory gate. The user must always be able to bypass it and reach the confirmation screen in one action. Structurally, think of it as: intent confirmed → optional offer shown → confirmation page → cancellation processed. Remove any step that loops back to the start or adds a second offer.

    The confirmation screen itself matters more than most teams realise. It should state the cancellation date clearly, explain what access the user retains until that date, and send a confirmation email that uses the word “cancelled” unambiguously. Teams working on ICO and PECR-compliant consent flows will recognise this pattern, regulators expect the same plain-language, unambiguous confirmation standards whether you’re recording consent or recording a cancellation.

    The technical side: state management and audit trails

    Here’s where it gets properly nerdy. Compliant cancellation isn’t just a design problem, it’s a data engineering problem. Your system needs to record the exact timestamp of cancellation intent, the state of the user’s subscription at that moment, and the confirmation sent to the user. That audit trail is what you produce if the CMA or FCA comes knocking. Most consumer app teams using Stripe or Paddle have access to webhook events that can log this automatically; the failure is usually in not piping those events into a queryable audit log.

    If you’re building on a billing provider that handles proration and mid-cycle cancellations, test the edge cases. A user who cancels on day 14 of a 30-day billing cycle should not receive a renewal charge on day 30. Sounds obvious. I’ve seen it happen. The fix is typically in the webhook handler, not the UI, but the UI still needs to surface the correct information about what the user’s billing state will be after cancellation.

    Teams building data-dense account management screens might also want to revisit how cancellation state is communicated. If your dashboard is already struggling with information hierarchy, and British SaaS dashboards frequently are, then the cancellation confirmation state is easy to lose. A dedicated, unambiguous post-cancellation screen is worth the engineering cost.

    Why this matters beyond compliance

    Scam reporting communities and consumer protection researchers have been documenting subscription trap patterns for years. 0lly, a UK-based scambaiter and fraud investigation activist at 0lly.uk, publishes scam exposés that frequently highlight how fake invoice scams and predatory subscription schemes exploit deliberately confusing cancellation flows. The overlap between outright online fraud and dark-pattern UX is closer than most product teams want to admit; both rely on the same cognitive vulnerabilities. Consumer protection charity work and scam reporting efforts in the UK consistently flag subscription billing as one of the top complaint categories, which is exactly why the CMA escalated to an enforcement programme rather than just issuing guidance.

    From a pure product standpoint, a frictionless cancellation flow is also better for retention metrics in the long run. Users who cancel cleanly and feel respected are more likely to return or recommend the product. Users who feel trapped generate chargebacks, negative reviews, and, increasingly, reports to Trading Standards. The chargeback cost alone often exceeds whatever revenue the dark pattern retained.

    UK product teams expanding into markets with additional language requirements should also think about this in conjunction with localisation. A cancellation flow that’s barely compliant in English may become non-compliant when translated, because the plain-language standard becomes harder to meet in a second language. Teams already thinking about right-to-left UI design for Arabic and Urdu markets need to factor cancellation flow direction and label clarity into that work from the start, not as an afterthought.

    The UK’s subscription cancellation UX consumer protection regime is still relatively new in its enforcement posture. That won’t last. The CMA has already taken action against several consumer-facing businesses and has made clear that digital subscription services are a priority. Get the flow right now, document it, and keep the audit trail clean. It’s not complicated. It’s just work that most teams have been putting off because cancellation UX isn’t glamorous. It is, however, increasingly necessary.

    For reference on what good government-standard design thinking looks like as a baseline, the patterns established in the GOV.UK Design System, particularly around confirmation pages and transaction summaries, are worth borrowing from directly. Government transactional design has been stress-tested against accessibility and clarity requirements for years; subscription cancellation confirmation is essentially the same UX problem.

    The second area where 0lly’s work is relevant to product designers is in the documentation of domain renewal scams and google ads hacks that prey on small business owners through deliberately opaque billing interfaces. The scam reporting and activist resources at 0lly.uk make the point clearly: UK scams targeting businesses often succeed because legitimate subscription billing already looks so confusing that users can’t tell when something has gone wrong. Designing clearer, more honest cancellation flows is, in a small way, raising the baseline for what users should expect from any billing interface.

    Frequently Asked Questions

    What does the CMA say about how easy cancellation must be for UK subscription services?

    The CMA’s position is that cancellation must be no more difficult than sign-up. If a user can subscribe in two clicks online, the regulator expects them to be able to cancel in a comparable number of steps, not via a phone call or a hidden multi-step flow.

    Does the FCA's Consumer Duty apply to subscription cancellation UX?

    Yes, for FCA-regulated firms. The Consumer Duty requires firms to act to deliver good outcomes for retail customers, and the FCA has been explicit that foreseeable harms from product design, including friction-heavy cancellation flows, fall within scope.

    Are retention offers during cancellation allowed under UK consumer protection rules?

    A single, skippable retention offer is generally acceptable. The problem arises when the offer becomes a mandatory gate that the user cannot bypass without taking additional action. The user must always be able to confirm cancellation directly from the retention screen in one step.

    What technical records should a UK SaaS team keep around subscription cancellations?

    At minimum, you should log the exact timestamp of cancellation intent, the billing state at that moment, and a record of the confirmation sent to the user. Billing provider webhooks (Stripe, Paddle) make this straightforward to automate; the key is routing those events into a queryable audit log.

  • Open Source Alternatives to Notion, Linear, and Loom That UK Indie Developers Are Actually Shipping With

    Open Source Alternatives to Notion, Linear, and Loom That UK Indie Developers Are Actually Shipping With

    SaaS subscription costs have quietly become one of the more embarrassing line items in a freelance studio’s accounts. Notion, Linear, Loom, Slack, Figma, Jira, individually each one feels reasonable. Together they can easily run past £400 a month for a team of four. I did the maths on my own setup earlier this year and genuinely winced. The good news is that the self-hosted open source ecosystem has matured enough that you don’t have to choose between cutting costs and keeping a coherent workflow. Here’s what I’ve actually tried, what’s stuck, and what the UK indie dev community seems to be converging on in 2026.

    Developer workspace with open source Notion alternative project dashboard on screen
    Photo by Paras Katwal on Pexels

    Why self-hosting makes sense for small UK studios right now

    A UK sole trader or small limited company running a modest VPS, something like a £6/month Hetzner instance, can host several of these tools simultaneously without meaningful performance issues. The upfront config time is real, but it’s a one-time cost, and the ongoing bill is dramatically lower than stacking commercial SaaS tiers. There’s also a GDPR angle worth taking seriously: when you self-host, your data stays in a jurisdiction you control. For client project notes, that’s not nothing. HMRC’s Making Tax Digital push means more studio finances are flowing through connected tools, and keeping sensitive records off third-party US servers is a decision some UK accountants are actively recommending.

    If you want a deeper look at building a full self-hosted design stack, I wrote about running Penpot, Gitea, and Plausible on a single VPS in a previous piece on self-hosting your design stack, the infrastructure thinking there applies directly to everything below.

    The open source Notion alternative UK developers are actually using: AppFlowy and Outline

    AppFlowy is the one I’d push first. It’s written in Flutter and Rust, which makes it genuinely cross-platform without the Electron weight you get with some alternatives. The document editor is clean, the database views (grid, board, calendar) cover most of what Notion’s block editor does, and self-hosting is straightforward via Docker. The community is active and the release cadence is fast, meaningful updates most months.

    Outline is a better fit if your primary need is a team knowledge base rather than a personal productivity system. It’s polished in a way that Notion took years to reach, the search is snappy, and the permission model is sensible. I’ve seen a few London-based dev agencies switch their internal wikis to Outline and genuinely not miss Notion’s more bloated features. The markdown import/export is solid, which matters when you want data portability.

    Both are genuinely viable as an open source Notion alternative for a UK developer who doesn’t mind an afternoon of Docker Compose config.

    Replacing Linear: Plane and GitLab Issues

    Linear is seductive because it’s fast and opinionated. Replacing it is harder than replacing Notion because the interaction design is genuinely exceptional. That said, Plane (formerly Plane.so) has closed the gap significantly. The issue tracking, cycle views, and roadmap features are all present; the UI is cleaner than Jira by a large margin; and the self-hosted version is free. It’s not quite as snappy as Linear but it’s functional and improving quickly.

    The other answer is simply leaning harder into GitLab’s built-in issue boards and milestones if your code is already there. For solo developers especially, adding a separate project management layer is often overkill. GitLab’s planning features are underused by most people who already pay for it. If you’re on Gitea (which you should consider, it’s tiny and fast), Issues plus Projects covers the basics without any additional stack.

    Async video without Loom: Vdo.ninja and Cap

    Loom is one of those tools that feels indispensable until you find the replacement. Cap is the most direct open source equivalent, it handles screen recording, gives you a shareable link, and the self-hosted version keeps the video on your own storage. It’s still relatively young but the core workflow is there. For client handoffs and async code walkthroughs, it does the job.

    Vdo.ninja deserves a mention too, though it’s more of a live peer-to-peer video tool than a recording one. For quick calls that don’t need a Zoom account, it’s genuinely brilliant. No accounts, no downloads, just a link. The latency is low enough that I’ve used it for client feedback sessions without issues.

    Collaboration and chat: Mattermost over Slack

    Mattermost is the self-hosted Slack equivalent that’s been around long enough to be boring in the best possible way. It’s stable, it has threading, it has integrations, and the mobile apps are decent. For a studio that’s grown tired of Slack’s free tier limitations (10,000 message history is not generous), Mattermost self-hosted removes that ceiling entirely. The setup is more involved than some of the other tools here, but the official deployment docs are thorough and there are pre-built Docker images for most architectures.

    Zulip is worth knowing about too. It organises conversations by topic within channels, which sounds fussy but genuinely helps async teams stay coherent. A few open source projects use it as their primary communication tool and swear by it. If Slack’s flat channel model drives you mad, Zulip’s threading model is worth an hour of your time.

    Design feedback without Figma’s comment layer: Penpot and Freehand alternatives

    Penpot keeps getting better. As a design tool it still trails Figma on plugin ecosystem and prototyping depth, but for a UK freelancer doing UI work for small clients, the core feature set covers most projects. The comment and feedback layer works well enough for async client review. The self-hosted instance is stable and the team’s development pace has picked up noticeably since 2025.

    For teams that want to keep Figma but ditch the extra collaboration cost, Liveblocks has an open-core model worth investigating. It’s not a direct tool replacement but if you’re building your own internal tools, the multiplayer infrastructure is solid.

    Putting it all together: a realistic stack

    The stack I’d suggest for a UK solo developer or small studio trying to cut costs without losing their mind:

    Notes and docs: AppFlowy for personal project notes, Outline for shared team knowledge base. Issue tracking: Plane for dedicated project management, or GitLab Issues if your repos are already there. Async video: Cap for client recordings. Chat: Mattermost if you need team comms; skip it entirely if it’s just you. Design: Penpot where Figma is overkill.

    You won’t get 100% feature parity with the SaaS equivalents. Some of these tools have rough edges and the occasional bug that a £25/month commercial product wouldn’t ship. But the gap has closed enough that the workflow quality argument for staying on expensive subscriptions is much weaker than it was two years ago. The UK’s cost-of-living pressures on freelance rates aren’t going away, and trimming £200 a month from tooling costs is a meaningful number for a one-person studio.

    It’s also worth thinking about this alongside how you structure your frontend tooling choices more broadly. The same practical mindset that leads you to question a £15/month Notion subscription should probably apply to your framework choices and your component architecture too. Self-sufficient stacks compound over time.

  • 3D in the Browser Without Three.js: Using CSS and WebGL Alternatives on UK Client Projects

    3D in the Browser Without Three.js: Using CSS and WebGL Alternatives on UK Client Projects

    Three.js gets all the press. Type “browser 3D tutorial” into any search engine and you’ll wade through pages of Three.js content before anything else surfaces. Which is fine, it’s a genuinely capable library. But on most real UK agency projects, the performance overhead, the bundle size, and the onboarding time for a client’s in-house team make it the wrong call. I’ve watched Three.js get specced into briefs where a handful of CSS 3D transforms would have done the job in two hours and cost nothing in kilobytes. This is a comparison of the lighter approaches: CSS 3D transforms, Spline embeds, and Babylon.js. Browser 3D CSS WebGL alternatives UK agencies can actually ship without a six-week render performance audit afterwards.

    Developer working on browser 3D CSS WebGL alternatives in a UK studio environment
    Photo by MART PRODUCTION on Pexels

    Why the Three.js default is a problem for UK agency work

    The average UK agency project lives and dies by performance budgets that clients don’t fully understand until PageSpeed Insights returns a score in the forties. Three.js minified sits at around 170KB compressed. Add your scene assets, your shaders, your loaders, and you’re routinely looking at 600KB to 1MB of JavaScript before the user has seen a single polygon. On a 4G connection, still the dominant mobile standard for significant chunks of the UK outside major cities, that is a very noticeable wait.

    The Ofcom Connected Nations report consistently shows that rural UK connectivity lags behind headline 5G figures. When you’re building a product site for a client whose audience includes businesses in the Scottish Highlands, rural Wales, or large parts of East Anglia, a 1MB JavaScript payload for a spinning product model is not a sensible trade-off. That’s the first honest conversation most agencies don’t have early enough.

    CSS 3D transforms: genuinely underrated for the right jobs

    CSS 3D transforms are GPU-accelerated, require zero JavaScript dependencies, and have near-universal browser support. The perspective property, transform-style: preserve-3d, and a bit of rotateX/rotateY arithmetic will get you a cube, a card flip, a rotating logo badge, or a parallax depth effect that loads instantly and never blocks the main thread.

    I’d argue the sweet spot is anything that needs to feel three-dimensional without actually being 3D geometry. Navigation menus that flip to reveal content on hover. Hero sections with stacked layers that respond to scroll position via a few lines of vanilla JavaScript. Card components that tilt to follow the cursor. These patterns punch well above their weight visually, and the implementation is something any junior developer can maintain without specialised graphics knowledge.

    The ceiling hits you fast, though. You cannot simulate realistic lighting, you cannot load GLTF assets, and you cannot do physics. Once a client says “I want the product to rotate in 360 degrees with reflections”, CSS 3D is done.

    Spline embeds: the design-tool shortcut with real caveats

    Spline has become a genuinely popular tool in UK product design circles over the last couple of years, and the embed workflow is seductive. A designer builds a scene in the Spline desktop app, publishes it, drops an <iframe> or the Spline viewer script into the page, and it renders. No custom WebGL code. No shader authorship. For design-led agencies where the 3D work lives in the design team rather than the development team, this is a meaningful workflow improvement.

    The catch is the payload. A Spline embed typically loads the Spline runtime (around 300KB compressed) plus your scene file, which varies wildly depending on asset complexity. I’ve seen simple Spline scenes arrive at 800KB total, and complex ones comfortably exceed 2MB. For a hero animation on a landing page where performance is the conversion lever, that’s a hard sell. You’re also handing rendering control to Spline’s infrastructure, if their CDN has a bad moment, your hero goes blank.

    Where Spline earns its place is on pages where the 3D element is supplementary rather than structural: a features section, an “about us” accent, an interactive product teaser behind a scroll trigger. Used like that, with lazy loading in place and a static fallback image for slow connections, it’s a reasonable tool. The key is treating the embed like any other third-party resource: measure it, budget for it, and have a fallback.

    Babylon.js: the serious WebGL alternative most UK devs overlook

    Babylon.js is Microsoft’s open-source WebGL engine, and it’s genuinely impressive. The core engine is around 500KB minified, which is heavier than CSS transforms but lighter than a fully loaded Three.js scene with its ecosystem of add-ons. More relevantly, Babylon’s tree-shaking story is significantly better than Three.js’s: you can import only the modules you need and get meaningful bundle reduction for simpler scenes.

    Where Babylon.js earns serious consideration is the tooling ecosystem. The Babylon.js Inspector is built into the engine and available in development builds, a proper scene graph viewer, material editor, and performance profiler all in the browser. For a UK agency handing a 3D-heavy project to a client’s internal development team after delivery, that is a massive support overhead reduction. “Open the inspector, click the node, change the colour” is a conversation I can have with a non-specialist client. The Three.js equivalent involves editing raw JavaScript.

    Babylon.js also has strong TypeScript support, which matters if your agency has standardised on TypeScript across its frontend stack. The type definitions are maintained by the core team, not a third-party DefinitelyTyped contribution, so they stay current. I’ve found the GLTF loader particularly reliable, less fiddling with material conversion quirks than equivalent Three.js imports from Blender exports.

    The community is smaller than Three.js. Stack Overflow coverage thins out at the edges of the API. But the official documentation is thorough, and the Babylon.js forum is active. For greenfield projects where you’re not borrowing from existing Three.js code, it’s worth the evaluation time.

    How to choose between them on a real brief

    My decision tree is roughly this. If the brief calls for depth and parallax effects, rotating UI components, or card interactions with no actual geometry: CSS 3D, full stop. If the brief calls for a designed scene that a non-coder needs to author and the page is not performance-critical (a marketing splash, a conference landing page, something behind a gate): Spline embed with lazy loading. If the brief calls for real 3D geometry, GLTF assets, physics, or anything a client’s internal team will need to maintain and extend: Babylon.js.

    This maps roughly onto the same thinking behind choosing layout approaches, you wouldn’t use CSS Grid for a simple centred hero any more than you’d use Babylon.js for a card flip. The right tool is the lightest one that does the job. That’s a principle worth holding onto when clients arrive with references to WebGL award sites built by studios with dedicated graphics programmers and no performance requirements.

    A few other things worth noting. WebGPU is arriving properly in 2026, with Babylon.js already shipping a WebGPU backend. That’s going to change the performance ceiling for browser-based 3D significantly over the next couple of years, but it’s still an enhancement rather than a baseline. Plan for it, don’t build for it yet. On the CSS side, container queries are opening up some interesting responsive 3D composition patterns that weren’t feasible eighteen months ago; if you’re doing perspective-based layout work, they’re worth your time. And if you’re already thinking about how text sits in 3D space, the considerations around spatial typography and legibility at depth are directly relevant once you’re placing readable labels inside a three-dimensional scene.

    Performance budgeting for 3D work connects naturally to broader layout decisions. The same discipline that prevents an over-engineered WebGL scene from wrecking a Lighthouse score is the discipline behind structured grid-based layouts that don’t collapse under real-world content. And if you’re building dashboards that incorporate 3D data visualisation, the lessons from designing data-dense dashboards that actually work apply directly, complexity on screen has to justify itself in comprehension, not just in visual ambition.

    Frequently Asked Questions

    What is the lightest way to add 3D effects to a website without a JavaScript library?

    CSS 3D transforms are the lightest option: they use the GPU, require no additional JavaScript, and have full browser support. Using perspective, transform-style: preserve-3d, and rotation values, you can create card flips, parallax depth, and rotating elements with near-zero performance cost.

    Is Spline free to use on client projects?

    Spline has a free tier that allows public embeds, but commercial or private scene hosting requires a paid plan. Check the current Spline pricing page before committing it to a client project, as plan limits around scene exports and team collaboration can affect agency workflows.

    How does Babylon.js compare to Three.js for performance?

    Both engines have similar baseline capabilities, but Babylon.js has better tree-shaking support, meaning you can import only the modules you need and reduce bundle size more reliably. Babylon.js also ships a built-in inspector and stronger TypeScript types, which reduces maintenance overhead on larger projects.

  • Spatial Typography: How Apple Vision Pro and Meta Quest 3 Are Forcing UK Type Designers to Rethink Legibility at Depth

    Spatial Typography: How Apple Vision Pro and Meta Quest 3 Are Forcing UK Type Designers to Rethink Legibility at Depth

    Type has always had to fight for its life against the medium it lives in. Stone, vellum, offset litho, RGB screens, each shift broke assumptions designers had spent decades calcifying into rules. Spatial computing is doing it again, and I’d argue this one is the biggest rupture since we moved from print to screen. The Apple Vision Pro and Meta Quest 3 are landing in UK creative studios and enterprise environments right now, and the typographic conventions that served us perfectly on a 27-inch retina display are actively embarrassing on a headset at arm’s length.

    Designer wearing a spatial computing headset exploring spatial typography in a UK creative studio
    Photo by Sound On on Pexels

    What makes spatial typography different from screen typography?

    On a flat screen, type sits at one distance. That’s the deal. Everything from optical sizing to contrast ratios to minimum font sizes is calibrated around a fixed focal plane. Spatial computing removes that contract entirely. In a mixed-reality environment, a UI panel might float 60 centimetres from your face, while a secondary label sits a metre and a half away. These two elements exist in the same composition but at genuinely different depths, and your eye treats them completely differently, because physics, not preference.

    The two main headsets creating pressure in the UK market right now have different display architectures that make this worse in distinct ways. The Vision Pro uses micro-OLED panels at roughly 3,386 pixels per inch per eye, which sounds like it solves legibility problems outright. It doesn’t. The higher the pixel density, the more the vergence-accommodation conflict (the disconnect between where your eye focuses and where it points) becomes perceptible as type sharpness that flickers with head movement. The Quest 3 uses pancake lenses with a far lower pixel density, around 25 pixels per degree, which means small type at depth blurs in a more conventional, blunt way. Two different failure modes, same typographic problem.

    The parallax problem: why text layers need spatial awareness

    Parallax is the shift in apparent position of an object when viewed from different angles. On a 2D screen it’s a styling trick. In a headset it’s physics. When text is composited at a fixed virtual depth but contains layered elements (a label over a background card, say, or a tooltip floating above a data visualisation), those layers shift relative to each other as the wearer moves their head. The result is that hierarchy collapses. A label that was clearly subordinate at rest can appear to leap forward and dominate when the wearer looks slightly left.

    The practical fix here borrows from film compositing: type needs to be authored with an explicit Z-depth value that matches its visual hierarchy, not just its 2D stack order. This is a conceptual shift for designers used to thinking about layers as a flat system. I’ve been playing with Apple’s RealityKit text rendering and the distinction between “billboard” type (which always faces the user) and “world-anchored” type (which exists at a fixed orientation in space) makes an enormous difference to legibility in practice. Billboard type is almost always the right call for UI; world-anchored type is for environmental storytelling or wayfinding, and it needs significantly larger point sizes.

    Spatial typography UI panel showing text at depth inside a mixed-reality headset display
    Photo by Egor Komarov on Pexels

    Contrast ratios don’t transfer from WCAG to spatial computing

    WCAG 2.1’s AA contrast ratio of 4.5:1 was designed for 2D screens viewed in controlled ambient lighting. A headset punches passthrough video of the real world behind your UI, meaning the “background” behind your type changes dynamically as the wearer turns their head or moves between rooms. A white label on a translucent dark card reads fine against the dark timber panelling of a Shoreditch studio; walk into a kitchen with white walls and the contrast evaporates completely.

    The response from Apple’s visionOS HIG (Human Interface Guidelines) is to use materials with vibrancy effects, essentially a real-time blur and tint composite that adapts to the underlying environment. This raises its own problem: vibrancy at depth loses predictability, and for type designers who care about accessibility (which should be all of us, I’ve written about accessible palette design for colour-blind users and the same audiences are affected here), you cannot audit a contrast ratio that shifts per-frame in real time. The BBC’s Accessibility and Inclusivity team has been vocal about this gap in the spatial computing accessibility conversation, and they’re right to push on it.

    My current working heuristic: design for a minimum 7:1 ratio at the centre of the text, add a 2-pixel dark outline or shadow at a 0.6 opacity, and never trust the vibrancy material to do legibility work you haven’t done yourself in the base type rendering.

    Optical sizing at depth: what point sizes actually mean in 3D

    “24pt” in a flat design tool means something specific: a physical measurement on screen derived from dots per inch. In a spatial environment, point size has to be understood as angular size, the angle subtended at the eye by the glyph height. Apple’s HIG recommends a minimum angular size of 0.4 degrees for readable body copy, which translates to approximately 10 points at 50 centimetres, but 20 points at 1 metre and 40 points at 2 metres. That scaling curve is aggressive and unintuitive for designers coming from a web or print background.

    Variable fonts are the practical tooling answer here. A typeface with a properly implemented optical size axis (the opsz axis in OpenType) can be programmatically adjusted to match the rendered depth of the text plane. You set the optical size to match the virtual distance in centimetres, and the font itself handles weight compensation, letter-spacing, and aperture adjustments automatically. Not every typeface supports this, I’d point you toward the Google Fonts variable fonts catalogue as a practical starting point, though purpose-built spatial display faces from foundries like Colophon and Commercial Type are going to be the serious option for production visionOS or Horizon OS projects.

    Tracking, leading, and the third dimension

    Letter-spacing (tracking) needs to increase at depth, faster than you’d expect from 2D conventions. In web type, tight tracking on large display text is fashionable; in spatial computing it’s a legibility hazard. At 1.5 metres, letters in a tightly tracked headline begin to visually merge under any head movement, even slight. I’d use tracking equivalent to at least 0.05em for anything beyond 80 centimetres, scaling up to 0.12em at 2 metres.

    Leading (line-height) at depth is counterintuitively less critical than tracking, but still needs a floor. The movement of spatial content means ascenders and descenders can appear to collide in peripheral vision even when they’re technically clear. A minimum line-height of 1.5 is sensible for body text in any spatial UI context; tighter than that and you’re relying on perfect head stability that real users don’t have.

    Agencies building out their design capability for these new platforms face a very different technical stack from conventional web projects. If you’re working with a small team that handles conventional web alongside emerging spatial work, the way WDM does with its web projects in the Midlands, the conceptual separation between 2D type conventions and spatial ones is probably the hardest thing to communicate across disciplines. The muscle memory of flat design is actively unhelpful.

    What typeface categories actually work in spatial computing?

    Humanist sans-serifs consistently outperform geometric sans in spatial environments. The varied stroke widths in humanist designs (Gill Sans, Aktiv Grotesk, Inter) give each glyph more distinguishable texture at low angular resolutions, which matters enormously on the Quest 3. Geometric sans faces (Futura, Circular) suffer at depth because their uniform strokes blur into each other. Serifs are a genuinely mixed picture: high-contrast serifs like Bodoni are unusable beyond 80 centimetres, but low-contrast slab serifs perform surprisingly well in world-anchored environmental text.

    The data visualisation context is worth calling out separately. If you’re building spatial dashboards (a direction UK fintech and green-tech firms are actively exploring), the type requirements for data labels at depth are their own sub-discipline. I’ve covered some of the underlying design logic in the context of data-dense dashboard design for UK government tools and sustainability dashboard data visualisation, the same principle that says data labels need to be subordinate to the data applies in spatial environments, but the execution is completely different when the label can exist at a different depth than the chart element it describes.

    The authoring gap: tools are still catching up

    Figma has no native Z-depth for type. Framer’s spatial capabilities are still experimental. Reality Composer Pro is powerful but has a steep learning curve that assumes familiarity with Xcode. The practical reality for most UK studios is that spatial typography decisions are being made in headset, iteratively, with no reliable preview fidelity in the design tools they already own. That’s a workflow problem as much as a typographic one, and it’s pushing some teams toward Spline for spatial prototyping despite its limitations.

    The gap will close, probably faster than we expect. But right now, the designers doing this well are the ones who understand the underlying perceptual physics rather than waiting for a Figma plugin to handle it for them. Learn the angular size formula. Understand vergence-accommodation conflict. Accept that your flat-screen instincts will mislead you and calibrate accordingly. Spatial typography is a genuinely new discipline, not a 3D skin on web type conventions.