https://www.jonoalderson.com/conjecture/its-time-for-modern-css-to-kill-the-spa/
[jono-alderson]
Jono Alderson
Independent technical SEO consultant
[ ] Menu
* Home
* About
* Speaking & Events
* Consulting
* Blog
* Recommendations
+ Tech & Tools
+ Books
* Contact
It's time for modern CSS to kill the SPA
24th July, 2025
[32defd3f59a0]
[file_00000000be4861f49d1f8af54cec64de]
Native CSS transitions have quietly killed the strongest argument for
client-side routing. Yet people keep building terrible apps instead
of performant websites.
The app-like fallacy
"Make it feel like an app."
At some point during the scoping process, someone says the words.
A CMO. A digital lead. A brand manager. And with that single phrase,
the architecture is locked in: it'll be an SPA. Probably React. Maybe
Vue. Almost certainly deployed on Vercel or Netlify, bundled with
a headless CMS and a GraphQL API for good measure.
But the decision wasn't really about architecture. It wasn't even
about performance, scalability, or content management. It was about
interactions. About how the site would feel when you click around.
The assumption was simple: Seamless navigation requires us to build
an app.
That assumption is now obsolete.
The false promise of SPAs
The reason SPAs became the default wasn't because they were better.
It was because, for a while, they were the only way to deliver
something that felt fluid - something that didn't flash white between
pages or jank the scroll position.
But here's the uncomfortable truth: most SPAs don't actually deliver
the polish they promise.
What you usually get is:
* A page transition that looks smooth, until you realise it's just
fading between two loading states
* Broken scroll restoration
* Inconsistent focus behaviour
* Delayed navigation while scripts rehydrate components
* Layout shift, content popping, or full-page skeletons
* A performance hit that's entirely disproportionate to the effect
This isn't theoretical. Look at most sites built with Next.js,
Gatsby, or Nuxt. They're shipping kilobytes (often megabytes) of
JavaScript just to fake native navigation. Routing logic, hydration
code, loading spinners - all just to stitch together something that
browsers already knew how to do natively.
Instead of smoothness, you get simulation. And instead of a fast,
stable, SEO-friendly experience, you get a heavy JavaScript machine
trying to recreate the native behaviour we threw away.
We've been adding mountains of JS to "feel" fast, while making
everything slower.
An aside - I went deeper on this in JavaScript broke the web,
where I outlined how our obsession with JS-first development is
actively eroding the web's foundations.
The web grew up
While we were busy reinventing navigation in JavaScript, the platform
quietly solved the problem.
Modern browsers - specifically Chromium-based ones like Chrome and
Edge - now support native, declarative page transitions. With the
View Transitions API, you can animate between two documents -
including full page navigations - without needing a single line of
JavaScript.
Yes, really.
What we're calling "modern CSS" here is shorthand for View
Transitions, Speculation Rules, and a return to native browser
features that were always designed to handle navigation, interaction,
and layout. These capabilities let us build rich, seamless
experiences - without rewriting the browser in JavaScript.
An aside - CSS is also declarative, resilient, expressive,
scalable, and increasingly intuitive. It's accessible to anyone
who can write plain HTML. And that structural clarity reinforces
everything I argued in Why semantic HTML still matters - that
clean, meaningful markup is the bedrock of performance,
maintainability, and machine readability.
That means you can:
* Fade between pages
* Animate shared elements (e.g. thumbnails - product detail)
* Maintain persistent elements like headers or navbars
* Do it all with real URLs, real page loads, and no JS routing
hacks
Let's make this concrete.
Basic cross-page fade transition
With just a few lines of CSS, you can trigger smooth visual
transitions between pages.
On both the current and destination page, add:
@view-transition {
navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation: fade 0.3s ease both;
}
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
That's it. The browser handles the transition - no client-side
routing, no hydration, no loading spinners.
Shared element transitions
Want to animate a thumbnail image into its full-size product
counterpart on the next page?
No JavaScript needed - just assign the same view-transition-name to
the element on both pages:
On the product listing page:
On the product detail page:
The browser matches and animates the elements between navigations.
You can animate position, scale, opacity, layout - all with CSS.
But what if I need JS-driven transitions?
You can manually trigger transitions inside a page too:
document.startViewTransition(() => {
document.body.classList.toggle('dark-mode');
});
Perfect for things like tab toggles or theme switches -- without
needing a framework or hydration layer.
Speculation rules: instant navigation without JS
View Transitions make things smooth. But what about fast?
That's where Speculation Rules come in. This lets the browser preload
or prerender full pages based on user behaviour - like hovering or
touching a link - before they click.
The result? Navigation that's instant. No waiting. No loading. No
spinners.
[?][?] A Note of Caution
Speculation Rules are a performance multiplier. On a lean site, they
make things feel instant. But if your pages are slow, bloated, or
JS-heavy, speculation just front-loads those costs.
If your site is bloated, speculation will still speculate - and the
user pays the price.
That means wasted CPU, network bandwidth, and mobile battery - often
for pages the user never even visits.
Use them carefully. On a fast site, they're magic. On a slow one,
they're a trap.
Browsers want to help - if we let them
Modern browsers are smarter than ever. They're constantly looking for
ways to improve speed, responsiveness, and efficiency - but only if
we let them.
One of the clearest examples is the Back/Forward Cache (bfcache),
which allows entire pages to be snapshotted and restored instantly
when users navigate back or forward.
It's effectively free performance - but only for pages that behave.
That means no rogue JavaScript, no intercepted navigation, no
lifecycle chaos. Just clean, declarative architecture. Just HTML
and CSS.
Unsurprisingly, this plays beautifully with a well-structured,
multi-page site. But for most SPAs, it's a non-starter. The very
design patterns that define them - hijacked routing, client-side
rendering, complex state management - break the assumptions that
bfcache relies on.
This is a microcosm of a much bigger theme: browsers are evolving to
reward simplicity and resilience. They're building for the kind of
web we should have been embracing all along. And SPAs are
increasingly the odd ones out.
SPA vs MPA: a performance reality check
Average Next.js marketing site
+ JS bundle: 1 - 3MB
+ TTI: ~3.5 - 5s (depending on hydration strategy)
+ Route transitions: simulated
+ SEO: complex, fragile
+ Scroll/anchor behaviour: unreliable
Modern MPA + View Transitions + Speculation Rules
+ JS bundle: 0KB (optional enhancements only)
+ TTI: ~1s
+ Route transitions: real, native
+ SEO: trivial
+ Scroll/focus/history: browser-default and perfect
Modern CSS doesn't just replace SPA behaviour - it outperforms it.
Don't build a website like it's an app
Most websites aren't apps.
They don't need shared state. They don't need client-side routing.
They don't need interactive components on every screen. But somewhere
along the way, we stopped making the distinction.
Now we're building ecommerce stores, documentation portals, marketing
sites, and blogs using stacks designed for real-time collaborative
UIs. It's madness.
A homepage with six content blocks and a contact form doesn't need
hydration, suspense boundaries, and a rendering strategy.
It needs fast markup, clean URLs, and maybe - maybe - a bit of
interactivity layered on top.
And yet, on every project:
1. A stakeholder says, "make it feel like an app."
2. A dev team reaches for Next.js or Nuxt.
3. Routing goes client-side.
4. Performance falls off a cliff.
5. Now you need edge functions, streaming, ISR, loading strategies,
and a debugging plan.
6. And somehow... it still feels slower than a regular link click and
a CSS animation.
This isn't about being anti-framework. It's about being intentional.
Use React if you want. Use Tailwind, Vite, whatever. Just don't ship
it all to the browser unless you need to.
Build a site like a site. Use HTML. Use navigation. Use the platform.
It's faster, simpler, and better for everyone.
Build for the web we have
SPAs were a clever solution to a temporary limitation. But that
limitation no longer exists.
We now have:
* Native, declarative transitions between real pages
* Instantaneous prerendered navigation via Speculation Rules
* Graceful degradation
* Clean markup, fast loads, and real URLs
* A platform that wants to help - if we let it
If you're still building your site as an SPA for the sake of
"smoothness," you're solving a problem the browser already fixed -
and you're paying for it in complexity, performance, and
maintainability.
Use modern server rendering. Use actual pages. Animate with CSS.
Preload with intent. Ship less JavaScript.
Build like it's 2025 - not like you're trapped in a 2018 demo of
Gatsby.
You'll end up with faster sites, happier users, and fewer regrets.
guest
Label [ ]
[ ] Name*
[ ] Email*
[Post Comment]
guest
Label [ ]
[ ] Name*
[ ] Email*
[Post Comment]
18 Comments
Inline Feedbacks
View all comments
Bob
Bob
Jul 25, 2025 6:53 pm
You almost sound like you're anticipating Datastar.
Reply
Andrew
Andrew
Jul 25, 2025 7:56 pm
Just commenting in jest, but command-clicking the link to your
"JavaScript broke the web (and called it progress)" article caused
_this page_ to navigate alongside the standard "open in new tab"
behavior. You may have broken the web.
Reply
Jono Alderson
Jono Alderson
Reply to Andrew
Jul 26, 2025 10:12 am
Thanks - fixed! Ironic stupid JavaScript bug
Reply
Andrew
Andrew
Jul 25, 2025 7:59 pm
Sorry, that was mildly rude to leave only that prior comment without
more context. In general -- I appreciate your advocacy for _just using
the modern web because it's awesome_. Thanks!
Reply
Nicholas
Nicholas
Jul 25, 2025 8:54 pm
This is amazing. I really love the idea of defaulting to native
browser features until there's a real need for something more
advanced.
Frameworks are great for developer experience -- no doubt. But too
often, they push you into shipping JS-heavy solutions that simulate
what the browser already does natively.
Are there frameworks that offer the same great DX, but lean into
these new native capabilities -- things like View Transitions and
Speculation Rules -- and handle it all at build time? And ready for
production and heavy traffic?
Reply
Tom Boutell
Tom Boutell
Jul 25, 2025 11:43 pm
I want to believe! But Firefox support isn't 100% yet:
View Transition API - Web APIs | MDN https://share.google/
lHz2xma4MTlhMaTeQ
Reply
john Andrews
john Andrews
Jul 26, 2025 12:14 am
Wow! An essay from Jono I actually like and agree with! Congrats!
There's an old fable about a man going on a journey (to SomeWhere)
and as he meets travelers he gets steered to side stops and cute
towns and scenic routes.. The travel wears him out before he gets
anywhere near SomeWhere, Every distraction was rationally justified...
even with some saying that staying on the standard route was like
wasting the trip.. missing all of the charm by staying on the sterile
highways.
Sometimes.... often?... the actual planned destination was the point of
the entire journey. Without extreme stamina and extra resources, the
divergent traveler ends up missing the whole purpose.
I called Edge SEO "Wedge SEO" because of the way devs sidelined
knowledgeable SEO people by driving a wedge between those hoping to
pursue new goals (which then demanded Edge techniques as a means of
SEO survival) and those chasing the original goals... which were
business goals. Merit-based SEO chasing performance got sidelined for
"modern" solutions that cost a lot more time and resources, with
complexity, and yet... didn't often perform well.
With complexity hidden in the edge cloud tech, further compounding
problems.
Reply
Steve
Steve
Jul 26, 2025 12:30 am
I'm listening!!!
Well said.
Recently I've been thinking of the web browser as an operating system
with native primatives and some pretty sensable defaults.
Reply
Manoj
Manoj
Jul 26, 2025 4:24 am
This was a fantastic read Jono - great article and very insightful
indeed.
Reply
Casey Dwayne
Casey Dwayne
Jul 26, 2025 6:45 am
I absolutely agree to a point. The amount of technical debt some
projects accrue is insane. Interested to check out these things.
I don't think we're out of the framework woodworks yet but with the
advances in JS, CSS, and now native browser behavior I am optimistic
we'll return to a cleaner, more sensible DX.
Reply
Stephen Reed
Stephen Reed
Jul 26, 2025 10:19 am
What would be a best practice prompt for Claude Code to use this
modern MPA design pattern?
Reply
Jono Alderson
Jono Alderson
Reply to Stephen Reed
Jul 26, 2025 12:17 pm
"use WordPress"
Reply
Caspar Harmer
Caspar Harmer
Reply to Stephen Reed
Jul 26, 2025 8:33 pm
Probably not many code examples scooped up by LLMs yet, so vibe
coding might be out.
Reply
martrapp
martrapp
Jul 26, 2025 11:39 am
Hi Jono,
Motivating article, and definitely true for many websites. Thanks for
spreading the word about how the View Transition API will change the
way we build websites, and how users will tell old-school sites from
modern ones in the future.
I understand that you included the first code snippet to show how
easy it is to enable cross-document view transitions and style the
pseudo-elements. For an example likely to be copied by people new to
the API, you might consider
@view-transition {
navigation: auto;
}
::view-transition-group(*) {
animation-duration: 0.3s;
}
Not only is it shorter, but more important, wouldn't override the
API's default styling (inheritance, isolation) so carelessly.
Reply
Jono Alderson
Jono Alderson
Reply to martrapp
Jul 26, 2025 12:18 pm
Ah, great example! Thanks!
Reply
Sylvain
Sylvain
Jul 26, 2025 12:20 pm
The article could also mention that SPA require more efforts in term
of web accessibility, and I am not just talking about the scroll
restoration. When a browser loads a new page, it will announce it to
screen readers. With a SPA people will often need to implement a live
region and an autofocus on the page h1.
Reply
Jono Alderson
Jono Alderson
Reply to Sylvain
Jul 26, 2025 1:10 pm
Absolutely! I touch on this in https://www.jonoalderson.com/
conjecture/why-semantic-html-still-matters/ and https://
www.jonoalderson.com/conjecture/
javascript-broke-the-web-and-called-it-progress/.
Reply
Brian
Brian
Jul 26, 2025 4:19 pm
I remember when web development used to be enjoyable... and then SPAs
reared their ugly heads, sucking the joy out of front-end work with
their preposterously overwrought frameworks.
I cannot wait to share this article with the next person who dares to
utter, "We should use for our next CRUD project!"
Reply
I'm speaking at...
SEOktoberfest / G50, 2025
2025-09-21 / 2025-09-26
Stanglwirt, Austria
seoktoberfest.net
Spotlight, 2025
2025-10-29
Amsterdam
spotlightconf.com
Recent blog posts...
[20250725_152851-rotated]
What the world's first factory can teach us about AI
25th July, 2025
[file_00000000aaf061faafa67952f692b]
Why Semantic HTML Still Matters
21st July, 2025
Hire me!
I'm an award-winning consultant, and I'm available for hire.
Ready to take your website's technical SEO, performance, or
structured data strategy to the next level?
Hire me as a consultant
Get in touch
Email: me@jonoalderson.com
* [twi]Visit my Twitter profile
* [bsk]Visit my Bluesky profile
* [lin]Visit my LinkedIn profile
(c) Copyright 2025 Applied Energistics LTD.
* Terms
* Privacy
* Cookies
* Contact
JonoAlderson.com is operated by Applied Energistics LTD - a company
registered in England and Wales (Company # 11681212), at 38 Jute
Road, York, YO26 5EN, United Kingdom
[ ] Insert