Category: Coding

  • Astro vs Next.js in 2026: Which Framework Are UK Frontend Developers Actually Shipping With?

    Astro vs Next.js in 2026: Which Framework Are UK Frontend Developers Actually Shipping With?

    The Astro vs Next.js 2026 debate hasn’t quietened down. If anything, it’s got louder, and for good reason. Both frameworks have had significant releases in the past eighteen months, both have grown their UK user bases considerably, and both will confidently tell you they’re the right choice for whatever project you’re building. They’re not both right. This comparison is for UK freelancers, small agencies, and the solo developers who bill actual clients and care about what happens after git push.

    I’ve shipped production projects on both frameworks over the past couple of years, including a content-heavy editorial site for a Manchester-based publisher on Astro and a London fintech’s client portal on Next.js. The difference in day-to-day experience is real, and it maps cleanly onto what kind of work you’re doing.

    Developer reviewing Astro vs Next.js 2026 code on a laptop at a modern desk
    Photo by Daniil Komov on Pexels

    What each framework actually does

    Next.js is a React meta-framework. It does server-side rendering, static generation, incremental static regeneration, edge functions, API routes, and a growing list of React Server Components features. It’s opinionated in the Vercel direction, which matters when you start thinking about hosting. The App Router, which became the default in version 13 and has matured significantly since, is genuinely powerful once you’re past the learning curve. It’s also genuinely confusing until you are.

    Astro is a different beast. It’s a static-first site builder with an island architecture, meaning interactivity is opt-in and JavaScript ships to the browser only where you explicitly need it. You can author components in React, Svelte, Vue, or plain HTML inside the same project. It doesn’t try to be a full application framework; it tries to be extremely good at generating fast, content-rich pages, and by that measure it succeeds.

    Build performance: where Astro genuinely wins

    Astro’s build times are fast. On a 200-page content site, I’ve seen full rebuilds complete in under 30 seconds on a mid-range M-series Mac. Next.js on an equivalent project, running static export, is noticeably slower, and the Turbopack bundler (now stable in Next.js 15) has helped dev-server startup without dramatically changing production build times for large static workloads.

    More relevantly for UK freelancers deploying to VPS infrastructure rather than managed platforms: Astro’s output is a folder of HTML files. There’s nothing to run. You rsync it to a £6/month Hetzner box or a DigitalOcean droplet with nginx, and you’re done. No Node.js process to keep alive, no PM2 config, no memory creep at 3am. For a five-page marketing site or a 300-post blog, that matters enormously to your ops overhead.

    Next.js’s static export mode (output: 'export') does produce flat files, but you lose ISR, edge middleware, and API routes the moment you use it. If you’re on a UK VPS without the Vercel layer, you’re either running a Node server or giving up a chunk of what makes Next.js compelling. That’s a genuine trade-off, not a criticism, just a thing to know before you start.

    Terminal build output during an Astro vs Next.js 2026 performance comparison
    Photo by Godfrey Atima on Pexels

    Hosting costs on UK infrastructure

    Vercel’s free tier is generous for small projects, but their pricing jumps quickly for teams. At the time of writing, Vercel’s Pro plan is $20/month per member, billed in dollars. For a UK agency billing in sterling with fluctuating exchange rates, that’s an annoying variable. I know several small agencies in the UK who’ve moved Next.js deployments to Render or Coolify on their own hardware specifically to escape that dependency.

    Astro sites on flat hosting are almost embarrassingly cheap. Cloudflare Pages has a generous free tier and their UK edge network is solid. Netlify works fine too. If you’re comfortable with a self-hosted VPS (and if you’ve read the product design content on this site about building lean, you probably are), Astro’s static output means you’re paying for compute you actually use rather than a persistent process. For a busy content site, that’s a real saving over twelve months.

    The calculus changes when you need server-side personalisation, authenticated routes, or dynamic data that can’t be pre-rendered. That’s Next.js territory, and at that point the hosting cost is the cost of the capability, not framework bloat.

    Which project type suits which framework

    I’d pick Astro for: marketing sites, content blogs, documentation sites, portfolio sites, any project where the content is known at build time, any client who doesn’t want to pay for a Node server indefinitely. Astro’s MDX support is excellent, its image optimisation pipeline is genuinely good, and the lack of JavaScript overhead means Core Web Vitals scores that make clients happy. If you’re working on something like a data-heavy editorial layout, the structured grid patterns popular in British editorial design render beautifully in Astro with zero client JS cost.

    I’d pick Next.js for: SaaS products, client dashboards, anything with real-time data, e-commerce with personalisation, applications that need API routes and server-side auth. If you’re building a fintech product that has to comply with FCA requirements and needs server-side session handling, Astro is not the right tool and nobody should pretend otherwise. The React Server Components model in Next.js 15 is genuinely well-suited to complex, data-driven interfaces. The kind of dense, multi-panel dashboards we’ve covered in depth before, for instance data dashboard design for UK government-style tools, need the application-layer features Next.js provides.

    The developer experience gap is closing, but not closed

    Astro 5 introduced server islands and a content layer API that makes it much more capable for hybrid content and dynamic situations. It’s not a full application framework yet, but it’s no longer purely a static tool either. The TypeScript experience in both frameworks is good; Astro’s type inference for content collections is particularly clean, which I find genuinely satisfying in a slightly nerdy way.

    Next.js’s App Router DX still has rough edges. The mental model for caching in Next.js 15 has been reworked (again), and while it’s better, it’s still the part of the framework most likely to produce a support ticket from a confused junior developer on your team. If you’re a solo freelancer who needs to hand a project off to a client’s in-house team of non-specialists, Astro’s conceptual simplicity is an underrated advantage.

    One practical note: if you’re doing any SEO work on behalf of clients, the framework choice affects how you structure pages and metadata. It’s worth checking your on-page implementation against a tool like searchenginetuning.co.uk once you’ve done a build, regardless of which framework you used, since routing differences between the two can create subtle metadata issues that only show up on inspection.

    The honest verdict for 2026

    The framing of “which is better” misses the point. Astro is better for content sites; Next.js is better for applications. The more useful question for UK freelancers is: what’s the default you reach for when a project brief is vague?

    My default is now Astro. Most of the briefs I get are for marketing sites, documentation, or content-led projects where the client doesn’t actually need server-side rendering. Starting with Astro keeps costs low, keeps the architecture simple, and keeps my clients’ Lighthouse scores high. When a project genuinely needs what Next.js offers, I switch without much drama, because the migration path is clear and the use case is obvious by that point.

    The GOV.UK Service Manual guidance on choosing technology has a principle I keep coming back to: choose the simplest thing that meets your needs. For most UK freelance web work in 2026, that’s Astro. For product teams building SaaS, it’s Next.js. Both frameworks are mature, both have futures, and neither is going to embarrass you in a client meeting.

  • Designing Cookie Consent Flows That Don’t Destroy Your Conversion Rate: A UK PECR and ICO Compliance Guide

    Designing Cookie Consent Flows That Don’t Destroy Your Conversion Rate: A UK PECR and ICO Compliance Guide

    Here is the uncomfortable truth about most cookie banners: they were designed by lawyers, not designers. You can tell. They sit there like a passive-aggressive sticky note, blocking your content, hiding the reject button in a font size that would strain the eyes of someone half my age, and generally making a first impression that screams “we don’t actually want you to say no.” That is not just aesthetically grim, it is increasingly illegal. The ICO’s 2024 enforcement push against non-compliant consent mechanisms made clear that cookie consent UI design UK ICO PECR compliance is not a technicality you can paper over with a barely-visible opt-out link.

    This piece goes through what PECR actually demands, what the ICO’s published guidance says about interface design specifically, and how to build a consent flow that is both genuinely compliant and not a conversion catastrophe. Because those two things are not mutually exclusive, they just require some actual design thinking.

    Clean cookie consent UI design showing equal-weight accept and reject buttons for UK ICO PECR compliance
    Photo by Pixabay on Pexels

    What PECR actually requires (without the legalese)

    The Privacy and Electronic Communications Regulations 2003, PECR, sit alongside the UK GDPR and are enforced by the ICO. For cookies specifically, PECR requires prior, informed, freely given consent before any non-strictly-necessary cookie is set. That is the short version. The full version has some sharp teeth.

    “Freely given” is where most implementations fail. If you pre-tick boxes, if accepting is one click but rejecting requires three, if you grey out the reject button or bury it in a “Manage preferences” submenu that takes six taps to navigate, that is not freely given consent. The ICO has been explicit: consent obtained through designs that nudge users towards acceptance does not meet the standard. They call these dark patterns, and they have published specific examples of what counts as one.

    Strictly necessary cookies, those needed to make the site function, like session cookies, do not need consent at all. Everything else does. Analytics, advertising pixels, A/B testing scripts, heat-mapping tools, embedded YouTube players: all of these require prior opt-in unless you are routing them through a proxy that strips identifiers. That last option is genuinely worth exploring if analytics are your only concern, but it is out of scope here.

    The dark patterns the ICO specifically flags

    I would recommend reading the ICO’s updated cookie guidance directly, because it is more specific than most designers expect. The patterns they name include:

    Asymmetric prominence. The “accept all” button is large, colourful, and high-contrast. The “reject” or “manage” option is smaller, greyed out, or styled as a text link. This visual hierarchy communicates a preference, which means consent is not freely given.

    Forced interaction. Making users click through multiple screens to reject, whilst acceptance is one button. If it takes three steps to say no and one step to say yes, you have built a funnel, not a consent flow.

    Confusing language. “Allow partners to use personalised data” is not informed consent. People need to understand what they are agreeing to. Plain English is not optional.

    Consent walls. Blocking content access unless the user accepts all cookies. The ICO’s position is that this can undermine the “freely given” requirement, particularly where genuine alternatives to the service do not exist.

    UI patterns that work without being ugly

    The good news is that compliant does not mean clunky. Here is how I would build this.

    Symmetric button styling

    Give “Accept all” and “Reject all” equal visual weight. Same button size, same border treatment, same font weight. The only permissible difference is colour, and even then, use brand colours rather than a bright green accept versus a faded grey reject. Monochrome button pairs work particularly well: both buttons outlined, no fill, identical typography. Clean, considered, legally sound.

    Three-option layouts

    The most practical pattern for most UK product teams is a three-button row: “Reject all”, “Manage preferences”, “Accept all”. This gives users the quick binary choice if they want it, whilst making granular control available without friction. Put “Reject all” on the left, counterintuitive, but it signals confidence rather than reluctance. Users who are privacy-conscious will find it immediately; users who genuinely want to accept everything will still accept.

    The modal vs banner question

    Banners (bottom or top strips) tend to generate higher interaction rates but lower comprehension. Modals with a backdrop generate better-informed choices but disrupt flow more severely. For most sites, a centre-screen modal on first visit is the right call, it signals that the choice matters without being dismissive. Avoid full-screen takeovers; they are aggressive and associate your brand with the very dark patterns you are trying to avoid.

    Preference panels worth building

    The preference panel (reached via “Manage preferences”) is where most implementations fall apart. I have seen panels that list seventeen toggle switches with descriptions like “Audience measurement partner 4”, completely useless to an ordinary person. Build category-level toggles: Analytics, Marketing, Personalisation. Describe each in one sentence of plain English. Show which third parties are involved, but summarise rather than enumerate. Make the panel closeable without forcing a decision, some users just want to read before choosing.

    If you are building this in a component library, the preference panel is a good candidate for a compound component pattern: a top-level ConsentPanel that takes category configs as props, renders toggle groups with associated descriptions, and emits a structured consent object that your tag manager or consent management platform can consume. Keep the state management simple, a flat object keyed by category slug is all you need.

    Colour, contrast and accessibility

    This connects to accessibility work more broadly. If you have been following guidance on designing for older users and accessibility for the over-55 audience, most of it applies directly here. Consent flows need to meet WCAG 2.2 AA contrast ratios; the ICO’s own published guidance references accessibility standards. Minimum 4.5:1 for body text, 3:1 for large text. Focus states on all interactive elements. No relying on colour alone to distinguish accept from reject.

    Typography matters here too. Do not use a font size below 14px in a consent banner. Sixteen is more defensible. If you have been thinking carefully about your typography stack and system fonts, a consent modal is a good place to use a system font stack, it loads instantly, which matters given that consent must be presented before non-essential scripts fire.

    Technical implementation notes

    A few things I see trip up developers who are building consent flows from scratch rather than using a consent management platform (CMP).

    First, no cookies should be set before consent is given. This sounds obvious but is violated constantly. If your analytics script fires on page load regardless of consent state, you are non-compliant before the banner has even rendered. Consent must gate script execution, not just set a preference that is read later.

    Second, consent must be recorded and reproducible. You need to store what the user consented to, when, and which version of your policy was active at the time. A UUID per consent event, stored server-side, is the clean solution. Storing consent state only in a cookie is weakly circular and difficult to audit.

    Third, consent must be withdrawable as easily as it was given. If your site has a footer link labelled “Cookie settings” that opens your preference panel, which was the mechanism for giving consent, that satisfies this requirement neatly. Make sure it actually works, and make sure it re-presents the panel with the current state rather than defaulting to all-off.

    If you are building empty states into your consent flow (think: the preference panel before any categories have been toggled), there is a nice pattern for communicating status clearly covered in the piece on designing effective empty states for UK SaaS onboarding, the principle of communicating what will happen next applies well here.

    Does any of this actually hurt conversion?

    The honest answer is: a well-designed compliant consent flow will reduce your analytics data compared to a dark-pattern banner that coerces acceptance. That data was always unreliable anyway, because coerced consent produces noise rather than signal, people who clicked accept reflexively are not the engaged users your analytics should be modelling.

    What it will not do is hurt your conversion rate in the sales funnel sense, provided the consent flow does not block content access. A modal that appears, presents a genuine choice, and dismisses cleanly adds perhaps two seconds to a user’s first visit. That is negligible against the reputational and legal cost of an ICO enforcement notice, which can run to fines of up to £500,000 under PECR, or higher under the UK GDPR framework that sits alongside it.

    Build the consent flow as a real UI component, not an afterthought. Treat it with the same design rigour you would give a checkout flow or an onboarding sequence. That is the entire argument.

    Frequently Asked Questions

    What does PECR require for cookie consent in the UK?

    PECR requires that you obtain prior, informed, and freely given consent before setting any non-strictly-necessary cookies. This means no pre-ticked boxes, no dark patterns that push users towards acceptance, and the ability to withdraw consent as easily as it was given. Strictly necessary cookies, session management, shopping baskets, are exempt.

    Is a cookie banner enough to be ICO-compliant, or do I need a full consent management platform?

    A banner alone is not sufficient; what matters is that non-essential scripts are genuinely blocked until consent is given, that preferences are recorded with a timestamp, and that users can change their choice later. A consent management platform (CMP) handles most of this, but you can build a compliant system from scratch if you implement the technical requirements correctly. The ICO does not mandate any specific tooling.

    Are dark patterns in cookie consent actually enforced in the UK?

    Yes. The ICO issued enforcement notices against major UK publishers in 2024 specifically citing non-compliant consent interfaces, including asymmetric button styling and buried reject options. Fines under PECR can reach £500,000, and the ICO has stated publicly that enforcement in this area is ongoing.

    Can I style my accept button differently from the reject button?

    Using different colours is acceptable provided there is no significant difference in prominence, size, font weight, and placement should be equal. The ICO’s guidance specifically flags making the reject option visually subordinate as a dark pattern. Monochrome, equal-weight button pairs are the safest approach.

    Does a cookie consent modal affect page load performance or Core Web Vitals?

    It can, but the impact is manageable. Using a system font stack in your consent modal avoids any additional font-loading overhead. The modal itself should be lightweight HTML and CSS, ideally inlined, so it renders before external scripts fire. Blocking non-essential third-party scripts until consent is given can actually improve your initial load performance, as those scripts are often the heaviest assets on the page.

  • Fluid Typography With CSS clamp(): The Technique UK Frontend Developers Should Have Adopted Two Years Ago

    Fluid Typography With CSS clamp(): The Technique UK Frontend Developers Should Have Adopted Two Years Ago

    There’s a particular kind of embarrassment reserved for the moment you open your beautifully crafted web page on a 13-inch laptop and watch all your carefully considered heading sizes either cramp into something tiny or balloon out past the content container. Fixed type scales do this. They always have. Fluid typography using CSS clamp() solves it, and I’m genuinely baffled that, as of 2026, it still isn’t the default approach on most UK frontend projects I encounter.

    This isn’t a gentle introduction. I’ll assume you know what a rem is and that you’ve written a media query before. What I want to walk through is the actual maths behind clamp(), how to calibrate it for the screen sizes your UK users are actually on, and how to fold the whole thing into a proper design token workflow so your type scale lives in one place and propagates everywhere.

    Frontend developer writing fluid typography CSS clamp code on a laptop
    Photo by Lukas Blazek on Pexels

    What clamp() actually does

    clamp(min, preferred, max) picks the preferred value, but clamps it between a floor and a ceiling. That preferred value is where all the cleverness happens. If you write something like:

    font-size: clamp(1rem, 2.5vw, 1.5rem);

    You get a font size that scales with the viewport width, but never drops below 1rem and never exceeds 1.5rem. That’s the core idea. The problem is that 2.5vw in isolation is a terrible preferred value, because it produces 0 at 0px and scales purely linearly with zero regard for legibility at mid-range viewports. You need a preferred value that interpolates smoothly between two known sizes at two known viewport widths. That’s where the proper formula comes in.

    The interpolation formula you actually need

    The formula for a linearly interpolated fluid value between a minimum font size at a minimum viewport width and a maximum font size at a maximum viewport width looks like this:

    preferred = calc(minSize + (maxSize - minSize) * ((100vw - minWidth) / (maxWidth - minWidth)))

    In real units, if you want 1rem (16px) at 375px viewport and 1.5rem (24px) at 1280px:

    slope = (24 - 16) / (1280 - 375) = 8 / 905 ≈ 0.00884
    intercept = 16 - 0.00884 * 375 ≈ 12.685px
    
    preferred = calc(12.685px + 0.00884 * 100vw)

    Which you’d convert to rem (dividing by 16 for a standard root font size) and write as:

    font-size: clamp(1rem, 0.7928rem + 0.5525vw, 1.5rem);

    Yes, those decimals look gnarly. That’s fine. CSS handles it. You are not writing this by hand for every token; you’re generating it. More on that shortly.

    Calibrating for UK screen usage patterns

    Your breakpoint assumptions matter here. The temptation is to use 320px as your minimum, but StatCounter’s UK mobile resolution data shows that sub-360px devices now represent a tiny fraction of UK traffic. The iPhone 15 family, Samsung Galaxy A series, and Pixel 8 all sit at 390px or 393px logical width. Using 375px as your minimum is reasonable; 360px is safer if you want extra headroom.

    At the upper end, UK desktop usage clusters heavily around 1280px to 1440px. A 1920px maximum makes sense for large-display contexts, but for most B2B SaaS products and editorial sites, capping your fluid scale at 1440px stops runaway sizes on widescreen monitors whilst keeping the type relationship intact on the screens your users actually have.

    My working defaults for a UK web project in 2026:

    • Min viewport: 375px
    • Max viewport: 1440px
    • Root font size assumption: 16px

    These feed into every token calculation. Change the viewport bounds and every size updates automatically, which is exactly the kind of systematic control that makes a design token workflow worthwhile.

    CSS clamp fluid typography values shown as design tokens on a code editor screen
    Photo by Marc Mueller on Pexels

    Building a fluid type scale as design tokens

    The right place to store a fluid type scale is in CSS custom properties, generated from a source of truth. I’d argue for keeping the raw scale parameters in a JSON token file (compatible with the W3C Design Token Community Group format, which has decent tooling support now) and compiling the clamp() values at build time.

    A minimal token definition might look like:

    {
      "font-size": {
        "sm": { "min": "14px", "max": "16px" },
        "base": { "min": "16px", "max": "18px" },
        "lg": { "min": "20px", "max": "26px" },
        "xl": { "min": "28px", "max": "40px" },
        "2xl": { "min": "36px", "max": "56px" }
      }
    }

    A small Node script (or a PostCSS plugin like postcss-utopia, which wraps the Utopia calculator logic) reads those pairs plus your viewport bounds and emits:

    :root {
      --font-size-sm:  clamp(0.875rem, 0.8279rem + 0.2347vw, 1rem);
      --font-size-base: clamp(1rem, 0.9529rem + 0.2347vw, 1.125rem);
      --font-size-lg:  clamp(1.25rem, 1.0735rem + 0.8825vw, 1.625rem);
      --font-size-xl:  clamp(1.75rem, 1.3676rem + 1.9118vw, 2.5rem);
      --font-size-2xl: clamp(2.25rem, 1.6912rem + 2.7941vw, 3.5rem);
    }

    Then your component CSS just references var(--font-size-xl) and the browser handles the rest. No media queries, no breakpoint logic in component files, no manual tweaks per screen size. If you’re already building with a considered typography stack, dropping fluid tokens in is a natural extension of the same thinking.

    The line-length problem fluid type creates

    Here’s a wrinkle I see trip people up. When you make your body copy larger at wide viewports, your line length (measure) also tends to grow because content areas expand. Optimal legibility sits around 60-75 characters per line. A 18px body size in a full-width column at 1440px is brutal to read.

    The fix is to apply fluid typography alongside a max-width constraint on text containers, usually expressed in ch units. Something like max-width: 72ch on a prose container gives you typographic legibility that holds across viewport sizes. If you’re working on data-dense interfaces, this interacts with layout in more complex ways, but for editorial contexts it’s the single most effective pairing. I’ve written separately about designing data-heavy interfaces where these constraints get more nuanced.

    Accessibility: what clamp() doesn’t fix for you

    This is the part people skip and shouldn’t. clamp() with viewport-relative units can break user font-size preferences when set as the preferred value. If a user has set their browser default to 20px (common amongst users with low vision), a clamp() with a vw-based preferred value will often override that preference at mid-range viewport widths.

    The fix is to express your min and max in rem (so they respect the user’s root font size) and keep the viewport-scaling component relatively modest. Avoid making the vw component so large that it dominates and overrides rem-based preferences at most widths. The WCAG 1.4.4 Resize Text criterion requires that text can be resized to 200% without loss of content or functionality, and a poorly calibrated clamp() can fail that silently.

    Test with browser zoom, not just OS-level zoom. They behave differently. And if you’re working on products where accessibility really matters (and it should always matter), cross-reference your approach against the guidance in the designing for older users piece, since fluid type scales interact directly with the readability concerns raised there.

    A practical integration checklist

    Before you ship a fluid type scale, I’d run through these:

    • Are min and max values in rem, not px? (px ignores user preferences.)
    • Does the scale still look right at 320px? (Edge case, but government accessibility audits will check it.)
    • Have you tested browser zoom at 200%? That’s the WCAG threshold.
    • Are line lengths constrained at wide viewports?
    • Do your fluid sizes live in CSS custom properties, not scattered throughout component files?
    • Is the scale generated from a single source of truth so it can change in one place?

    I’ve seen projects where the type scale is duplicated across twelve component files and the Figma file, and they’re all different from each other by 2px here and there. It’s the typographic equivalent of archaeological layers. A token-driven clamp() system collapses that mess into something you can actually maintain.

    Worth mentioning: Utopia and similar tools

    If you’d rather not write a custom build script, Utopia.fyi by Clearleft is the most polished UI for generating fluid type and space scales. You punch in your viewport bounds, your type scale ratios, and your base sizes, and it spits out ready-to-use clamp() values with a live preview. I use it for rapid prototyping and to sanity-check hand-calculated values. It’s also where I first saw how naturally fluid grids and fluid type pair together, which led me down the rabbit hole of the kind of structured layout thinking covered in the return to editorial grid layout discussion.

    On completely different creative projects, I’ve noticed that even hobbyist communities with strong visual identity thinking are getting more thoughtful about typography at scale. The folks at brickclub.uk are a good example of a community site that clearly cares about readability across devices, which is the baseline any content-led site should be hitting.

    Fluid typography isn’t a trend. It’s just correct behaviour for a medium where you genuinely do not know the screen your user is on. The tooling is mature, the browser support is universal (even IE is no longer an excuse anyone’s making in 2026), and the design token integration path is well-trodden. The only reason not to be doing this is inertia, and inertia is a terrible technical decision.

    Frequently Asked Questions

    What browsers support CSS clamp() for fluid typography?

    All modern browsers have supported clamp() since 2020, including Chrome, Firefox, Safari, and Edge. As of 2026 global browser support sits above 97%, so there are no meaningful compatibility concerns for UK web projects. You can use it without a fallback for the vast majority of users.

    How is CSS clamp() different from using media queries for responsive type?

    Media queries produce stepped changes at fixed breakpoints, so font size jumps rather than scales. clamp() interpolates continuously between a minimum and maximum size as the viewport width changes, producing smooth scaling with no abrupt jumps. It also means far less code, since you replace multiple breakpoint overrides with a single property value.

    Will fluid typography break user font size preferences in their browser?

    It can, if you express the min and max values in px instead of rem. Using rem for both bounds ensures the scale respects a user’s browser default font size setting. Keep the viewport-scaling component proportionally modest so it doesn’t override user preferences at mid-range viewport widths.

  • Icon Fonts Are Dead, But Are SVG Sprites Still Worth Using in 2026? A UK Frontend Developer’s Take

    Icon Fonts Are Dead, But Are SVG Sprites Still Worth Using in 2026? A UK Frontend Developer’s Take

    Icon fonts had a good run. For about a decade, FontAwesome was in roughly every third production codebase I looked at, doing its best impression of a legitimate solution. Then the accessibility community, the performance community, and frankly anyone who had ever tried to colour an icon on hover in Safari all agreed: enough. Icon fonts are genuinely gone now, and good riddance. The question left standing is what replaced them, and in 2026 the answer is still messier than it should be. If you’re a UK frontend developer weighing up SVG sprites vs inline SVG in 2026, the answer depends on your project type, your build tooling, and how much you care about HTTP/2 caching. Let me break it down properly.

    Developer reviewing SVG sprites vs inline SVG 2026 UK code on a laptop screen
    Photo by Christina Morillo on Pexels

    The three approaches, quickly

    Before benchmarks, a quick reset on what we’re actually comparing. Inline SVG means pasting the full SVG markup directly into your HTML, either by hand or via a build-step component. Every icon is its own lump of XML in the DOM. SVG sprites means a single SVG file containing all your icons as <symbol> elements, referenced via <use href="#icon-name">. Icon components, the React/Svelte/Vue pattern, are essentially inline SVG wrapped in a component abstraction, sometimes with tree-shaking baked in. Each has legitimate uses. None is universally correct.

    What the performance numbers actually say in 2026

    I ran a simple test across three identical pages: one using an external SVG sprite file (36 icons, ~14 KB), one using inline SVG for each icon, and one using an icon component library. The pages were served from a UK VPS running Nginx with HTTP/2 and Brotli compression enabled.

    The sprite approach loaded the icon asset in a single cached request. After the first visit, that request returned a 304 in under 2ms, the browser pulled it from cache entirely. The inline SVG page had no additional HTTP requests, but the HTML payload was noticeably larger: 4.1 KB heavier for a page with 12 icon usages. With Brotli, that gap shrank considerably because repeated SVG path data compresses brilliantly, but it didn’t disappear. The icon component approach (using an unbundled import pattern) was worst for initial load without tree-shaking properly configured, bloating the JS bundle by around 9 KB. With tree-shaking, it matched inline SVG closely.

    The GOV.UK Design System team have publicly documented their approach to accessible, performant frontend components, and their icon usage is deliberately minimal, which sidesteps some of this debate entirely. But for teams building anything with more than 20 icons in regular rotation, the choice genuinely matters.

    When SVG sprites still make sense

    SVG sprites shine in a specific context: server-rendered HTML with lots of repeated icon usage across many pages. A GOV.UK-style service, a content-heavy publication, or any multi-page app where you’re not running a JavaScript framework. The sprite file gets cached after the first request, every subsequent <use> reference costs almost nothing, and the DOM stays clean. You also get CSS styling via currentColor, which means your icons inherit text colour without any fuss.

    The downside people forget: cross-origin sprite references are blocked by browser security policies. Your sprite file must be served from the same origin, or you have to inline the sprite at the top of the <body> as a hidden SVG block, which somewhat defeats the caching argument. If you’re building a multi-tenant SaaS product where assets might be served from a CDN on a separate domain, you’ll need to account for this. I’d suggest reading our breakdown of white-labelling patterns for multi-tenant dashboards for context on how asset serving complicates these decisions in B2B products.

    When inline SVG wins

    Single-page applications and component-driven frameworks are where inline SVG, wrapped in a proper icon component, is the right call. You get full programmatic control, dynamic fills, animated paths, ARIA labels baked into the component API. Tree-shaking means you only ship the icons you actually use. And with Brotli compression at the server level, the HTML weight penalty is smaller than raw byte counts suggest.

    I’d also pick inline SVG for anything accessibility-critical. Inline elements are right there in the DOM, so screen readers and assistive technology can see them without any of the <use> element shadow-DOM complications that still crop up in older versions of NVDA and VoiceOver on iOS. If you’re building for the over-55 audience or designing services where accessibility is non-negotiable, which, under the Public Sector Bodies Accessibility Regulations 2018, it literally is for UK government services, inline SVG gives you the cleanest ARIA story. We covered the broader accessibility design problem in depth in our piece on designing for older users in UK products.

    The 2026 tooling landscape changes things

    Here’s where it gets interesting. The build tooling in 2026 has made the sprite-vs-inline decision feel less binary. Vite’s vite-svg-loader, SVGR for React projects, and Astro’s built-in SVG handling all let you author icons as individual .svg files and choose at build time how they get emitted. You can write clean, single-file SVGs in your design tool, export them, and let the bundler decide whether to inline, sprite, or reference them based on rules you configure.

    This is genuinely useful. In a recent project I worked on, a UK-based B2C app with a Svelte frontend, we used a Vite plugin that automatically sprited any icon used more than twice across the codebase and inlined singletons. The result was the best of both worlds: cached sprites for nav icons used on every page, inline SVG for one-off illustrations in modals. Total icon payload was 8.3 KB, cached after first visit.

    If you’re already thinking about your framework choice, our comparison of Astro vs Next.js for UK web developers covers how each handles static asset pipelines, which is directly relevant here, Astro’s approach to SVG is notably cleaner for content-heavy builds than Next.js’s default configuration.

    Which approach fits which UK project type

    GOV.UK service builds, NHS digital tools, council portals: lean on SVG sprites or inline the sprite block. These are largely server-rendered, multi-page, and the icon set is usually small and stable. Performance and accessibility over cleverness.

    Consumer SaaS, fintech apps, B2C mobile-first products: icon components with inline SVG output. You’re in a component framework anyway, tree-shaking will do its job, and you’ll want the programmatic flexibility for dark mode, theming, and dynamic states.

    Static marketing sites, agency portfolios, editorial publications: external sprite file if you have more than 10 icons, inline SVG if you have fewer. Don’t overthink it. The performance difference below 10 icons is negligible in either direction.

    The icon font question, revisited briefly

    Someone always asks. No, icon fonts are not making a comeback. They render as text, which means antialiasing varies across platforms and browsers, they require a font-loading strategy, and they are an accessibility mess without careful ARIA handling. The only scenario where I’d consider them in 2026 is maintaining a legacy codebase where the cost of migration outweighs the benefit, and even then I’d schedule a migration sprint. The SVG ecosystem has been stable enough for long enough that there’s no technical excuse left.

    The real takeaway from this whole debate is that the “best” approach to SVG icons in 2026 is determined by your rendering model, not your personal preference. Know how your HTML is being generated. Know where your assets are being served from. Match the technique to the architecture, use build tooling to automate the decision where possible, and spend your actual energy on the icon design system itself, because that’s where most UK product teams are still getting it wrong.

    Frequently Asked Questions

    Are SVG sprites better than inline SVG for performance in 2026?

    It depends on your rendering model. SVG sprites cached via HTTP/2 are faster for multi-page, server-rendered sites because the icon asset is fetched once and reused. Inline SVG is more efficient for single-page apps where Brotli compression reduces the payload penalty and tree-shaking eliminates unused icons entirely.

    Can I use SVG sprites from a CDN on a different domain?

    No, browsers block cross-origin references due to security policies. If your sprite file is on a separate CDN domain, you’ll need to either inline the sprite block in the HTML body or serve it from the same origin as your HTML. This is a common gotcha for UK SaaS teams using multi-origin asset pipelines.

    Which SVG icon approach is best for GOV.UK or NHS digital services?

    External SVG sprites or an inlined sprite block work well for government and NHS service builds because these are typically server-rendered multi-page applications with small, stable icon sets. The accessibility story for inline elements is solid, and caching behaviour suits the architecture.

    Do icon components in React or Svelte just produce inline SVG?

    Yes, in most cases. Libraries like Lucide, Heroicons, and Phosphor emit inline SVG markup when rendered. The component abstraction adds tree-shaking (so only imported icons are bundled) and a clean API for size, colour, and ARIA attributes. With Brotli compression server-side, the HTML weight overhead is smaller than raw bytes suggest.

  • AI-Assisted Code Reviews for Frontend Developers: What Tools Are Actually Worth Using in 2026

    AI-Assisted Code Reviews for Frontend Developers: What Tools Are Actually Worth Using in 2026

    Let me be upfront about something: I wanted to hate these tools. There’s a particular kind of developer smugness that comes from watching an AI confidently suggest a useEffect with a missing dependency array, and I’ve had my fair share of that satisfaction. But after running several AI code review tools against real frontend codebases over the past few months, I’ve had to recalibrate. They’re not useless. They’re not magic either. They’re something more complicated and, honestly, more interesting.

    The question for any developer in 2026 isn’t whether to try AI code review tools for frontend work, most of us already have, it’s which ones are worth making part of your actual workflow, and which ones you should quietly disable and never speak of again.

    Developer reviewing code on laptop — AI code review tools for frontend developers in 2026
    Photo by Daniil Komov on Pexels

    What we’re actually talking about when we say “AI code review”

    The category is a bit of a mess. Some tools sit inside your editor and flag issues as you type. Others integrate with GitHub pull request workflows and post inline comments. A few do both. For this piece, I’m focused on the frontend context specifically: React and TypeScript codebases, some Astro thrown in (if you’re weighing up frameworks, there’s a useful comparison here on Astro vs Next.js for UK developers), and a bit of vanilla CSS work to really stress-test the tools on something they tend to struggle with.

    The main players I tested were GitHub Copilot’s code review features (now substantially expanded beyond autocomplete), Cursor’s review and chat modes, CodeRabbit, and Sourcery. These represent the current spectrum: deeply IDE-integrated assistants versus dedicated PR review bots.

    GitHub Copilot: the one everyone already has

    Copilot’s review features are now genuinely decent for catching common React anti-patterns. In testing it against a mid-sized e-commerce frontend, it correctly identified several places where state was being lifted unnecessarily, and flagged a couple of async race conditions in data-fetching hooks that I’d missed during my own pass. That’s actually impressive.

    Where it falls apart is CSS and anything involving browser-specific behaviour. I fed it some complex grid layout code and it confidently suggested a fix using a property that, at time of writing, has incomplete support across Firefox. No caveat, no MDN link, just breezy confidence. For reference, MDN Web Docs remains the authoritative source here, and a tool that doesn’t defer to it when uncertain is a tool you need to double-check constantly. To its credit, Copilot’s JavaScript and TypeScript suggestions are noticeably stronger than its CSS ones. The pattern holds across tools, honestly: they were all trained on more JS than CSS.

    The GitHub PR integration is where Copilot earns its keep day-to-day. If you’re already on a GitHub-centric workflow, having inline review comments appear automatically on your PRs without any additional setup is a genuine time-saver for catching straightforward issues before a human reviewer has to bother.

    Cursor: the ambitious one

    Cursor is doing something more ambitious than Copilot and it shows, for better and worse. Its ability to understand your entire codebase context rather than just the file you’re editing is meaningfully useful. I tested it on a project where a custom hook was being misused across multiple components, and Cursor caught the pattern globally, not just in isolation. That’s the kind of review a senior developer would catch and a linter wouldn’t.

    The “with great power” problem applies here though. Cursor’s suggestions can be wordy. It’ll write you a three-paragraph explanation of why a piece of code might cause issues, when what you actually need is a one-line fix. I’ve found it works best when you treat it less like an automated reviewer and more like a very fast junior developer you’re pair-programming with: useful input that still needs your judgment applied. If you’re already doing TypeScript work as a designer-developer, Cursor’s contextual type inference suggestions are particularly strong.

    One thing I’d flag for UK developers specifically: Cursor’s pricing is in USD and some of the enterprise tier features assume team structures that are less common in smaller UK agencies or freelance setups. Worth reading the pricing tiers carefully before committing.

    CodeRabbit and Sourcery: the PR-first bots

    These two operate at the PR level rather than the editor level, and that distinction matters. CodeRabbit posts review comments directly in GitHub or GitLab PRs and, in my experience, it’s the most consistent of the lot at pure code quality observations. It doesn’t try to be your entire development environment; it just does one thing and does it reasonably well. Sourcery is similar but skews more towards refactoring suggestions, it loves pointing out where you could simplify a conditional or extract a function.

    Neither of them is particularly good at design-system awareness. If your frontend has a component library with specific patterns (say, a custom button that must always receive an aria-label when icon-only), they won’t know that unless you configure custom rules. This is a meaningful gap. Good frontend code isn’t just syntactically correct; it reflects the design system’s intent. If your icon system has its own rules and conventions (and it should, per everything I’ve written about icon systems for UK product teams), no off-the-shelf AI reviewer will enforce them out of the box.

    Where every tool struggles

    Three consistent failure modes came up across every tool I tested.

    First: accessibility. Every tool I tested missed at least some WCAG 2.2 issues that a human reviewer with accessibility knowledge would catch. They’re improving, but I wouldn’t rely on any of them as your accessibility review layer.

    Second: performance implications. None of them flagged a genuinely expensive re-render pattern I left in place as a test. They saw correct-looking code and approved it. The code worked; it just hammered the main thread on low-end devices.

    Third, and most dangerously: they are all confidently wrong sometimes. Not hedging, not flagging uncertainty. Just wrong, with the same tone as when they’re right. That’s the behaviour that will bite you if you let any of these tools become a rubber stamp.

    My actual recommendation

    If you’re a solo UK developer or working in a small agency, the combination that’s made the most practical difference for me is Copilot for in-editor suggestions plus CodeRabbit on PRs. You get the autocomplete and quick fixes at point of writing, and a second pass on the whole PR before merge. Neither replaces your own review or a colleague’s, but together they catch a reasonable chunk of the boring stuff so your human review time can focus on architecture and intent.

    Cursor is worth trying if you’re on a larger TypeScript project where codebase-wide context matters. Just don’t expect it to replace the kind of holistic design thinking that comes from actually understanding what your interface is supposed to do for a user.

    The tools are good enough to use. They’re not good enough to trust unattended. That distinction is doing a lot of work in 2026, and any developer who forgets it is going to spend a frustrating afternoon debugging something a very confident AI told them was fine.

    Frequently Asked Questions

    Are AI code review tools good enough to replace human code review in 2026?

    No, not reliably. They catch common syntax issues, anti-patterns, and some logic errors well, but they miss accessibility problems, performance implications, and design-system-specific rules. They work best as a first pass before a human reviewer looks at a PR.

    Is GitHub Copilot's code review feature worth paying for as a UK freelancer?

    If you’re already using Copilot for autocomplete, the review features are included and genuinely add value at no extra cost. For pure code review without the editor features, CodeRabbit’s free tier is worth trying first.

    How does Cursor differ from GitHub Copilot for frontend code review?

    Cursor reads your entire codebase for context, not just the current file, which makes it stronger at catching cross-file issues and misused patterns. Copilot’s PR integration is more lightweight and easier to fit into an existing GitHub workflow without changing your editor.

    Do AI code review tools understand TypeScript properly?

    They handle TypeScript meaningfully better than plain CSS or browser-compatibility edge cases. Type inference suggestions from Cursor in particular are quite strong. That said, complex generic types and conditional types can still confuse them, always verify anything non-trivial.

  • The Typography Stack in 2026: How to Choose and Pair System Fonts Without Looking Cheap

    The Typography Stack in 2026: How to Choose and Pair System Fonts Without Looking Cheap

    Here’s the thing that nobody says out loud: most web font implementations are a performance tax in disguise. You add a Google Fonts <link> in the <head>, you pick something that looks nice in Figma, and then you wonder why your Largest Contentful Paint is a disaster. I’ve watched this play out on dozens of projects, and the pattern is almost always the same. The typography looks considered, the loading experience does not.

    Building a solid system font stack in 2026 isn’t about giving up on brand expression. It’s about being smarter with what you load, when you load it, and how you fall back gracefully when things go wrong. This guide walks through the actual mechanics of doing that, from stack construction to subsetting to font-display strategy, with enough detail to be genuinely useful rather than just vaguely inspirational.

    Designer reviewing a system font stack layout on a widescreen monitor
    Photo by Miguel Á. Padriñán on Pexels

    Why system fonts deserve more respect than they get

    The reputation of system fonts is stuck somewhere around 2014, when using them felt like admitting defeat. That narrative is outdated. The current default system stacks are genuinely good. Apple’s San Francisco is a masterclass in legibility at small sizes. Segoe UI Variable (shipping with Windows 11) is properly optical-size aware. Inter, whilst technically a web font, ships natively in several Linux environments and is increasingly used as a system default in design tools.

    A well-constructed system font stack in CSS looks something like this:

    font-family:
      system-ui,
      -apple-system,
      BlinkMacSystemFont,
      'Segoe UI Variable',
      'Segoe UI',
      Roboto,
      Oxygen,
      Ubuntu,
      sans-serif;

    That stack costs zero bytes, loads in zero milliseconds, and renders without layout shift. On a content-heavy site, that’s worth a lot. The Core Web Vitals improvement alone can be significant, particularly for Cumulative Layout Shift (CLS) and LCP, both of which get hammered by render-blocking font requests.

    When you actually need a web font (and when you don’t)

    I’d argue the honest answer is: less often than you think, and almost never for body copy. System fonts at 16px with sensible line-height and letter-spacing are perfectly readable. The use cases where a web font genuinely earns its keep are narrower than most designers admit.

    Genuine reasons to load a web font include: a logotype-adjacent display face that’s core to the brand identity, a distinctive serif for editorial contexts where the personality of the letterform matters, or a monospaced font for code samples where system options (Courier New, I’m looking at you) are visually terrible. Body copy at 16–18px? The system font stack wins almost every time.

    The question I ask on every project is whether a user could actually tell the difference between the web font and a well-configured system font at reading distance. More often than not, the answer is no. What they will notice is a flash of invisible text or a layout jump. That’s the trade you’re making.

    CSS code showing a system font stack configuration in a dark-theme editor
    Photo by Pixabay on Pexels

    How to load web fonts without destroying your performance scores

    When a web font is genuinely justified, the loading strategy matters as much as the font itself. There are three levers that make the biggest difference: self-hosting, subsetting, and font-display.

    Self-host everything

    Stop using Google Fonts via CDN. It creates a cross-origin request that costs you a DNS lookup, a TCP connection, and potentially a TLS handshake before a single byte of font data arrives. Self-host the files on your own domain instead. Tools like google-webfonts-helper generate the CSS and font files you need in seconds. Your font is now one fewer external dependency, and you have full control over caching headers.

    Subset aggressively

    A full variable font file can be 200–400KB. For a UK-facing site serving primarily Latin-script content, you need maybe 20% of that. The unicode-range descriptor in your @font-face rule tells the browser which characters are actually in each file, so it only downloads the subset it needs. Use pyftsubset from the fonttools library to strip everything you don’t need. A Latin subset for English content typically covers U+0020–U+00FF, plus U+2013–U+2122 for typographic punctuation. That’s it.

    For a typical paragraph-weight subset, I regularly get files under 20KB. That’s a very different conversation than serving 300KB of font data for characters your users will never see.

    Use font-display correctly

    The font-display descriptor controls what happens whilst your web font loads. The options matter:

    • swap renders immediately in a fallback, then swaps when the web font arrives. Fast but causes layout shift.
    • optional gives the browser a very short window to load the font; if it doesn’t arrive in time, the browser uses the fallback for the entire page load. Zero layout shift, and on repeat visits the cached font renders immediately. This is my default for body fonts.
    • block renders invisible text for up to 3 seconds. Avoid it. It’s what’s killing your LCP on slow connections.

    For display fonts where the visual difference genuinely matters, swap is acceptable if you’ve done the work to minimise the metric mismatch between your web font and fallback. Which brings us to the next bit.

    Matching your fallback metrics to eliminate layout shift

    The biggest cause of font-related CLS is metric mismatch: your web font has different line-height, letter-spacing, or ascender/descender values than the system font rendering in its place. When the web font arrives, everything reflows. That’s the jump users hate.

    CSS now has size-adjust, ascent-override, descent-override, and line-gap-override descriptors inside @font-face, and they’re specifically designed for this. You apply them to a local fallback font definition to make it metrically equivalent to your web font. The Malte Ubl approach of using the Font Style Matcher is worth reading, and the Fontaine library automates this for build pipelines.

    A simplified example for matching Inter with a system fallback:

    @font-face {
      font-family: 'Inter Fallback';
      src: local('Arial');
      ascent-override: 90.2%;
      descent-override: 22.48%;
      line-gap-override: 0%;
      size-adjust: 107.4%;
    }
    
    body {
      font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif;
    }

    The layout is now almost identical before and after the web font loads. CLS drops to near zero. This is the kind of detail that separates a properly engineered typography stack from one that just happens to look okay in Figma. Incidentally, if you want to see this principle applied at scale, the approach connects directly to the same performance discipline I mentioned in our variable fonts deep dive, which covers how font axes interact with rendering at different viewport sizes.

    Pairing system fonts with brand type: a practical approach

    The pairing model I’ve settled on for most UK product work is a single web font for display headings (H1, H2, hero text) and a system font stack for everything else: body copy, UI labels, form inputs, navigation. This gives you brand personality where it’s visible, and zero performance cost where it isn’t.

    For display type, a variable font is almost always the better choice over a static weight. You get the full weight and width axis with a single file. Fonts like Cabinet Grotesk, Fraunces, or Anybody Variable cover a huge range of expressive territory at roughly 40–80KB subsetted. Pair that against system-ui for body and the contrast is actually better than two web fonts fighting for attention.

    The same principle applies to design systems built in Figma. If your design system is using a bespoke web font for every text style, someone on the engineering side is eventually going to raise a performance ticket. Getting ahead of that in the design phase, by deliberately scoping which styles use the web font and which defer to system defaults, is much cleaner than retrofitting it later. The same discipline that makes a good design system in Figma applies here: decide at the token level which typefaces are brand-critical and which are functional.

    On a practical note: some niches have specific loading constraints that make this even more pressing. Anyone building content-heavy landing pages under heavy traffic, whether that’s e-commerce, media, or specialist verticals like vape seo where organic performance is critical, will feel font loading overhead in real conversion metrics. It’s not an abstract concern.

    Preloading the right files

    If you’ve subsetted correctly, you can preload your critical font files without much guilt. The pattern is a <link rel="preload"> in the document <head>, pointing at your subset WOFF2 file with as="font" and crossorigin. This tells the browser to fetch it as early as possible, even before the CSS is parsed.

    Only preload what’s above the fold. Preloading three font weights because they all appear somewhere on the page is still a mistake. For most sites, one or two files is the right ceiling. The web.dev font best practices guide covers the nuances of preload priority, and it’s worth reading alongside your Lighthouse report rather than in isolation.

    The goal isn’t a font system that’s technically impressive. It’s one that users never think about because the text was simply there, readable and on-brand, from the first paint. That’s harder than it sounds, but the tooling in 2026 makes it genuinely achievable without exotic build steps or compromising visual quality.

    Frequently Asked Questions

    What is a system font stack and why should I use one?

    A system font stack is a CSS font-family declaration that references fonts already installed on the user’s operating system, such as San Francisco on macOS or Segoe UI on Windows. Because no files need to be downloaded, there’s zero loading time and no risk of layout shift from font swapping. For body copy especially, modern system fonts are high-quality and save significant performance overhead.

    Do system fonts hurt brand identity?

    Not if you’re strategic about where you use them. The approach most experienced product teams use is to load a single web font for display headings and marketing-facing text, while using a system font stack for body copy, labels, and UI elements. You preserve brand character where it’s most visible, and avoid paying a performance cost on the parts users don’t consciously notice.

    What is font-display: optional and when should I use it?

    The font-display: optional descriptor gives the browser a very short window (typically around 100ms) to load a web font; if it doesn’t arrive in time, the fallback is used for that entire page load and no swap occurs. On subsequent visits, the cached font renders immediately with no shift. It’s the best choice for body fonts because it eliminates Cumulative Layout Shift entirely whilst still serving the web font to most repeat visitors.

    How do I stop my web fonts causing layout shift (CLS)?

    The main cause is metric mismatch between your web font and its fallback. Use the CSS descriptors ascent-override, descent-override, and size-adjust inside a local @font-face rule to make your system fallback font metrically match your web font. This means the layout stays identical before and after the web font loads, bringing CLS close to zero. Tools like Fontaine can automate this at build time.

  • How to Build a Deployable Chrome Extension From Scratch in 2026: A UK Developer’s Walkthrough

    How to Build a Deployable Chrome Extension From Scratch in 2026: A UK Developer’s Walkthrough

    Chrome extensions are one of those rare bits of software where the gap between “idea” and “shipped product” is genuinely small. A weekend, a decent text editor, and some patience with the Chrome Web Store review queue is all it takes. But Manifest V3, Google’s current extension platform, has enough sharp edges that I’ve seen experienced developers waste a full day on avoidable mistakes. This walkthrough covers the complete build pipeline: folder structure, service workers, permissions, icon design, the popup UI, and the specifics of publishing through a UK developer account. Let’s get into it.

    Developer building a Chrome extension Manifest V3 project on a laptop at a desk
    Photo by Christina Morillo on Pexels

    What Manifest V3 actually changes

    If you last built a Chrome extension pre-2023, the mental model shift here is real. Manifest V3 replaced persistent background pages with service workers. That means your background script now has a lifecycle, it wakes up, does work, and gets terminated. You cannot store state in a global variable and expect it to persist across events. That tripped me up the first time.

    The other big change is the content security policy and the removal of webRequestBlocking for most developers. Ad blockers were the headline casualty, but for the overwhelming majority of extensions, productivity tools, tab managers, colour pickers, form helpers, none of that matters. What matters is that you use chrome.storage.local or chrome.storage.session instead of memory, and that you structure your service worker around event listeners rather than long-running logic.

    Setting up the folder structure

    Chrome extensions have a flat-ish structure. Here is what I use as a starting point:

    my-extension/
    ├── manifest.json
    ├── background.js
    ├── popup/
    │   ├── popup.html
    │   ├── popup.js
    │   └── popup.css
    ├── content/
    │   └── content.js
    └── icons/
        ├── icon16.png
        ├── icon32.png
        ├── icon48.png
        └── icon128.png

    The manifest.json is the entry point for everything. A minimal but real Manifest V3 file looks like this:

    {
      "manifest_version": 3,
      "name": "My Extension",
      "version": "1.0.0",
      "description": "Does something useful.",
      "permissions": ["storage", "activeTab", "scripting"],
      "background": {
        "service_worker": "background.js"
      },
      "action": {
        "default_popup": "popup/popup.html",
        "default_icon": {
          "16": "icons/icon16.png",
          "32": "icons/icon32.png",
          "48": "icons/icon48.png",
          "128": "icons/icon128.png"
        }
      },
      "icons": {
        "16": "icons/icon16.png",
        "48": "icons/icon48.png",
        "128": "icons/icon128.png"
      }
    }

    Request only the permissions you actually need. The Chrome Web Store reviewers check this, and users see permission prompts. activeTab is far less scary to users than tabs; use the narrower one if you can.

    Chrome extension Manifest V3 service worker code shown in a dark code editor
    Photo by Godfrey Atima on Pexels

    Writing the service worker

    Your background.js registers event listeners at the top level. That is it. Any logic that needs to run when something happens goes inside those listeners.

    chrome.runtime.onInstalled.addListener(() => {
      chrome.storage.local.set({ enabled: true });
    });
    
    chrome.action.onClicked.addListener(async (tab) => {
      const { enabled } = await chrome.storage.local.get('enabled');
      await chrome.storage.local.set({ enabled: !enabled });
    });

    If you need to communicate between the service worker and a content script, use chrome.runtime.sendMessage and chrome.runtime.onMessage. Keep those message payloads small and serialisable, no DOM nodes, no class instances.

    Building the popup UI

    The popup is just an HTML file. It renders in a small window when the user clicks the extension icon, with a max width of 800px and a max height of 600px in practice. I treat it like a tiny web app: semantic HTML, a small CSS file, and a JavaScript module that talks to storage and the background via messages.

    One thing worth knowing: the popup re-renders from scratch every time it opens. Read your state from chrome.storage in a DOMContentLoaded listener, not in a module-level variable. This is where the service-worker mental model bleeds into the popup too, nothing is persistent in memory. For anything more complex than a toggle, I’ve started reaching for a small reactive state pattern. Nothing fancy, just a single render(state) function that updates the DOM whenever storage changes. If you want full component structure, you can bundle a tiny framework like Preact into the popup directory, but for most tools that is overkill.

    On the design side: popup UIs are brutally small. Every pixel is load-bearing. I wrote recently about how most product teams get icon systems wrong, and extension popups are where that really bites, unclear icons in a 400px-wide interface with no room for labels are a usability disaster. Use 20px minimum touch targets, high-contrast text, and a single clear primary action per screen.

    Icon design for the Chrome Web Store

    You need four icon sizes: 16px, 32px, 48px, and 128px. The 128px version is what the Web Store displays on your listing page, so it needs to look polished at that size. The 16px version appears in the browser toolbar, meaning it must be readable as a silhouette, not as a detailed illustration.

    The practical approach I use: design at 128px, then manually redraw the 16px version as a simplified glyph. Do not just scale down the 128px, it will look terrible. Export as PNG with transparency. Avoid thin strokes under 2px at small sizes; they disappear entirely. If you want a deep dive on designing multi-resolution icon sets properly, the icon design guide on this blog is worth your time.

    Testing before submission

    Load your unpacked extension via chrome://extensions with Developer Mode toggled on. Reload it after every change to manifest.json; other file changes sometimes hot-reload, sometimes don’t. Use the service worker’s DevTools (there is an “inspect views” link on the extension card) to debug background script issues.

    Run through this checklist before you zip anything up: all declared permissions are actually used in code; icons exist at all four declared sizes; the popup opens without console errors; storage reads and writes work across a browser restart; and the extension does not break the pages it injects into. That last one sounds obvious, but content scripts can conflict with page CSS in ways that only appear on specific sites.

    Publishing to the Chrome Web Store as a UK developer

    You need a Google developer account, which costs a one-time fee of $5 USD (about £4 at current rates). Pay it once, and you can publish unlimited extensions. The payment goes through Google’s system, so your card needs to be set up for international transactions, most UK bank accounts handle this without any fuss.

    Create a ZIP of your extension directory (not a folder containing it, the manifest.json should be at the root of the ZIP). Upload it through the Chrome Web Store Developer Dashboard. You will need: a 440x280px promotional tile image, at least one 1280x800px screenshot, a short description (132 characters max), and a full description. The review process currently takes between a few hours and five business days for a new submission, Google does not publish a hard SLA.

    If your extension handles any user data, you must complete a privacy disclosure. UK developers should be aware that if you collect or transmit personal data, the ICO’s guidance on browser-based data collection applies to your extension just as it does to any other software product. Worth reading the ICO’s guidance for organisations before you hit publish if your extension touches anything beyond local storage.

    Version bumps are straightforward: update the version field in manifest.json, re-ZIP, and upload a new package in the dashboard. The review cycle for updates is usually faster than for initial submissions.

    Build pipeline considerations

    For a simple extension, you don’t need a bundler. Plain ES modules with "type": "module" work in content scripts and popups. But if you are importing npm packages, you need a bundler, Vite handles Chrome extension builds cleanly with the vite-plugin-web-extension plugin, which manages multi-entry-point builds and hot reloading during development.

    For teams shipping extensions alongside other web products, I’d think about how your extension fits into the broader development workflow. Good tooling compounds, the same efficiency gains that apply to web product pipelines apply here too. R2G.co.uk has an interesting take on how workflow efficiency translates to profitability that’s worth a read if you’re thinking about that side of things.

    For TypeScript users: you absolutely should be using it for anything beyond a toy extension. The Chrome extension types package (@types/chrome) is comprehensive and will save you from a class of runtime errors that are genuinely annoying to debug. If you are new to TypeScript in a design-adjacent context, the TypeScript introduction for UK freelancers on this blog is a good starting point before you wire it into a build pipeline.

    Ship something real. The Chrome extension ecosystem is less crowded than the App Store, the barrier is lower than you think, and a focused tool that solves one problem well consistently outperforms anything that tries to do everything. Pick your itch, build the fix, and get it listed.

    Frequently Asked Questions

    What is Manifest V3 and do I have to use it?

    Manifest V3 is the current version of Chrome’s extension platform, which replaced Manifest V2 with service workers instead of persistent background pages, among other changes. Google has phased out MV2 support, so yes, any new extension you build in 2026 must use Manifest V3, and existing MV2 extensions have been disabled in Chrome.

    How long does Chrome Web Store review take for a new extension?

    Review times vary from a few hours to around five business days for a first submission. Updates to existing extensions are typically reviewed faster. Google does not guarantee a specific turnaround, so factor in review time if you have a launch date in mind.

    How much does it cost to publish a Chrome extension in the UK?

    The one-time Google developer registration fee is $5 USD (roughly £4), paid through Google’s payment system. After that, there are no per-extension fees or annual costs, you can publish as many extensions as you like under the same account.

  • Astro vs Next.js in 2026: Which Framework Should UK Web Developers Actually Build With?

    Astro vs Next.js in 2026: Which Framework Should UK Web Developers Actually Build With?

    The framework debate has a new shape in 2026. A couple of years ago, the conversation was basically “are you using Next.js or are you wrong?” That’s no longer the case. Astro vs Next.js is now a genuine technical decision, and if you’re a UK freelancer or small agency picking a stack for a new project, getting it wrong costs you real time and money. I’ve spent a fair chunk of the last year shipping with both, and I have opinions.

    This isn’t a “both are great in their own way” fence-sit. I’ll tell you which one wins for which type of work, what the hosting implications look like on UK infrastructure, and why the choice matters more than most tutorials let on.

    Web developer comparing Astro vs Next.js framework code on dual monitors
    Photo by Alicia Christin Gerald on Pexels

    What each framework actually is

    Next.js, maintained by Vercel, is a React-based full-stack framework. It does server-side rendering, static generation, incremental static regeneration, API routes, middleware, edge functions, the lot. It’s been the default choice for serious React projects for years, and the ecosystem around it is enormous.

    Astro is something different. It’s an islands-architecture framework that ships zero JavaScript to the browser by default. You write components in whatever flavour you fancy (React, Svelte, Vue, or plain HTML) and Astro only hydrates the interactive bits. The result is pages that are genuinely fast in a way that feels almost unfair. For content-heavy sites, the Lighthouse scores are embarrassing compared to a typical Next.js build.

    These two frameworks are not really competing for the same use case. The problem is that a lot of developers reach for Next.js out of habit, even when Astro would be the smarter pick.

    Build performance: where Astro genuinely wins

    For a marketing site, a blog, a documentation hub, or a portfolio, Astro is faster to build with and faster to serve. Full stop. The islands architecture means your pages arrive in the browser as HTML with CSS, and only the components that need interactivity (a search bar, a contact form, a live pricing widget) get JavaScript injected. Everything else is static.

    I rebuilt a client’s agency site last autumn, previously on Next.js with a bloated component tree, and the move to Astro dropped Time to First Byte from around 800ms to under 120ms on a cold UK edge. The Core Web Vitals went green across the board without any heroics. If you’ve been wrestling with layout performance issues at the CSS level, the framework layer matters even more than you might think.

    Next.js, by contrast, carries React into the browser on every page by default. Even with server components (which are genuinely clever and have improved things substantially), you’re shipping more JavaScript than Astro would. For a SaaS dashboard, a social platform, or anything with heavy real-time state, that’s a reasonable trade. For a brochure site? It’s not.

    Hosting costs on UK providers

    This is where the decision gets practical and where UK developers often get stung. Next.js is heavily optimised for Vercel’s own infrastructure. That’s not a criticism, Vercel is a good product, but their free tier has limits that bite fast, and their Pro tier at roughly £17 per user per month adds up for a small agency with multiple projects running.

    Alternatives like Netlify or UK-adjacent providers such as Cloudflare Pages handle Next.js, but you lose some features (middleware running on Vercel’s edge, ISR at the granular level) or find workarounds necessary. The Vercel lock-in is real, even if the company insists otherwise.

    Astro sites, because they’re largely static output, deploy anywhere. Cloudflare Pages for free. An S3-compatible UK bucket behind a CDN. Your client’s existing cPanel host, if it comes to that. I’ve run Astro sites on Hetzner VPS instances in their UK datacentre (Falkenstein is technically Germany, but their London presence is growing) for under £5 per month including everything. Next.js apps with server-side requirements need a Node runtime, which means either a managed platform with associated costs or your own self-hosted VPS setup that you need to maintain.

    For UK freelancers billing smaller clients, the hosting cost difference can genuinely affect whether a project is profitable at the price point the client expects.

    Which projects actually suit each framework

    Astro wins for: marketing sites, landing pages, blogs, documentation, portfolios, news sites, e-commerce storefronts where the cart is a third-party embed, and any project where content is the primary product. If the page is mostly read, not interacted with, Astro should be your first choice.

    Next.js wins for: SaaS products with authenticated dashboards, apps with real-time data requirements, anything with complex server-side logic that needs to live close to the data layer, and projects where the team is already deep in the React ecosystem and moving away would cost more than it saves. It’s also the better pick when you’re building something that will grow into a full application, because its routing and API layer scale well.

    A pattern I see constantly in UK agency work: someone specs a Next.js build for a 12-page product site because the team knows React. The site ends up with a bundle size north of 800KB for pages that are basically text and images. Then they spend a sprint optimising what the framework choice created. Astro wouldn’t have done that.

    There’s also a middle path worth mentioning. Some teams are now running hybrid setups: Astro for the public-facing marketing and content pages, Next.js (or a lighter alternative like Remix) for the authenticated product area. The two can coexist on subdomains or subdirectories, and if you’re thoughtful about the split, you get the best performance characteristics of both without too much architectural overhead.

    The developer experience difference

    Next.js has the larger ecosystem, more Stack Overflow answers, and a bigger community in the UK dev scene. If you’re hiring or collaborating, finding someone familiar with Next.js is easier. The Astro community is growing fast, and the documentation is genuinely excellent, but it’s still smaller.

    Astro’s component model, where you can mix React, Svelte, and vanilla components in the same project, sounds chaotic but is surprisingly workable. For a freelancer who has built up a library of components across different frameworks, it’s actually liberating. I’ve pulled in a React data-viz component alongside a Svelte interactive widget and it just worked. That flexibility is part of why Astro suits project-based freelance work so well.

    One thing I’ve noticed: clients who care about their web presence (the ones spending money on cookie free display advertising UK services and similar performance-focused digital channels) are increasingly asking specific questions about page speed. Astro makes it much easier to hit those targets without a performance engineering sprint at the end of every project.

    For teams already building design systems or working across multiple products, the framework choice also interacts with your component architecture. If you’re maintaining icon systems or a full icon design system across multiple surfaces, understanding how your framework handles component hydration will affect how you structure those shared resources.

    My actual recommendation

    Default to Astro for anything content-first. The performance advantages are real, the hosting costs are lower, and the developer experience is genuinely good. You won’t spend three hours debugging why your bundle includes a library you imported once in a layout file.

    Use Next.js when the project genuinely needs it: persistent server-side state, complex auth flows, real-time features, or a codebase that’s going to grow into something an engineering team maintains long-term.

    The Astro documentation is worth an afternoon of your time even if you’re not planning to use it immediately. You’ll come away with a much clearer sense of what “islands architecture” actually means in practice, which makes the Next.js vs Astro decision far more intuitive when a new project lands on your desk.

    Both frameworks are mature, production-ready, and supported well enough that you won’t be stranded. The choice is about fit, not about quality. Pick the tool that matches the problem, not the one you’re most comfortable with.

    Frequently Asked Questions

    Is Astro faster than Next.js for production sites?

    For content-heavy, mostly static sites, yes, Astro ships zero JavaScript by default, which results in dramatically smaller payloads and faster load times. For highly interactive apps like SaaS dashboards, Next.js with server components is more competitive because it’s designed for that kind of complexity.

    Can I host an Astro site on cheap UK hosting?

    Yes, and that’s one of Astro’s main advantages. Because the output is static HTML, CSS, and minimal JS, you can host on Cloudflare Pages (free tier is generous), any S3-compatible storage, or a basic UK VPS for a few pounds per month. Next.js apps with server-side features need a Node runtime environment, which typically costs more.

    Should I use Astro or Next.js for a SaaS product?

    Next.js is the stronger choice for a full SaaS product with authenticated users, complex server logic, and real-time data requirements. That said, many SaaS teams use Astro for their public marketing site and Next.js only for the authenticated app area, which is a sensible split.

    Does Astro work with React components?

    Yes. Astro supports React, Svelte, Vue, Solid, and plain HTML components in the same project simultaneously. Only components that need client-side interactivity are hydrated; everything else renders as static HTML at build time.

  • Designing Offline-First Apps: Why UK Developers Should Be Building for Patchy Connectivity

    Designing Offline-First Apps: Why UK Developers Should Be Building for Patchy Connectivity

    Britain has a connectivity problem it keeps pretending it doesn’t have. You can be forty minutes outside Leeds, somewhere sensible and entirely on the map, and your mobile signal will flatline completely. According to Ofcom’s Connected Nations reports, significant portions of rural England, Scotland, and Wales still experience 4G not-spots or broadband speeds that make a dial-up modem look ambitious. If you’re building apps for a UK audience and you’re not thinking about offline-first app design UK, you’re quietly breaking the experience for a chunk of your users every single day.

    The good news: the web platform caught up. Service workers, IndexedDB, the Cache API, and background sync have matured considerably. Building an offline-capable application in 2026 is no longer a heroic engineering effort reserved for Google and large infrastructure teams. It’s a design and architecture decision you can make at the start of a project, and one that pays compounding dividends in user trust.

    Construction worker using a tablet on a UK building site, illustrating offline-first app design UK for field use

    What Does Offline-First Actually Mean?

    Offline-first doesn’t mean your app works exclusively without a connection. It means the app treats network availability as an enhancement rather than a prerequisite. The mental model shifts: instead of “assume connected, handle errors when not”, you build for “assume nothing, sync when possible”. That inversion changes almost everything about your data flow, your UI states, and your error handling strategy.

    Compare this to the more common “offline-tolerant” approach, where developers bolt on a friendly error screen and call it done. That’s not offline-first. That’s just dressed-up failure. Proper offline-first means a user in a Snowdonia valley or on a Highland train can read their data, create new records, and edit existing ones. When connectivity returns, the app catches up. The user never stares at a spinner waiting for permission to use their own software.

    Service Workers: The Engine Room of Offline Capability

    Service workers are background scripts that sit between your app and the network, intercepting requests and deciding what to do with them. They’re the backbone of any serious offline-first architecture. A service worker can cache your app shell on first load, serve stale content when the network is unavailable, and queue outgoing requests to replay once connectivity resumes.

    The setup is conceptually straightforward. Register your service worker in your main JavaScript entry point, then implement a fetch event listener to define your caching strategy. For static assets (your JS bundles, CSS, fonts), a cache-first strategy is sensible. For API calls, a stale-while-revalidate approach gives you speed plus freshness: serve what’s cached immediately, then fetch and update in the background. For write operations, background sync via the SyncManager API queues failed requests and retries them automatically when the connection recovers.

    Libraries like Workbox (from Google, but widely used in UK product teams) abstract much of this boilerplate. You define your caching strategies declaratively, and Workbox handles the plumbing. For most teams, this is the pragmatic starting point rather than hand-rolling everything.

    Local Storage Strategies: IndexedDB Is Where You Actually Live

    The localStorage API gets reached for instinctively, but it’s synchronous, limited to roughly 5MB, and stores only strings. For any serious offline data layer, IndexedDB is the right tool. It’s asynchronous, stores structured data including blobs, and has no practical size ceiling for most use cases (browsers impose soft limits, but you’re typically looking at hundreds of megabytes).

    Developer inspecting service worker and IndexedDB data in browser devtools as part of offline-first app design UK

    Working with raw IndexedDB is famously verbose, which is why wrapper libraries exist. Dexie.js is my personal favourite for this: clean promise-based API, excellent TypeScript support, and an active community. You define your schema as a simple object, run version migrations, and query with a syntax that looks almost like SQL’s friendlier sibling. For React-based projects, RxDB adds reactive querying on top of IndexedDB, which pairs nicely with component-driven UIs.

    One design decision worth spending time on: what do you actually persist? You don’t want to naively dump your entire server state into the client. Think about the user’s current session, their most recently accessed records, and anything they’d need to do their core job. A field-based app, for instance, might cache the last thirty jobs rather than the full historical archive. Scope your local database to what’s genuinely useful offline, and you’ll save yourself a world of pain later.

    This is exactly the kind of consideration that matters for industries operating in physically demanding or remote environments. Construction and building services firms, for example, need site workers to log inspection data, capture photos, and update compliance records even in basements and rural plots with no signal. Asbestos Compliance Solutions Ltd, a specialist asbestos services provider based in Mansfield, Nottinghamshire, is precisely the type of operation where offline-first app design becomes business-critical rather than a nice-to-have. A surveyor working on a construction site or inside a building earmarked for demolition needs an app that saves their asbestos survey data locally and syncs it to the back end once they’re back in signal. You can find more about their specialist services at https://asbestoscompliancesolutions.co.uk/, the point being that the field-to-office data flow is one of the strongest real-world arguments for building offline-first.

    Sync Conflict UI: The Design Problem Nobody Wants to Talk About

    Here’s where offline-first gets genuinely hard, and where most tutorials quietly stop. If two users edit the same record while both are offline, and then both sync at the same time, you have a conflict. Last-write-wins is the laziest resolution strategy. It works sometimes. It silently discards data the rest of the time.

    A more robust approach is a CRDT (Conflict-free Replicated Data Type) model, where data structures are designed to merge without conflicts by their mathematical nature. Libraries like Automerge and Yjs implement CRDTs in JavaScript and are increasingly viable for product-scale applications. For simpler scenarios, you can track vector clocks or timestamps and surface conflicts explicitly to the user.

    That last option is a UI design challenge as much as an engineering one. When a conflict exists, your interface needs to show the user what happened in plain language, present both versions of the record, and let them choose or merge. This is not glamorous design work. It won’t win you a Webby Award. But it’s the difference between an app that’s trustworthy in the field and one that occasionally loses data in ways users can never quite prove.

    For building and construction-adjacent tools, where compliance records and specialist services data carry regulatory weight, a sync conflict that silently overwrites an asbestos survey result could have consequences well beyond a frustrated user experience. The design of conflict resolution UI should be proportional to the stakes of the data.

    Offline UX Signals: Telling Users What’s Actually Happening

    Beyond the technical architecture, offline-first has a UX communication layer that often gets undercooked. Users need to know when they’re offline, when their actions have been queued rather than confirmed, and when the sync has completed. The navigator.onLine API gives you a basic boolean, though it’s worth knowing this can give false positives (a device connected to a router with no internet access will report online). Listening to the online and offline window events is more reliable in practice.

    Visually, a persistent but unobtrusive status indicator works well: something in the interface chrome that shows “Saving locally” or “Syncing” without interrupting the user’s flow. Avoid modal alerts for this. Nobody wants a dialogue box telling them they’re in a tunnel. The app should handle it quietly and surface the status when the user actually looks for it.

    Pending action queues deserve visibility too. If a user has submitted three records while offline, a small badge or count somewhere accessible gives them confidence their work hasn’t vanished. When sync completes, a brief toast notification closes the loop. These are small interactions, but they’re the entire psychological foundation of trusting an offline-first system.

    Progressive Web Apps and the UK Distribution Opportunity

    One last thought: PWAs (Progressive Web Apps) are the natural delivery vehicle for offline-first experiences on the web. They install to the home screen, run in a standalone window, and unlock the service worker capabilities described above. For UK developers building field tools, B2B utilities, or anything serving users in connectivity-challenged environments, a PWA sidesteps app store friction entirely.

    Asbestos Compliance Solutions Ltd and similar specialist services businesses in the building and construction sector represent a whole category of professional tools that would benefit enormously from this model. A well-built PWA for asbestos survey management, distributed directly via a URL, updated silently in the background, and fully functional on a construction site with no signal, is a genuinely better product than a native app requiring Play Store or App Store submission cycles.

    The technical foundations are solid. Service workers are supported across all modern browsers. IndexedDB is ubiquitous. Background sync has broad coverage. The remaining barrier is mostly cultural: the assumption that “proper” apps need a server call for every interaction. Rural Britain, and the millions of professionals who work in basements, tunnels, and remote sites, would politely like you to rethink that assumption.

    Frequently Asked Questions

    What is offline-first app design and how is it different from just caching?

    Offline-first app design means building an application where local data access is the default behaviour, and network requests are an enhancement rather than a requirement. Basic caching typically just stores static assets; offline-first goes further by persisting application data locally, queuing write operations, and syncing changes when connectivity returns.

    Which UK areas have the worst mobile and broadband connectivity for app users?

    According to Ofcom’s Connected Nations data, large parts of rural Scotland, Wales, and Northern England have persistent 4G not-spots and below-average broadband speeds. Areas including the Scottish Highlands, mid-Wales, and parts of Yorkshire and Cumbria regularly appear in coverage gap reports, making offline-first design especially relevant for apps targeting users in these regions.

    How do service workers enable offline functionality in web apps?

    Service workers are background scripts that intercept network requests made by your application. They can serve cached responses when the network is unavailable, implement strategies like cache-first or stale-while-revalidate for different resource types, and queue failed write requests using the Background Sync API to replay them once connectivity is restored.

  • How to Self-Host Your Design Stack on a UK VPS: Penpot, Gitea, and Plausible Without the SaaS Bill

    How to Self-Host Your Design Stack on a UK VPS: Penpot, Gitea, and Plausible Without the SaaS Bill

    Figma’s pricing has crept up year on year. GitHub’s free tier comes with caveats. Google Analytics 4 remains a GDPR headache that most UK studios are quietly sweating over. If you’ve been running a small design or development practice and watching your SaaS subscriptions quietly devour your margin, there’s a genuinely viable escape hatch: self-hosting your core stack on a UK-based virtual private server. This guide walks through how to self host design tools on a UK VPS, specifically Penpot (your Figma replacement), Gitea (your GitHub replacement), and Plausible (your GA4 replacement), with enough technical detail to actually get you moving.

    The total monthly outlay for a mid-spec VPS from a UK provider like Mythic Beasts, Memset, or Hetzner’s UK edge nodes typically runs between £8 and £25 depending on RAM. Compare that to Figma’s Organisation tier at roughly £40 per editor per month, GitHub Team at around £3.50 per user, and GA4’s 360 tier when you outgrow the free limits. The maths tilts hard in favour of self-hosting once you have more than two or three people on a team.

    Developer setting up self host design tools on UK VPS with Penpot in a London office

    Choosing Your UK VPS Provider

    Where your server physically lives matters for two reasons: latency for your UK team, and data residency under UK GDPR. The ICO’s guidance is clear that personal data should remain in jurisdictions with adequate protections, and keeping it on UK soil is the simplest way to stay compliant without writing a lengthy transfer impact assessment.

    For this walkthrough, assume a VPS with 4GB RAM, 2 vCPUs, and 80GB SSD storage. That comfortably runs all three services simultaneously. You’ll want Ubuntu 24.04 LTS as your base OS, Docker and Docker Compose installed, and a domain with DNS pointing at your server’s IP. Nginx will act as a reverse proxy in front of everything, with Let’s Encrypt handling SSL via Certbot.

    Once your VPS is provisioned, update the system and install Docker:

    sudo apt update && sudo apt upgrade -y
    sudo apt install -y docker.io docker-compose-v2 nginx certbot python3-certbot-nginx
    sudo systemctl enable docker
    sudo usermod -aG docker $USER

    Setting Up Penpot on Your VPS

    Penpot is the open-source design and prototyping tool from the Spanish studio Kaleidos. It’s browser-based, handles vector work and component libraries, and supports real-time collaboration. It won’t do everything Figma does, but for most UI design workflows it’s genuinely solid. The ICO would be pleased: no data leaving your server.

    Penpot ships an official Docker Compose configuration. Grab it:

    mkdir ~/penpot && cd ~/penpot
    wget https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
    wget https://raw.githubusercontent.com/penpot/penpot/main/docker/images/config.env

    Open config.env and set the PENPOT_PUBLIC_URI to your subdomain, something like https://design.yourdomain.co.uk. Also set PENPOT_FLAGS to include enable-registration initially so you can create your admin account, then flip it to disable-registration afterwards. Run it:

    docker compose -f docker-compose.yaml up -d

    Penpot binds to port 3449 by default. Your Nginx config for this subdomain should proxy to that port, with SSL terminated at the Nginx layer. Once Certbot has issued your certificate and you’ve reloaded Nginx, your design.yourdomain.co.uk should show the Penpot login screen. Create your admin account, invite your team, and you’re running a fully functional self host design tools UK VPS setup in under an hour.

    Deploying Gitea as Your Private Git Host

    Gitea is a lightweight, self-hosted Git service written in Go. It’s fast, it uses minimal resources, and it has a web interface that feels close enough to GitHub that your team won’t mutiny. It handles repositories, issues, pull requests, webhooks, and CI integration with Gitea Actions.

    Create a compose file at ~/gitea/docker-compose.yml:

    version: "3"
    services:
      gitea:
        image: gitea/gitea:latest
        environment:
          - USER_UID=1000
          - USER_GID=1000
          - GITEA__database__DB_TYPE=sqlite3
        volumes:
          - ./data:/data
        ports:
          - "3000:3000"
          - "222:22"
        restart: always

    SQLite works fine for teams under about 20 users. If you’re running something bigger, swap to PostgreSQL. The SSH port mapping (222:22) means your team will push with git remote add origin ssh://[email protected]:222/yourorg/repo.git. Nginx proxies HTTP traffic on port 3000 under its own subdomain. Run docker compose up -d, navigate to the web installer at your subdomain, and configure your instance. Disable public registration immediately.

    Installing Plausible Analytics

    Plausible is the privacy-first, GDPR-compliant analytics tool that’s been winning converts from Google Analytics for a few years now. The self-hosted version is functionally identical to the cloud product. For UK studios with clients asking about cookie banners and data processing agreements, hosting Plausible on your own UK VPS means the conversation becomes dramatically simpler: the analytics data never leaves your infrastructure.

    According to the ICO’s UK GDPR guidance, analytics tools that collect personal data without consent require explicit opt-in. Plausible’s cookieless approach sidesteps most of this entirely, but self-hosting adds another layer of confidence for clients.

    Clone the Plausible hosting repo:

    git clone https://github.com/plausible/community-edition ~/plausible
    cd ~/plausible
    cp plausible-conf.env.example plausible-conf.env

    Edit plausible-conf.env. Set BASE_URL to your analytics subdomain, generate a SECRET_KEY_BASE using openssl rand -base64 48, and configure your SMTP details for email verification. Then:

    docker compose up -d

    Plausible runs on port 8000. Nginx proxy config follows the same pattern as the others. Once it’s live, embed the lightweight tracking script (<script defer data-domain="yourclient.co.uk" src="https://analytics.yourdomain.co.uk/js/script.js"></script>) in your clients’ sites and you’re collecting privacy-respecting, UK-hosted analytics.

    Nginx Reverse Proxy Config Pattern

    Each service follows the same Nginx block structure. Here’s the template:

    server {
        listen 443 ssl;
        server_name service.yourdomain.co.uk;
    
        ssl_certificate /etc/letsencrypt/live/service.yourdomain.co.uk/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/service.yourdomain.co.uk/privkey.pem;
    
        location / {
            proxy_pass http://localhost:PORT;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    
    server {
        listen 80;
        server_name service.yourdomain.co.uk;
        return 301 https://$host$request_uri;
    }

    Replace PORT with 3449 for Penpot, 3000 for Gitea, and 8000 for Plausible. Run sudo certbot --nginx -d service.yourdomain.co.uk for each subdomain. Certbot will modify the block automatically to include the certificate paths. Reload Nginx after each one.

    Backups and Maintenance

    Self-hosting means you own the failure. Set up a daily cron job to dump Docker volumes to a compressed archive and rsync it off-server to a separate UK storage bucket or a second VPS. Mythic Beasts and Bytemark (now part of IONOS UK) both offer object storage with UK data residency. A backup that lives on the same physical host as your data is not a backup.

    Docker image updates are the other recurring task. A monthly docker compose pull && docker compose up -d across all three services keeps you on current releases without much fuss. Subscribe to the Penpot, Gitea, and Plausible release channels on their respective Git hosts so you catch security patches promptly.

    The total monthly compute bill for this three-service stack on a decent UK VPS sits somewhere around £12 to £20. For a small studio of four or five people, you’re almost certainly saving north of £100 per month compared to the equivalent SaaS tiers, and you’ve got full data sovereignty to boot. The setup overhead is a few hours, and the maintenance overhead is genuinely low once it’s running.