Files
supabase/apps/studio/components/interfaces/App/ShellFallback.tsx
Alaister Young 1144b83885 feat(studio): add loading and fallback states to SPA shell (#48185)
The prerendered TanStack SPA shell (`_shell.html`) had a visually empty
body, so every cold load showed a blank page until the JS bundle
downloaded and hydrated. This bakes proper fallback states into the
shell as static HTML — none of them rely on JS executing.

**Added:**
- `ShellFallback` component, rendered as the `ClientOnly` fallback
around the root `<Outlet />` — during the shell prerender it serializes
into `_shell.html`, and on the client it unmounts the moment the app
mounts (no hydration mismatch: `ClientOnly` renders the fallback on the
server and first client render)
- Animated `LogoLoader` (Supabase logo outline) centered on screen — the
stroke-dash animation is pure CSS so it runs before any JS executes
- Stuck-load help text that fades in after 7s via CSS `animation-delay`
(clear cookies / reload, contact support@supabase.com — the support
email is gated behind `IS_PLATFORM` so self-hosted builds don't get it)
- `noscript` message for JS-disabled browsers, which also hides the
loader so users don't see an infinite spinner (uses
`dangerouslySetInnerHTML` so React hydration never diffs noscript
children)
- `data-nosnippet` on both text blocks so Google doesn't surface the
boilerplate as the search snippet for dashboard URLs (the one shell
serves every route)

## To test

All on the Vercel preview:

- Open the preview — on a cold load you should catch the animated logo
loader before the app mounts (throttle to "Slow 4G" in devtools if it
flashes by too fast), and it never reappears on client-side navigation
- In devtools, block the JS bundle (Network tab → right-click the
`/assets/index-*.js` request → Block request URL) and reload — the
loader animates on its own, and the help text (clear cookies / contact
support) fades in after ~7s
- Disable JavaScript (devtools command palette → "Disable JavaScript")
and reload — no spinner, just the "requires JavaScript" message
- View page source (or `curl` any preview URL) — the body contains the
logo SVG, the help text, and the noscript block, all with
`data-nosnippet` on the text

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added a client-aware loading shell for Studio during initialization.
* Shows a branded loader with a help message that appears after a short
delay.
  * Includes platform-specific support contact details when available.
* **Bug Fixes**
* Prevents partial or incomplete content from rendering before the app
is ready.
* Improves consistency for no-JavaScript fallback rendering to avoid
hydration mismatches.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-07-22 22:08:52 +08:00

74 lines
2.6 KiB
TypeScript

import { LogoLoader } from 'ui'
import { IS_PLATFORM } from '@/lib/constants'
// Baked into the prerendered SPA shell (_shell.html) — this is the static HTML
// shown on every cold load until the JS bundle hydrates, so nothing here can
// rely on JS: the loader animation, the 7s stuck-load help reveal, and the
// noscript message are all pure CSS/HTML.
export function ShellFallback() {
return (
<>
<style>{`
/* visibility (not just opacity) so the text is also hidden from
screen readers until the reveal. */
#studio-shell-help {
visibility: hidden;
opacity: 0;
animation: studio-shell-help-reveal 0.3s ease-out 7s forwards;
}
@keyframes studio-shell-help-reveal {
to {
visibility: visible;
opacity: 1;
}
}
`}</style>
<div
id="studio-shell-loader"
className="fixed inset-0 flex flex-col items-center justify-center gap-8 p-4"
>
{/* LogoLoader fills its parent, so give it a fixed-height box — otherwise
it stretches to the full column and pushes the help text offscreen. */}
<div className="h-[62px] w-full">
<LogoLoader />
</div>
{/* data-nosnippet keeps Google from surfacing this boilerplate as the
search snippet for dashboard URLs (the shell serves every route). */}
<p
id="studio-shell-help"
data-nosnippet=""
className="max-w-md text-center text-sm text-foreground-light"
>
Taking longer than expected? Try clearing your browser cookies and reloading the page.
{IS_PLATFORM && (
<>
{' '}
If the problem persists, contact{' '}
<a href="mailto:support@supabase.com" className="underline">
support@supabase.com
</a>
.
</>
)}
</p>
</div>
{/* dangerouslySetInnerHTML keeps React hydration from diffing noscript
children, which the server renders as real markup but the client
treats as raw text. */}
<noscript
dangerouslySetInnerHTML={{
__html: `
<style>#studio-shell-loader{display:none}</style>
<div data-nosnippet class="fixed inset-0 flex items-center justify-center p-4">
<p class="text-sm text-foreground-light text-center">
Supabase Studio requires JavaScript. Enable JavaScript in your browser settings and reload the page.
</p>
</div>
`,
}}
/>
</>
)
}