What you actually want from a modern frontend
Speed on real networks, not localhost. Accessibility without an accessibility consultant. Design tokens that survive a rebrand. A component system that lives in the repo, not in a library you cannot change. Tests that run in CI and block merges. Deploys that roll back in one click.
Every one of those is boring. All of them matter more than the framework brand.
Legacy frontend modernisation
Most jQuery and AngularJS codebases are not broken - they are just expensive to change. New features take three times as long. Hiring is harder. Every PR risks something unrelated.
The fix is not a rewrite. A full rewrite means six months of no new features while you rebuild what you already have. We use the Strangler Fig pattern instead.
How it works:
- New React or Next.js modules run alongside the legacy codebase from day one.
- Routing intercepts redirect specific paths to the new stack.
- Feature flags control which users see which version.
- Legacy modules are retired one at a time as the new equivalents ship.
- Production stays live. Your roadmap keeps moving.
We have migrated jQuery SPAs, AngularJS dashboards, and server-rendered PHP frontends using this approach. The typical timeline for a mid-size product is 4 to 9 months - faster if the legacy codebase has clear module boundaries, slower if the data model is entangled.
Tell us what you are running and we will tell you the migration order that causes the least pain.
Micro-frontend architectures
When does a monolithic frontend stop working?
When five teams are merging into the same repo and someone’s checkout flow breaks the marketing team’s homepage. When a dependency upgrade needs sign-off from three product areas. When a deploy affects ten teams but only one requested it.
Micro-frontends split the frontend the same way microservices split the backend. Each domain - payments, catalogue, account, admin - owns its own codebase, its own CI pipeline, and its own release cycle.
The mechanism: Webpack Module Federation or a Vite-based federated runtime. Child modules load dynamically at runtime. The host shell assembles them. Each team ships independently without touching anyone else’s code.
When you do not need this:
- Single product team, under 20 frontend engineers, clear module boundaries you can enforce with a monorepo.
When you do:
- Multiple squads blocked by a shared deploy, business units that need independent release cycles, acquisitions being integrated into an existing product.
We will tell you which applies before you spend three months wiring it up.
State management - picking the right tool
Client state in a React app has two distinct problems. Mixing them causes most of the bugs.
Server state - data that lives on the server and needs to stay in sync with the UI. TanStack Query (React Query) for REST. SWR when the pattern is simpler. These handle background refetching, stale-while-revalidate, optimistic updates, and cache invalidation. They are not global state libraries - they are async synchronisation tools.
UI state - what the user has open, selected, or typed that does not need to survive a page refresh. Zustand for most things: minimal API, no boilerplate, easy to test in isolation. Redux Toolkit when the state logic is genuinely complex - multi-step transactions, undo/redo, cross-slice derived state. MobX when the team already knows it and the reactive model fits.
The mistake we see most: global Redux store for data that should be a server fetch, and prop drilling for state that should be in Zustand. We document the split in the architecture doc before writing a single hook.
Core Web Vitals as a design discipline
Fast pages are not an optimization at the end. They come from decisions made early:
- LCP (largest contentful paint) - preload the hero image and hero font. No layout shift for above-the-fold content.
- INP (interaction to next paint) - keep hydration islands small. Defer non-critical JS to idle.
- CLS (cumulative layout shift) - always set image width and height. Reserve space for anything that loads late.
We instrument CWV with Vercel Analytics or a self-hosted alternative. The dashboard tells us when a deploy regressed something. Regressions are caught and fixed, not discovered from a user complaint.
Accessibility, not theatre
Every ship passes a keyboard-only pass and a screen-reader pass (VoiceOver + NVDA). We use Radix under shadcn because the ARIA is done correctly upstream. axe-core runs on every PR. Contrast ratios are part of the design token system, not a last-minute fix.
For UK and EU clients this is not optional - WCAG 2.2 AA is required under the European Accessibility Act and the UK Equality Act. We build it in from day one, not audited out at the end.
Accessibility is not a compliance checkbox. An app that works with a keyboard works for a user holding a baby, a user with tendonitis, a user on a train, and a user behind a screen reader.
Tailwind, shadcn/ui, and Bootstrap - which and why
Most component libraries (MUI, Chakra, Ant Design) solve the “I need a button” problem and create two new problems:
- The styles live in node_modules. Changing a border-radius means fighting the library or shipping CSS overrides.
- The bundle bloats. Even with tree-shaking, a component library is 80 to 200KB of CSS + JS you did not write.
shadcn/ui on Tailwind is our default for custom UIs. Radix primitives (which handle ARIA correctly) styled with Tailwind in your repo. Want to change the button? Edit the file. No library upgrade path breaks your design.
Bootstrap 5 has its place - rapid prototypes, internal admin panels, early MVPs where the goal is a working interface in days, not a pixel-perfect one in weeks. It is faster to scaffold. The trade-off is less CSS control and a grid system you did not design. We use it when the brief earns it.
How we test frontend code
Shipping a component that looks right on your laptop is not the same as shipping one that works on an iPhone SE at 3G, with a screen reader running, in a right-to-left locale.
What we check before any merge:
Visual parity - font weights, spacing, and layout matched to the design file on Chrome, Firefox, and Safari. Playwright visual regression catches pixel-level drift on every PR.
Component states - every interactive element tested across all states: default, hover, focus, active, disabled, loading, error, empty, and tooltip. Most bugs live in the states you tested once and assumed were done.
Responsive layout - mobile (375px), tablet (768px), desktop (1280px), and wide (1920px). Flexbox and grid contracts checked at each breakpoint. No stretched images or broken overflow.
Content tolerance - components tested with minimum content (one word) and maximum content (500 characters). Text wrapping and overflow are explicit, not assumed.
Accessibility - axe-core on every PR. Keyboard navigation tested on every flow. VoiceOver and NVDA pass before ship. Contrast ratios enforced at the design token level.
Performance - Lighthouse on every PR via CI. Regressions block merge. LCP, INP, and CLS tracked against baseline. Animations checked at 60fps on a throttled device profile.
E2E coverage - Playwright covering critical user flows against a live preview URL. Not 100% coverage - the flows that, if broken, would stop a user from completing their goal.
Design systems and component libraries
- Design tokens in CSS variables - one source of truth for colour, spacing, type scale, radii, shadows.
- shadcn/ui on Radix primitives - copy-in components, not a dependency. ARIA correct upstream.
- Storybook for component libraries that need isolated review across teams.
- Figma design tokens synced to CSS variables via a token pipeline when the design team changes often.
Deploys, CI, and rollbacks
- GitHub Actions - type-check, unit tests, Playwright against preview on every PR.
- Preview deploys per branch. Product reviews happen on real URLs.
- Production behind a protected branch. Manual gate before promote.
- One-click rollback. vercel rollback or equivalent. We test the rollback in the first week, before we need it.
What we will not do
- Rewriting jQuery on a deadline. If the codebase is jQuery, we maintain it and migrate incrementally. A big-bang rewrite that stops all features for six months is the wrong call almost every time.
- Webflow as the final answer. Fine for a 4-week marketing MVP. Not fine for a product that needs to grow.
- CSS-in-JS runtimes on new builds. The performance tax is real. The ergonomics win is marginal against Tailwind + CSS variables.
- Chasing 100% test coverage. We chase the tests that catch regressions, not the metric.
Why teams pick our frontend delivery
We pick the framework to match the product: Next.js for apps, plain React + Vite for SPAs, Bootstrap where speed beats precision. Opinionated, but not married to any of them.
Core Web Vitals go on the dashboard from week 1, not after a user complaint. Accessibility is engineering: Radix for ARIA, axe-core on every PR, keyboard pass before every ship. Design tokens live in CSS variables so a brand colour change moves one line, not twelve files. Legacy codebases maintained and migrated without a production freeze. Senior engineers on the product. Source and design files yours at launch.