Category: Coding

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

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

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

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

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

    What the CrUX data actually shows for UK sites

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

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

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

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

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

    What sluggish scores are doing to conversions

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

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

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

    How to actually fix INP on a JavaScript-heavy site

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

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

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

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

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

    LCP: the quick wins that are still being missed

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

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

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

    Tools worth adding to your workflow right now

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

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

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

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

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

    Frequently Asked Questions

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

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

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

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

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

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

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

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

  • Contrast Grids and Colour Token Naming: How UK Design Teams Are Finally Solving Their Semantic Colour System Problems

    Contrast Grids and Colour Token Naming: How UK Design Teams Are Finally Solving Their Semantic Colour System Problems

    Colour decisions in product design look deceptively simple until your rebrand lands, someone switches to dark mode, and your carefully chosen palette turns into a contrast disaster that fails WCAG AA at about twelve different points. I’ve watched this happen to teams who thought they had a proper colour system, only to discover they had a colour list with a system-shaped name badge on it. There is a meaningful difference, and design system colour tokens UK teams are increasingly using to close that gap are finally mature enough to be worth writing about seriously.

    Designer reviewing design system colour tokens UK documentation on a large monitor
    Photo by cottonbro studio on Pexels

    What a real semantic colour token system actually looks like

    The core idea is layered naming. You have primitive tokens at the bottom: raw values like blue-500: #2563EB. Above that sit semantic tokens that describe function rather than appearance: colour-interactive-default, colour-surface-elevated, colour-feedback-error. The primitive never goes directly into a component. The semantic token does. The layer in between is where the magic happens, because when a rebrand requires swapping blue-500 for teal-600, you change one reference and everything that consumes colour-interactive-default updates automatically.

    This three-tier approach, often called primitive, semantic, and component-level tokens, has been championed by Salesforce’s Lightning Design System for years, but UK product teams have been slower to adopt it fully. Part of that is legacy: many British SaaS companies built their design systems before design tokens were a coherent concept, and the technical debt of retrofitting a flat Sass variable file into a structured token architecture is real. The other part is that tooling only recently caught up.

    Why naming conventions matter more than most teams realise

    Here’s where I’d argue most colour system conversations go wrong. Teams spend weeks debating whether blue-500 should be #1D4ED8 or #2563EB, and approximately twenty minutes on naming conventions. Then they ship tokens with names like primaryBlue, brandGreen, and accentColor1, which are essentially useless the moment a new designer joins or the brand team decides teal is the new blue.

    A robust naming convention is schema-first. The structure I’ve seen work best in UK teams follows: [namespace]-[category]-[property]-[variant]-[state]. So ds-colour-text-primary-hover instead of textHoverMain. The namespace scopes the token to your system and prevents collisions if you’re working in a monorepo with multiple products, which plenty of UK B2B companies are. Category and property describe what the token affects. Variant and state handle the stuff that trips everyone up: default versus disabled, rest versus hover versus focus.

    The W3C Design Tokens Community Group published their format specification to standardise how tokens are stored and transferred between tools. It’s not a finished standard yet, but it’s close enough that Figma’s variables implementation and Style Dictionary both align with it reasonably well. If you’re building a token system in 2026 and not at least glancing at that spec, you’re probably building something you’ll have to rebuild in two years.

    Contrast grids: the tooling that makes WCAG compliance tractable

    WCAG 2.1 AA requires a contrast ratio of 4.5:1 for normal text and 3:1 for large text and UI components. WCAG 2.2, which the UK public sector accessibility requirements point to for public sector digital services, doesn’t change those ratios but adds a clearer expectation that you’ve done the work systematically. The problem is that checking every foreground-background combination manually is genuinely tedious. A palette with twelve colours has 66 unique pairs. If you’re also considering dark mode, you’ve effectively doubled your matrix.

    Contrast grids, popularised by Eightshapes’ free tool, solve this by visualising the entire foreground-background matrix at once. You input your palette, and the grid shows every combination colour-coded by whether it passes AA, AAA, or fails entirely. This is enormously useful not just for compliance checking but for making design decisions. When you can see that your neutral-700 passes AA on both your surface-primary and surface-secondary but fails on surface-tertiary, you can encode that constraint directly into your token documentation rather than waiting for it to surface in a QA pass.

    Several UK product teams I’m aware of have started integrating automated contrast checking into their CI pipelines using tools like @accessibility-checker/contrast or custom scripts built on the wcag-contrast npm package. The idea is that a token file change that introduces a failing combination gets flagged in the pull request before it ever reaches a designer’s screen. That’s the right direction. Catching contrast failures at the system level rather than the component level is much cheaper to fix.

    Dark mode is where token systems either prove themselves or collapse

    Dark mode is the stress test every semantic colour token system has to pass eventually. If you’ve built a flat system where components reference primitive values directly, implementing dark mode means touching every component. If you’ve built a properly layered system, dark mode is just a second theme file that remaps your semantic tokens to different primitives.

    In practice, this looks like having two sets of semantic token values: one for light mode, one for dark. colour-surface-primary maps to neutral-100 in light and neutral-900 in dark. A CSS custom property implementation handles the swap via prefers-color-scheme or a data attribute on the root element. Style Dictionary can output both token sets as separate CSS files or as a single file with media query scoping.

    Where teams go wrong is assuming their colour palette works symmetrically in both modes. It almost never does. A contrast grid for your dark mode token mappings is not optional; it’s a separate exercise from your light mode grid, because the contrast ratios change when you flip surfaces. I’d recommend running both grids simultaneously when you’re building out a dual-mode system, and locking the acceptable pairings into your token documentation so that designers can’t accidentally use colour-text-secondary on colour-surface-overlay just because it looks fine in one mode.

    This connects directly to work happening at the component level too. If you’re already thinking carefully about how CSS container queries are reshaping component architecture for UK frontend teams, it’s worth considering how your colour token layer interacts with component-scoped theming. A component that can respond to its container’s size should probably also respond to a locally-scoped theme token rather than relying solely on document-level custom properties.

    Surviving a rebrand with a token system intact

    Rebrands are where semantic token systems pay back every hour of upfront investment. The UK financial services sector goes through brand refreshes constantly, and I’ve seen what happens to design systems that were built around hex values rather than token references. It’s not pretty. Months of find-and-replace work, inevitably inconsistent across products, with rogue legacy values lurking in edge cases until someone files a bug six months later.

    A token system handles this cleanly. Your primitive tier changes. Your semantic tier doesn’t. Your components certainly don’t. If your new brand blue is #1A56DB instead of #2563EB, you update one value, run your contrast grid to confirm nothing broke, and ship. That’s the whole thing.

    Teams building more complex multi-brand or white-label products need to go one level further. Rather than a single primitive-to-semantic mapping, they maintain multiple primitive tiers (one per brand) that all map to the same semantic layer. This is the approach used in sophisticated public sector design systems, and it scales well into the kind of multi-tenant SaaS architecture where a single codebase serves several clients with different brand guidelines.

    The toolchain that makes this tractable in 2026 is roughly: Figma variables for design-side token management, Style Dictionary for transformation and output to CSS, JSON, Swift, Kotlin or whatever your platforms need, and a contrast grid check baked into your release process. None of these tools are particularly new. The difference is that they now work well together, and the naming conventions are stable enough that a token defined in Figma actually means the same thing when it emerges from Style Dictionary on the other side.

    If you’re also thinking about how design system decisions affect the designer-developer handoff, the current state of Figma’s Dev Mode is relevant here; token visibility in dev mode has improved substantially, and a well-structured token system makes the handoff cleaner regardless of which side of the gap you’re on.

    The teams doing this well right now are not necessarily the largest UK product organisations. Some of the most rigorous colour token work I’ve come across is happening at mid-size British fintechs and healthtech companies where the contrast requirements are non-negotiable and the rebrand cycle is fast. Necessity, as always, is doing most of the architectural work.

  • Designing for the NHS App Ecosystem: What Third-Party Health Developers Need to Know About NHS Design Patterns

    Designing for the NHS App Ecosystem: What Third-Party Health Developers Need to Know About NHS Design Patterns

    If you’ve ever tried to build a companion app or integration layer for the NHS ecosystem, you’ll know the experience is somewhere between solving a puzzle with missing pieces and navigating a labyrinth designed by committee. The documentation exists. The NHS Digital Service Manual is actually one of the better-maintained design systems in the UK public sector. The problem isn’t the documentation. The problem is that most health tech startups read it the way people read terms and conditions: quickly, selectively, and with total confidence they’ve understood the important bits.

    They haven’t. I’ve seen the same mistakes repeat across a dozen health tech projects, from GP appointment add-ons to mental health tracking apps trying to slot into NHS workflows. So let’s go through what’s actually being misunderstood, because some of these errors go beyond aesthetics into genuinely risky territory.

    Tablet displaying NHS design patterns in a clinical setting
    Photo by Matheus Bertelli on Pexels

    Why NHS design patterns are not just a visual style guide

    The first and most common misconception: teams treat NHS design patterns as a branding exercise. Copy the blue. Use the logo typeface (Frutiger, or the open-source Fraunhofer approximation). Borrow the card components. Job done, looks credible.

    That’s backwards. The NHS design system is built around a set of interaction principles and a specific clinical context that most commercial UI thinking doesn’t account for at all. The GOV.UK Design System, which shares some DNA, at least gets discussed in design circles (I wrote about what GOV.UK gets right that private sector UI libraries get wrong a while back), but the NHS variant has additional constraints layered on top, because the users and stakes are different.

    Consider one obvious example: error messaging. In a typical SaaS product, an error message is a minor friction point. In a clinical UI, an error message about a dosage input or an appointment slot could cause a patient to take the wrong action. The NHS Service Manual specifies error summary patterns precisely because the consequences of user confusion in health contexts are categorically different from, say, a failed checkout flow. The pattern is not decoration. It is a risk mitigation tool.

    What health tech startups consistently get wrong about accessibility

    WCAG 2.2 compliance gets mentioned in every NHS digital service assessment, and most teams know this, at least in theory. In practice, I see two failure modes.

    The first is treating accessibility as a checklist rather than a design constraint. Teams run an automated audit with axe or Lighthouse, fix the obvious contrast ratios, add some aria-labels, and declare compliance. But automated tools catch roughly 30-40% of WCAG issues. The remainder, focus order, screen reader narrative flow, meaningful alternative text, timeout behaviour for users with cognitive impairments, only surfaces in manual testing with real assistive technology.

    The second failure mode is misunderstanding the NHS user base. NHS app users are not a homogeneous tech-savvy audience. As I covered in my piece on designing for older users and accessibility, the over-55 demographic represents a huge proportion of NHS service users, and they interact with digital interfaces in ways that invalidate many assumptions baked into modern component libraries. Small tap targets, hover-only states, icons without labels, modals that trap keyboard focus badly: all of these become genuine barriers at scale when your user base skews older and potentially has reduced motor control or vision impairment.

    Developer writing code for NHS design patterns component library
    Photo by Harold Vasquez on Pexels

    The NHS explicitly requires user research with real patients as part of service assessments. Not personas. Not assumed user journeys mapped on a whiteboard. Actual sessions with actual people. Teams that skip this stage almost always pay for it later, either in failed assessments or in post-launch complaints that damage the trust relationship with NHS partners.

    Trust signals in clinical UI: where commercial design intuitions break down

    Here’s where it gets philosophically interesting. Commercial product design has spent the last decade optimising for conversion: clear CTAs, reduced friction, bright colour on primary actions, social proof via user counts and review scores. Health tech apps that import these patterns wholesale often end up looking untrustworthy to clinical users, and sometimes to patients.

    Clinical trust operates differently. A patient interacting with a mental health app doesn’t want it to feel like a productivity tool. They want it to feel safe, unhurried, and unambiguous. This means generous whitespace (which commercial designers sometimes read as “wasted space”), plain language at a reading age appropriate for a wide public audience, and absolutely no dark patterns near anything related to consent or data sharing.

    The NHS has very specific guidance on data handling UI precisely because health data is special category data under UK GDPR. The ICO and NHS England both publish guidance on this. If your consent flows borrow the kind of nudge patterns you’d find on a subscription cancellation screen (pre-ticked boxes, vague opt-in language, confusing toggle states), you are not just creating bad UX. You are potentially creating a legal and regulatory problem. I wrote about UK consumer protection and UX design in a different context, but the principle around manipulation through UI design is directly applicable here.

    The NHS digital service standard: what the assessment process actually demands

    Teams building apps that will connect to NHS systems, or carry NHS branding, typically need to pass a Digital Technology Assessment Criteria (DTAC) review. This is not the same as simply passing an NHS design assessment, and conflating them is a common error.

    DTAC covers clinical safety (DCB0129 compliance), data protection, technical security, interoperability (FHIR R4 where relevant), and usability. The clinical safety requirement alone, which requires a named Clinical Safety Officer and a documented hazard log, catches many startups completely off guard. This is not a box-ticking exercise. A hazard log means identifying the ways your software could harm a patient if it behaves unexpectedly, and documenting what you’ve done to mitigate each risk.

    I’ve seen teams, including a couple of otherwise well-run companies that handle complex integrations, treat the DTAC process as a hurdle to clear rather than a framework that makes their product genuinely safer. The distinction matters enormously when something goes wrong. Agencies building products with technical integration complexity, like Dijitul Developments, understand that integration architecture decisions made early have compliance implications downstream, and the same is true in spades for health tech builds touching NHS infrastructure.

    Component library choices and why “looks NHS” is not enough

    The NHS frontend library is open source and available on GitHub. It covers form components, navigation patterns, warning callouts, and action links. Using it is sensible. But a few nuances trip people up.

    First, the library is React-agnostic by design, but most health tech teams are building in React or React Native. The official components are vanilla HTML/CSS/JS. There are community-maintained React wrappers, but these are not officially supported, and their fidelity to the latest NHS frontend version varies. Teams need to own the maintenance overhead if they use them.

    Second, the NHS design system has specific guidance on what components to use for clinical decision support contexts versus patient-facing contexts. Using a summary card component (designed for showing appointment details to a patient) inside a clinician-facing dashboard creates cognitive dissonance for clinical staff who have built up a mental model of what each pattern means. Context collapse in UI is a real problem that purely visual pattern-matching doesn’t prevent.

    What building well actually looks like

    The teams I’ve seen do this well share a few habits. They read the NHS service manual properly, including the content design guidance, which is where a lot of the philosophy is actually explained. They involve a clinical safety lead from the start rather than bolting one on before submission. They conduct genuine usability testing with NHS staff and patients, not just developers who have become blind to their own interface.

    They also build with the understanding that NHS design patterns are living documents. The manual updates. Guidance around things like app accessibility and FHIR integration evolves. Setting up a process to track changes, rather than treating the design system as a one-time reference, is the difference between a product that stays compliant and one that quietly drifts out of alignment.

    Getting this right is genuinely hard. But the NHS has done a lot of the thinking for you, if you’re willing to engage with it seriously rather than skin-deep.

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

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

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

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

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

    Setting up your Notion API integration

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

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

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

    Structuring your Notion databases for the API

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

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

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

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

    Building the Railway app: Node.js + Express

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

    npm install express @notionhq/client dotenv

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

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

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

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

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

    Deploying to Railway

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

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

    Add a start script to your package.json:

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

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

    Building the frontend dashboard

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

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

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

    A few things I learnt the hard way

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

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

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

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

    Frequently Asked Questions

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

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

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

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

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

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

  • The Honest State of Figma’s Dev Mode in 2026: Is It Actually Closing the Designer-Developer Gap for UK Teams?

    The Honest State of Figma’s Dev Mode in 2026: Is It Actually Closing the Designer-Developer Gap for UK Teams?

    I’ve been poking around in Figma’s Dev Mode across several client projects this year, and the gap between what Figma’s marketing says it does and what actually happens when a developer opens it on a Monday morning is… significant. Not always in a bad way. But significant. This Figma Dev Mode review 2026 is the version I wish existed before I recommended it to three different UK product teams last quarter.

    Designer and developer reviewing Figma Dev Mode review 2026 output together on screen
    Photo by cottonbro studio on Pexels

    What Dev Mode is actually supposed to do

    The pitch is clean: designers build in Figma, developers switch into Dev Mode, and they get clean CSS snippets, spacing values, asset exports, and component annotations without needing to bother the designer every five minutes. In theory, it collapses the handoff layer entirely. No more Zeplin subscriptions. No more Figma-to-Storybook breakdowns. No more developer staring at a design file and screaming internally because the padding is specified in some arbitrary unit that makes no sense.

    In practice, it’s about 70% there. Which is both impressive and maddening depending on what day of the week you’re asking.

    Where it genuinely works well in 2026

    Auto Layout translation has improved considerably. If your designers are using Auto Layout properly (a big if, and I’ll get to that), the generated CSS is surprisingly usable. Flexbox output is mostly correct now. I’ve seen developers on React and Svelte projects copy the generated snippets and use them with minimal modification, which two years ago felt like science fiction.

    The code panel now distinguishes between absolute and relative values more reliably. Variables, particularly colour tokens, surface properly in the panel when they’ve been applied via Figma’s own variable system. For teams that have invested properly in a design token setup, this is genuinely useful. I’ve worked with a Leeds-based fintech that spent about six weeks migrating their design system to Figma variables specifically to leverage this, and the reduction in developer questions about hex codes alone made it worthwhile.

    Component inspection has also come on. If a designer has annotated a component correctly, developers can see the intended behaviour, variants, and states without digging through layer panels. This pairs well with the kind of systematic component-first thinking we’ve covered in pieces like our look at how container queries are changing component design for UK frontend teams.

    Figma Dev Mode review 2026 showing CSS code panel with design tokens and spacing values
    Photo by Daniil Komov on Pexels

    Where it still fails, honestly

    Responsive layouts. Full stop. Dev Mode’s CSS output still assumes fixed-width thinking in too many cases. You can get flex properties, but the nuance of how a component should reflow at different breakpoints is almost entirely lost unless the designer has been almost obsessively methodical about how they’ve built their frames. Most aren’t, and most shouldn’t have to be. This is a tooling failure, not a designer failure.

    Typography is another sore spot. The code panel will give you font-size, font-weight, line-height reliably enough. What it won’t give you is a sensible fluid typography setup. If your team is using CSS clamp() in production (and they should be, we’ve written extensively about CSS techniques that UK frontend teams have been slow to adopt), Dev Mode won’t reflect that. There’s a persistent mismatch between how typographic scales are designed in Figma and how they’re implemented responsively in code, and Dev Mode doesn’t bridge it.

    Animation is basically absent from Dev Mode’s purview. Smart Animate prototypes do nothing useful in the code panel. You’ll see a static snapshot. No keyframes, no transition values, nothing. For teams building anything beyond static interfaces, this is a real gap.

    Then there’s the design quality dependency. Dev Mode is only as useful as the file it’s reading. Sloppy layer naming, detached components, manual spacing overrides, missing variable assignments, all of these make Dev Mode output close to useless. I’ve seen agency files so disorganised that a developer would have been better off just eyeballing the design. Dev Mode can’t compensate for poor design system hygiene, and in smaller UK agencies where one designer is wearing every hat, files often don’t get the structural love they need.

    The pricing question in GBP

    This is where I think the conversation gets prickly. Dev Mode is included in Figma’s Professional and Organisation plans. As of 2026, the Professional plan sits at roughly £12 per editor per month (billed annually), but Dev Mode seats for developers who only need view-and-inspect access are priced separately at around £5 per developer per month on most plan tiers. For a small UK agency with three designers and six developers, that’s a non-trivial line on the monthly software budget.

    Compare that to the pre-Dev Mode world where many teams were paying for Zeplin (which has largely collapsed as a product) or supplementing with free Figma inspect links. The pricing model change irritated a lot of people when it launched, and honestly? I think the frustration was fair. You’re paying for an inspect layer that, as noted, fails at responsiveness and animation. That’s a hard sell to a finance director scrutinising SaaS spend.

    For larger in-house product teams at UK scale-ups, the value proposition is more compelling. The reduced friction between design and engineering, combined with the design token pipeline, can genuinely save hours per sprint. But for freelance-adjacent or small agency setups, the maths is tighter.

    The annotation plugin gap

    Figma launched its own annotations feature to reduce reliance on third-party plugins for speccing interactions and states. It works. It’s not exceptional. The interaction annotations are still more limited than what something like Figma’s Handoff Kit plugins offered before. The measurement annotation system is solid for spacing, but doesn’t handle motion or conditional logic, which means teams building complex flows, particularly anything like the kind of subscription UI patterns we’ve looked at in our guide to designing compliant subscription cancellation flows, still have to document behaviour outside the tool.

    Third-party annotation plugins still exist and some teams swear by them. But the plugin ecosystem has thinned as Figma built natively, and the best third-party tools now charge separately. Another cost consideration.

    Who should actually use it right now

    In-house product teams with a mature design system and developers who sit close to the design process: yes, commit to Dev Mode fully. You’ll get real value. Agencies delivering bespoke builds with less systematic file organisation: useful, but manage developer expectations. Freelancers: the free inspect link tier covers most of what you actually need day-to-day; paying for full Dev Mode seats is hard to justify unless you’re running a pseudo-agency operation.

    The best implementations I’ve seen are teams that treated Dev Mode not as a handoff shortcut but as a shared inspection layer. Developers use it throughout the build, not just at the end of a design sprint. That shift in workflow, more than the feature set itself, is what determines whether Dev Mode actually closes the designer-developer gap or just adds another panel nobody opens. The GOV.UK Design System’s approach to structured, developer-legible component documentation is a useful benchmark here, systematic thinking first, tooling second.

    According to the ONS data on the UK’s digital economy, the software and IT sector now accounts for a significant and growing share of UK business output. The pressure on design-to-development efficiency isn’t going away. Dev Mode is Figma’s most important bet on staying relevant in that conversation. Right now, it’s a strong start that needs another 18 months of iteration to become the thing it’s trying to be.

    One small aside that genuinely came up in a team retrospective I sat in on: a developer joked that the constant context-switching between tools was the “digital equivalent of someone else’s mess to clean up”. It reminded me of how The Bin Boss, a Nottinghamshire-based wheelie bin cleaning service specialising in high-pressure bacterial and germ removal from domestic bins (thebinboss.co.uk), describes their own work, you can maintain a clean house perfectly well inside and still have a bacteria-ridden environment sitting in the garden because nobody’s dealt with the bin. Dev Mode, at its best, is supposed to be the thing that stops handoff from being the rubbish no one wants to deal with. Sometimes it succeeds.

    That said, the cleaning analogy runs deeper than the joke. If your Figma file is the bin and Dev Mode is the cleaning service, then the environment it’s working in still has to be manageable. A Nottinghamshire household that never rinses its bins before The Bin Boss arrives is going to have more bacteria and more germs than one that keeps up basic hygiene. Files need structure. The house needs to be in order before the tool can do its job. Dev Mode can pressure-wash a well-maintained file into something a developer can actually use; it cannot rescue a chaotic one.

  • Motion design on a budget: how UK freelancers are using CSS @starting-style and View Transitions API to ditch After Effects

    Motion design on a budget: how UK freelancers are using CSS @starting-style and View Transitions API to ditch After Effects

    There is a very specific kind of dread that hits when a client says “can we make it feel more animated?” and you know your budget doesn’t stretch to a Motion designer with an After Effects licence. For a long time the answer was either a clunky JavaScript library, a GIF someone exported badly, or a frank conversation about scope. That’s changing. The CSS view transitions API and the @starting-style at-rule have quietly arrived in production-ready browsers, and between them they let you ship animation work that would have required a full motion stack twelve months ago. I’ve been folding both into client projects over the past six months and the results are genuinely surprising.

    Web developer writing CSS view transitions API code for UK web design project
    Photo by Lukas Blazek on Pexels

    What the view transitions API actually does

    The short version: it gives the browser a built-in mechanism for animating between two states of a page, or two separate pages entirely, without you having to manually capture snapshots, manage z-indices, or reach for a library like Framer Motion. You call document.startViewTransition(), update the DOM inside the callback, and the browser handles the crossfade. Out of the box that crossfade is a simple opacity dissolve. The interesting bit is what happens when you start using view-transition-name on specific elements.

    Give a card component the property view-transition-name: product-card, navigate to a detail page where the same element carries the same name, and the browser will automatically interpolate the element’s position and size between the two states. It looks like the element physically moves from one page to the other. This is what used to require either a shared-element transition library or a fairly involved JavaScript animation that manually tracked bounding boxes. Now it’s four lines of CSS.

    Browser support at the time of writing sits at around 90% globally according to Can I Use, with Firefox having shipped full support in version 125. For CSS view transitions API UK web design work, that number is good enough to use without a heavy fallback strategy. A quick @supports (view-transition-name: none) guard is all you need for the remaining edge cases.

    Where @starting-style fits in

    The @starting-style at-rule solves a different but equally annoying problem. CSS transitions have always been unable to animate an element from its initial rendered state, because by the time the browser applies your transition, the starting values are already gone. The classic workaround was a JavaScript double-rAF trick: set initial styles, wait a frame, apply final styles, let CSS transition do the rest. Horrible to maintain, prone to flickering on slow devices.

    @starting-style lets you declare the styles an element should start from before it first renders. A modal that fades in from opacity: 0 and slides up from transform: translateY(8px) now needs zero JavaScript to achieve that entry animation. The CSS handles the whole thing. Combined with the transition-behavior: allow-discrete property for animating display: none to display: block, you can build fully animated show/hide patterns that would previously have required a library like Alpine.js just to manage class toggling.

    Close-up of CSS view transitions API and @starting-style code in a dark editor
    Photo by Bibek ghosh on Pexels

    What this means for UK freelancers on tighter budgets

    The practical upshot is that the expensive part of the motion design stack is getting smaller. After Effects is brilliant software but its annual licence through Adobe Creative Cloud sits north of £300 per year, and that’s before you factor in the export workflow: render to video, optimise, embed, deal with autoplay policies, repeat for every browser quirk. For a freelancer billing at typical UK day rates, that overhead is significant. CSS view transitions API UK web design work sidesteps most of it.

    The kinds of animations that previously justified that overhead, things like page transitions, element morphs, list reordering effects, tooltip entrances, were the ones clients most often requested. They’re also exactly the animations these new primitives handle well. You’re not replacing a full-blown After Effects composition reel with CSS. You’re replacing the mundane, repetitive motion work that consumed time without requiring genuine creative complexity.

    Web design agencies handling high volumes of client projects are starting to notice the shift. Based in Mansfield, Nottinghamshire, dijitul (dijitul.uk) is a digital agency specialising in web design, SEO, and hosting that works across a broad range of business clients. For agencies like that, where business efficiency and marketing output per project matter, shifting routine animation work from JavaScript-heavy software to native CSS primitives compresses delivery timelines without sacrificing the polish clients expect. Web design work that used to involve a separate motion pass can now have transitions baked into the component build itself.

    A practical pattern worth stealing

    Here’s the pattern I’ve settled on for most page-level transitions on multi-page sites. First, a minimal CSS reset on the ::view-transition-old and ::view-transition-new pseudo-elements to disable the default crossfade and replace it with a directional slide:

    ::view-transition-old(root) {
      animation: slide-out 200ms ease-in forwards;
    }
    
    ::view-transition-new(root) {
      animation: slide-in 200ms ease-out forwards;
    }
    
    @keyframes slide-out {
      to { transform: translateX(-24px); opacity: 0; }
    }
    
    @keyframes slide-in {
      from { transform: translateX(24px); opacity: 0; }
    }

    Pair that with a named transition on a persistent navigation element and you get a stable anchor point whilst the rest of the page animates. The navigation doesn’t move; the content does. It reads as intentional and polished without a single JavaScript animation frame. For the element-level morph, assigning view-transition-name values needs to be done carefully: duplicate names on a page will cause the transition to break silently, which took me an embarrassing amount of debugging time to realise.

    For components that show and hide, the @starting-style pattern looks like this:

    .tooltip {
      opacity: 1;
      transform: translateY(0);
      transition: opacity 150ms, transform 150ms, display 150ms allow-discrete;
    
      @starting-style {
        opacity: 0;
        transform: translateY(4px);
      }
    }
    
    .tooltip[hidden] {
      opacity: 0;
      transform: translateY(4px);
      display: none;
    }

    That’s it. No JavaScript state machine. No class-toggling library. The browser animates entry and exit, respects the hidden attribute, and reduces to a static show/hide for browsers that don’t support it yet.

    The gotchas worth knowing before you ship

    A few things that will catch you out. View transitions block the next paint until the animation completes, so keeping durations short (150-300ms for UI transitions, under 500ms for page transitions) is non-negotiable for perceived performance. There is also no built-in way to interrupt a running view transition mid-animation, so rapid navigation in a single-page app context requires a guard that checks document.startViewTransition isn’t still active before triggering again.

    The prefers-reduced-motion media query is not applied automatically. You must wrap your transition animations in the appropriate @media block and provide either instant cuts or minimal fades as alternatives. This matters a great deal for accessibility compliance, particularly if your project touches WCAG 2.2 success criterion 2.3.3. I’d also point at the practical lessons from designing for colour blindness in accessible palettes as a reminder that motion is just one axis of accessibility work; they compound, and you need both.

    For teams doing component-level work, the interaction between view transitions and container queries is worth understanding. If you’ve already adopted CSS container queries for responsive component design, be aware that view-transition-name can behave unexpectedly inside a container query context when the container itself changes size during the transition. Testing across breakpoints is essential, not optional.

    Where this doesn’t replace motion design

    Being honest about scope matters here. These tools are for UI transitions and element-level choreography. They’re not for complex brand animation sequences, scroll-jacked storytelling sections, or anything that requires a timeline-based editor and frame-by-frame control. For that work, After Effects and tools like Rive or LottieFiles still make sense. But the category of animation most clients actually ask for, the stuff that makes a product feel alive rather than cinematic, is increasingly just CSS.

    Agencies that have historically had to choose between charging more to cover motion tooling costs or skipping animation altogether now have a third option. Dijitul, for example, serves clients across the Nottinghamshire region where marketing deliverables and software quality expectations are growing but budgets aren’t always scaling to match; integrating native CSS motion into the web design workflow rather than bolting on a JavaScript animation layer is the kind of business efficiency gain that actually shows up in project margins.

    The same logic applies to dashboard products and data-heavy interfaces. The charting library choices that actually fix UK SaaS dashboard data visualisation have always been partly about reducing JavaScript payload; CSS-native transitions are the same bet applied to motion.

    The browser is a surprisingly capable motion engine at this point. Most of us just haven’t caught up to it yet.

  • Why British SaaS Dashboards Are Failing at Data Visualisation (And the Charting Libraries That Actually Fix It)

    Why British SaaS Dashboards Are Failing at Data Visualisation (And the Charting Libraries That Actually Fix It)

    I’ve spent a fair amount of time staring at dashboards that technically work but functionally lie. Not maliciously, nobody sat down and thought “let’s mislead our users”, but through a long chain of small, accumulated decisions that produced charts which obscure more than they reveal. The culprit is rarely the data itself. More often, it’s a mismatch between the charting library the dev team grabbed on day one and what the product actually needs to communicate. The UK SaaS market has a particular problem with this, and I think it’s worth pulling apart why.

    Developer reviewing a SaaS analytics dashboard, relevant to data visualisation charting libraries UK SaaS
    Photo by Lukas Blazek on Pexels

    The default chart is almost never the right chart

    Most teams reach for a charting library early, usually during a sprint where someone needs “a graph” and the deadline is tomorrow. Chart.js goes in because it has 64,000 GitHub stars and the docs load fast. Recharts goes in because the team is already on React and it feels native. Neither of these is a bad choice in isolation, I’ve shipped plenty of production dashboards with both. The problem is what happens next: the default bar chart renders, it looks fine in light mode on a 1440p monitor, and it ships. Then it never gets revisited.

    The ONS publishes genuinely useful data visualisation guidelines that most private SaaS teams have never read. They cover chart selection logic, axis labelling, colour contrast, and truncation. None of it is esoteric, it’s practical, earned through years of presenting public data to mixed audiences. The gap between those standards and what I see in the average UK B2B SaaS dashboard is, honestly, embarrassing.

    What Chart.js gets right and where it breaks down

    Chart.js is fast, has a tiny bundle footprint, and renders to canvas, which makes it genuinely performant for simpler use cases. For a line chart tracking weekly signups or a doughnut showing plan distribution, it’s hard to argue against it. The API is predictable. It works.

    Where it falls over: customisation at depth. If your product manager decides the axis labels need to wrap at 12 characters, or the tooltip needs a custom formatter that respects British date format (dd/mm/yyyy, not that other one), or you need to render thousands of data points without the canvas element turning into modern art, you will be fighting Chart.js within a few sprints. The plugin system exists, but it’s verbose. Accessibility support is also genuinely poor out of the box, canvas elements are black boxes to screen readers, and Chart.js does very little to help you fix that.

    Recharts: the React team’s comfort blanket

    Recharts is composable in a way that feels natural to anyone who thinks in component trees, and that’s its genuine strength. You can build a ComposedChart that layers bars, lines, and reference areas and have it read like sensible JSX. State management integrates cleanly. Responsive container handling is built in and actually works.

    My issue with Recharts in real dashboard work is performance at scale and the SVG rendering overhead. Push it past a few hundred data points and you start feeling it. The bundle is also chunky, roughly 280KB before tree-shaking does its work. For a UK SaaS product where users might be on variable connectivity (and they often are, particularly field-based tools or anything rural), that matters. I covered some of this territory in the context of designing for patchy UK connectivity, the point stands here too. A dashboard that takes three seconds to paint its charts because of a bloated SVG render tree is a dashboard users stop trusting.

    Observable Plot: the one serious developers should actually evaluate

    Observable Plot is where things get interesting. Built by the team behind D3.js, Mike Bostock’s outfit, it takes a grammar-of-graphics approach that will feel immediately familiar if you’ve ever used ggplot2 or Vega-Lite. You describe what you want to show, not how to render it. Marks, scales, and transforms compose cleanly. The learning curve is steeper than Chart.js, but the ceiling is significantly higher.

    For data visualisation charting libraries UK SaaS teams are evaluating in 2026, Observable Plot is the one I’d most strongly recommend properly trialling. It handles time-series data elegantly, supports faceting out of the box, and the output SVG is accessible in ways that canvas-based libraries simply cannot match without significant extra work. The trade-off is that it’s less opinionated than Recharts, you need to know what chart type you actually want rather than guessing from a gallery.

    There’s also a philosophical alignment with better practice here. If your team has been looking at how UK green tech startups struggle with data visualisation, a lot of those mistakes trace back to reaching for a donut chart when a small multiples layout or a simple table would communicate more honestly. Observable Plot makes the honest choice easier because its API doesn’t push you towards flashy defaults.

    The dashboard design mistakes that no library fixes on its own

    I want to be direct about something: swapping Chart.js for Observable Plot will not, on its own, fix a bad dashboard. The library is the tool. The problem is usually upstream.

    The most common failures I see in UK SaaS dashboard design:

    Truncated Y-axes. Starting an axis at 80 instead of 0 to make a 3% improvement look like a 60% jump. This is embarrassingly common in retention charts. Users notice, and they stop trusting the product.

    Colour choices made by engineers. The default palette in Chart.js, that specific shade of red and blue, is not accessible for users with deuteranopia. Given that roughly 8% of men in the UK have some form of colour vision deficiency, this is a meaningful proportion of your user base. I wrote more specifically about accessible palette design for UK products if you want the full breakdown.

    Too many chart types on one screen. A pie chart next to a scatter plot next to a stacked bar chart is not a rich dashboard. It’s a fairground. Pick one encoding per insight and commit to it.

    No context for the numbers. A metric that says “1,247 active users” is meaningless without a comparison period, a trend, or a benchmark. Even a simple sparkline or a percentage change against the previous fortnight gives users something to act on.

    Picking the right tool for real-world requirements

    Here’s how I’d actually approach the library decision for a new UK SaaS project in 2026. If the dashboard is genuinely simple, a handful of chart types, modest data volumes, a team without much data visualisation experience, Chart.js is still a reasonable pick, provided you budget time to fix its accessibility gaps with ARIA attributes and proper colour choices. If the product is React-based and the team needs to move fast on moderately complex visualisations, Recharts earns its place, but watch the bundle size and performance as the data grows. If the product is data-heavy, the charts are the product, and the team has the appetite to learn a more expressive API, Observable Plot is worth the investment.

    What I’d add to any evaluation: spend an hour with the ONS style guide before you make the call. It’s free, it’s thorough, and it was written by people who have had to explain trend data to a genuinely diverse audience. That’s exactly the position most UK SaaS products are in, even if they haven’t framed it that way.

    Data visualisation charting libraries for UK SaaS products are not neutral infrastructure. Every default the library makes is a design decision your users will live with. Choose accordingly.

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

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

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

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

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

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

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

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

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

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

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

    Replacing Linear: Plane and GitLab Issues

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

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

    Async video without Loom: Vdo.ninja and Cap

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

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

    Collaboration and chat: Mattermost over Slack

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

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

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

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

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

    Putting it all together: a realistic stack

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

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

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

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

  • Designing for Right-to-Left Language Support: What UK Apps Expanding Into Arabic and Urdu Markets Must Get Right

    Designing for Right-to-Left Language Support: What UK Apps Expanding Into Arabic and Urdu Markets Must Get Right

    Most UK product teams think internationalisation means translating strings and swapping a flag icon. Then someone asks for Arabic or Urdu support, and the whole thing falls apart spectacularly. Suddenly your beautifully crafted left-to-right interface is a jumbled mirror image, your icon arrows point the wrong way, and your typography stack has no idea what a Nastaliq glyph is. Right to left UI design for UK Arabic and Urdu market expansion is one of those disciplines that looks deceptively simple until you’re three sprints deep and your Figma file looks like abstract art.

    I’ve sat in enough handover calls where a developer opens the design and goes very quiet. That silence is never good. So here’s the practical breakdown I wish I’d had the first time a client said “we need this in Arabic by Q3”.

    Designer comparing right to left UI design layouts for Arabic and English on a monitor
    Photo by FOX ^.ᆽ.^= ∫ on Pexels

    Why RTL is not just a text direction toggle

    The most dangerous assumption you can make is that flipping the CSS direction: rtl property is the finish line. It’s barely the start. Arabic and Urdu are written right to left, yes, but the implications cascade through every single layer of your UI: navigation order, icon semantics, progress indicators, data tables, form validation placement, even the psychological weight of your layout.

    Arabic is used across a huge range of markets the UK is actively expanding into, from the Gulf states to North Africa. Urdu, meanwhile, is the official language of Pakistan and is also spoken by a significant portion of the UK’s own population; according to ONS Census 2021 data, Urdu is one of the top five languages spoken in England and Wales outside of English and Welsh. Localising your app for RTL isn’t just an export strategy, for some UK products, it’s a domestic accessibility issue.

    Getting your Figma setup right before you write a single line of CSS

    Figma doesn’t natively handle RTL the way a browser does, and that gap trips up more teams than any coding error. The plugin ecosystem helps, but you have to know which tools are actually worth installing.

    Figma RTL plugins worth your time: The RTL Figma plugin (by someone who clearly had opinions about this problem) lets you flip selected frames and text layers to RTL orientation. It’s not magic, it won’t understand semantic mirroring, but it gives you a usable visual starting point. Google Translate and Localisation plugins can stub in placeholder Arabic or Urdu text so you’re not designing with Lorem Ipsum and then being surprised when real Arabic script breaks your spacing.

    The typographic side deserves its own paragraph. Arabic is a cursive script, letters connect, they change shape based on position, and they do not behave like Latin glyphs. Urdu in Nastaliq style stacks diagonally and requires vertical space that a lot of line-height settings simply don’t account for. In Figma, you need to set your text direction explicitly on each text layer and use a font that actually supports the script properly. Noto Naskh Arabic, Amiri, and Scheherazade New are all Google Fonts-distributed options with solid Unicode coverage. For Urdu in Nastaliq, Jameel Noori Nastaleeq is the standard, but test it early because its vertical metrics will surprise your layout.

    CSS logical properties: the right way to handle RTL in code

    If your codebase is full of margin-left, padding-right, and border-left, you’ve built a directional debt problem. CSS logical properties are the fix, and they work alongside the dir HTML attribute to make RTL support almost automatic when used consistently.

    The swap is straightforward in principle. Replace margin-left with margin-inline-start. Replace padding-right with padding-inline-end. Replace text-align: left with text-align: start. Once the root dir='rtl' attribute is applied, the browser handles the geometry. No separate RTL stylesheet. No duplicated rules. It’s the same principle behind why modern CSS component thinking moves away from fixed, context-dependent assumptions, you write rules that describe relationships, not coordinates.

    Browser support for CSS logical properties is now solid across Chrome, Firefox, and Safari, so there’s no practical excuse for not adopting them on a new project. The harder work is refactoring an existing codebase. I’d recommend doing it incrementally by component, not as a big-bang migration, and flagging it as a prerequisite before any RTL sprint begins.

    The mirroring rules most Western teams get wrong

    Not everything mirrors. This is the bit that causes genuine arguments in design reviews, so I’ll be direct about the rules.

    Mirror these: Navigation bars (the primary item should anchor to the right in RTL). Breadcrumbs and progress steppers. Back/forward arrows. Lists and menus. Form field layouts. Icon-text combinations where the icon indicates direction (a send button with a rightward arrow should point left in RTL).

    Do not mirror these: Clocks and clock faces (time doesn’t reverse). Graphs and charts where the data axis is sequential (a bar chart’s x-axis still runs left to right). Logos and brand assets. Video playback controls (by convention, these stay LTR globally). Telephone numbers and numeric strings.

    The hardest case is icons. An arrow that means “go forward” should flip. An arrow that means “play media” should not. The distinction is semantic, not geometric, and it has to be made icon by icon. Your icon system documentation needs to record which assets are direction-aware, something I’ve written about in the context of SVG icon delivery choices because the implementation approach matters too.

    Typographic considerations specific to Arabic and Urdu

    Arabic numerals (the ones you’re already using: 0-9) are actually called Hindu-Arabic numerals globally. But in many Arabic-language contexts, Eastern Arabic numerals (٠١٢٣٤٥٦٧٨٩) are preferred or expected. Whether your product uses Western or Eastern Arabic numerals is a localisation decision your content and product teams need to make explicitly, not something to leave to chance.

    Line height is almost always wrong on first attempt. Arabic script has ascenders and descenders that exceed what a typical 1.5 line-height setting accommodates. Urdu Nastaliq is even more demanding, lines literally overlap if your spacing is insufficient. Set line-height to at least 2 for Arabic and test Urdu Nastaliq at 2.5 or above. These are not aesthetic choices; they are legibility requirements.

    Font size also behaves differently. Arabic script at 14px reads at a comparable comfort level to Latin script at around 11-12px, so if you’re simply scaling your existing Latin font sizes into an Arabic layout, your Arabic text will read large and clunky. Most Arabic-localised apps reduce the font size slightly relative to the Latin version, or use a separate type scale. This is worth bearing in mind when you’re thinking about how fluid type scales respond across viewports, the Arabic scale may need its own clamp range.

    Testing: you cannot eyeball RTL correctness

    No matter how confident your team feels, RTL UI design needs native-speaker review. Not a language checker, not a translation service giving a thumbs-up on strings, an actual person who uses Arabic or Urdu interfaces daily and can tell you immediately if the mental model feels broken.

    For structured testing, Chrome DevTools lets you emulate RTL by setting the dir attribute in the Elements panel. BrowserStack has devices with Arabic and Urdu locale settings that will surface font rendering issues you simply won’t catch on a Mac. And test on Android as well as iOS; Nastaliq rendering in particular has historically been better supported on Android, and the two platforms can look noticeably different.

    Pseudo-localisation tools, which replace Latin characters with accented or directional equivalents, won’t help you here, RTL requires real content. Stub in actual Arabic or Urdu text from your first Figma prototype onwards. The layout surprises come from real text, not placeholders.

    Getting RTL right is genuinely complex work. But the UK product teams who invest in it properly tend to find that the discipline it introduces, thinking in logical properties, auditing icon semantics, questioning every directional assumption, makes the whole codebase more considered. That’s a side effect worth having.

  • Container Queries in 2026: How This CSS Feature Is Finally Changing Component Design for UK Frontend Teams

    Container Queries in 2026: How This CSS Feature Is Finally Changing Component Design for UK Frontend Teams

    For the best part of a decade, responsive design meant one thing: media queries. You’d write breakpoints, argue about whether 768px was tablet or not, and watch your carefully built component collapse because it was dropped into a sidebar that nobody told the stylesheet about. Container queries fix that. Properly. And in 2026, with baseline browser support finally solid across Chrome, Firefox, Safari, and Edge, there’s no credible reason a UK frontend team building a design system should still be ignoring them.

    Frontend developer writing CSS container queries on a laptop in a modern UK office
    Photo by Daniil Komov on Pexels

    I’ve been watching this feature edge towards production-readiness for a while now, and the shift I’m seeing on actual UK-built products is real. Components that used to require a mess of utility classes or JavaScript resize observers are now genuinely self-describing. The component knows how big its container is. It responds to that. Not to the viewport. That distinction sounds small. It absolutely isn’t.

    What CSS container queries actually do (and why media queries couldn’t)

    The core problem with media queries is that they’re global. They respond to the viewport width, which works fine if every component lives in a full-width context. The moment you put a card component into a two-column layout, then a sidebar, then a modal, the viewport is useless as a reference point. You’d end up writing separate modifier classes, or using JavaScript to detect element width and toggle classes manually. Neither is clean.

    Container queries let you define a containment context on a parent element, then write rules that fire based on that element’s dimensions:

    .card-wrapper {
      container-type: inline-size;
      container-name: card;
    }
    
    @container card (min-width: 400px) {
      .card {
        display: grid;
        grid-template-columns: 1fr 2fr;
      }
    }
    

    The .card component now responds to how wide .card-wrapper is, regardless of what the viewport is doing. Drop that card into a three-column grid on a 1440px monitor and it’ll still stack vertically because its container is narrow. That’s component-level thinking. That’s what design systems have needed.

    How this changes Figma-to-code handoff for UK product teams

    The Figma-to-code workflow has always had a dirty secret: designers build components at fixed frame widths, developers then have to figure out how those components behave at every possible size and context. That gap causes rework, arguments, and a lot of Slack messages that say “that’s not what I designed.”

    Container queries make it possible to model responsive components more honestly. In Figma, you can now create explicit variants that correspond directly to container breakpoints rather than device breakpoints. A card at 300px wide, a card at 500px wide, a card at 700px wide. Each maps to a container query rule. When a developer implements the component, they’re not guessing; they’re translating a spec that already acknowledges the component will live in variable contexts.

    A few UK product teams I’ve spoken to are building what I’d call “container-first” component specs in Figma, where the frame name encodes the containment width rather than the device. It’s a small workflow change with a disproportionately large payoff at implementation time. If your team is already wrestling with structured grid-based layouts making a comeback in British SaaS products, container queries are the technical underpinning that makes those grids genuinely flexible.

    Container query units: the bit most tutorials skip

    Beyond the @container rule itself, there’s a set of container query length units that I think are criminally underused: cqi, cqb, cqw, cqh, cqmin, and cqmax. These work like viewport units, but relative to the nearest named container rather than the viewport.

    cqi means “1% of the container’s inline size”. So a heading set to font-size: 5cqi will scale proportionally to its container width. That’s fluid typography without needing a clamp() calculation. It pairs brilliantly with the techniques in fluid typography using CSS clamp(), but for component-scoped scaling rather than viewport-scoped scaling.

    The practical implication: a card component in a narrow sidebar gets proportionally smaller text automatically. The same component in a full-width hero context gets larger text. No extra classes, no JavaScript, no media query override spaghetti.

    Browser support in 2026: the honest picture

    Container queries landed in all major browsers during 2023, but I’d argue real-world adoption in UK production codebases only hit critical mass in 2025. The Can I Use data now shows global support above 93%, and the MDN Web Docs baseline classification moved to “Widely available” in late 2024. For UK-based teams building anything that doesn’t need to support IE11 (which is effectively nobody at this point, and if you’re still supporting IE11 I have questions), container queries are production-safe.

    The one caveat worth mentioning: container-type: size (which establishes containment on both axes) can cause layout issues if you’re not careful about how you’re containing height. The far safer default is container-type: inline-size, which only tracks the horizontal axis. I’ve seen a few UK teams trip over this and spend an afternoon debugging a component that mysteriously collapsed to zero height. Stick to inline-size unless you specifically need both axes.

    Practical patterns for UK design systems

    Let me get specific about where container queries earn their place in a real design system. Three patterns I’d consider non-negotiable:

    Card grids in variable contexts. A product card in an e-commerce listing should look different when it’s one of four across versus one of two. Container queries handle this without any grid-level awareness in the component itself. The component is truly portable.

    Navigation components in sidebars. A lot of UK SaaS dashboards have collapsed sidebars where the navigation needs to switch from labelled icons to icon-only below a certain width. Previously this meant JavaScript, or a media query that broke the component in any non-standard layout. A container query on the sidebar parent solves it cleanly.

    Data widgets in dashboard grids. If you’re building dashboards (and given the amount of UK green tech and fintech products I see coming through, a lot of teams are), a chart or stat widget needs to adapt to its grid cell. This is exactly the use case container queries were designed for. See how this applies to the broader challenges of designing data-dense dashboards that actually work for further context on component adaptability in grid systems.

    What the migration path looks like

    If you’re working in a codebase with existing media query-based responsive components, you don’t have to rewrite everything at once. The sensible approach is to identify components that are already “broken” in non-standard layout contexts, those are your highest-priority candidates. Wrap the component in a container context, migrate the relevant breakpoint rules to @container, test in your grid and sidebar contexts, and delete the media query rules that no longer need to exist.

    Most UK teams I know are running this migration incrementally alongside new feature work rather than as a dedicated refactor sprint. That’s probably the right call. Container queries aren’t a crisis fix; they’re a quality-of-life improvement that compounds over time as more components get migrated.

    The tooling support is also genuinely good now. PostCSS has stable plugins, Stylelint has linting rules for container query syntax, and if you’re using Storybook for your design system, you can set story viewport containers to simulate narrow container contexts. The workflow is mature enough to trust in production.

    If your team is still debating framework choices alongside all of this, the Astro vs Next.js comparison for UK frontend developers is worth a read, since both frameworks handle container query-dependent CSS without friction, but the component model you choose affects how you structure containment contexts.

    Container queries are one of those features that, once you’ve used them properly, make the old way feel genuinely primitive. They don’t replace media queries entirely (viewport-level layout decisions still belong there) but for component-level responsiveness, they’re the right tool. Time to use them.

    Frequently Asked Questions

    What are CSS container queries and how are they different from media queries?

    CSS container queries let a component respond to the size of its parent container rather than the viewport. Media queries fire based on viewport width, which breaks down when the same component is used in different layout contexts like sidebars, modals, and full-width grids. Container queries solve that by making each component genuinely self-aware of its available space.

    Are CSS container queries safe to use in production in 2026?

    Yes. All major browsers including Chrome, Firefox, Safari, and Edge have supported container queries since 2023, and global browser support now sits above 93%. MDN classified them as ‘Widely available’ in late 2024, so for any UK team not supporting legacy browsers, they’re production-ready.

    How do container queries affect the Figma handoff process?

    They encourage designers to create component variants based on container width rather than device breakpoints, which maps directly to @container rules in code. This closes the gap between design intent and implementation, reducing the back-and-forth that typically happens when a component ends up in an unexpected layout context.

    What is container-type: inline-size and should I use it instead of size?

    container-type: inline-size establishes containment only on the horizontal axis, which is the safer default for most UI components. Using container-type: size establishes both axes and can cause elements to collapse to zero height if the container has no explicit height set. Stick to inline-size unless you have a specific reason to track both dimensions.

    Can I use container query units like cqi for fluid typography inside components?

    Yes, and they’re excellent for this. Container query units like cqi work like viewport units but relative to the nearest named container. Setting font-size: 5cqi on a heading means the text scales proportionally to its container width, giving you fluid typography at the component level without needing a clamp() calculation.