Server-Side Rendering
Hollows UI works seamlessly with SSR and SSG frameworks with zero hydration mismatches.
Overview
Hollows UI is designed for server-first architectures. The empty state output is deterministic -- the same props always produce the same HTML. This means SSR, SSG, and ISR all work out of the box without any special configuration.
SSR compatibility matrix
| Framework | SSR | SSG | Streaming |
|---|---|---|---|
| Next.js (App Router) | ✓ | ✓ | ✓ |
| Next.js (Pages Router) | ✓ | ✓ | -- |
| Nuxt 3 | ✓ | ✓ | ✓ |
| SvelteKit | ✓ | ✓ | ✓ |
| Astro | ✓ | ✓ | -- |
Why No Hydration Mismatch
Hydration mismatches happen when the server renders different HTML than the client expects. Hollows avoids this because:
- 1.Deterministic output. The empty state HTML is derived purely from props and the static registry. No randomness, no Date.now(), no browser-only APIs.
- 2.Inline SVGs, not images. Illustrations are rendered as inline SVG elements, not fetched from URLs. The same SVG string renders identically on server and client.
- 3.CSS-only responsive behavior. Layout changes are handled via CSS media/container queries, not JavaScript resize listeners that produce different initial values on server vs client.
- 4.No useEffect for rendering. The Hollow component renders synchronously. There is no client-side-only effect that changes the initial output.
Next.js
Hollows works with both the App Router and Pages Router. With the App Router, the Hollow component can be used in Server Components directly.
App Router (Server Component)
import { Hollow } from 'hollows-ui/react'import { getMessages } from '@/lib/api'// This is a Server Component — no 'use client' neededexport default async function InboxPage() { const messages = await getMessages() return ( <Hollow name="user-inbox" empty={messages.length === 0}> <MessageList messages={messages} /> </Hollow> )}App Router (Client Component)
'use client'import { Hollow } from 'hollows-ui/react'import { useMessages } from '@/hooks/use-messages'export function Inbox() { const { data, isLoading } = useMessages() return ( <Hollow name="user-inbox" empty={!data || data.length === 0} loading={isLoading}> {data && <MessageList messages={data} />} </Hollow> )}Pages Router
import { Hollow } from 'hollows-ui/react'export async function getServerSideProps() { const messages = await fetchMessages() return { props: { messages } }}export default function InboxPage({ messages }) { return ( <Hollow name="user-inbox" empty={messages.length === 0}> <MessageList messages={messages} /> </Hollow> )}Nuxt
Use the Vue adapter with Nuxt 3. The component renders identically on server and client.
<script setup>import { Hollow } from 'hollows-ui/vue'const { data: messages } = await useFetch('/api/messages')</script><template> <Hollow name="user-inbox" :empty="!messages?.length"> <MessageList :messages="messages" /> </Hollow></template>For Nuxt, register the Hollows plugin to load the registry automatically:
import { defineNuxtPlugin } from '#app'import { loadRegistry } from 'hollows-ui/vue'import registry from '~/hollows/registry.json'export default defineNuxtPlugin(() => { loadRegistry(registry)})SvelteKit
The Svelte adapter works with SvelteKit's load functions and SSR mode.
<script> import { Hollow } from 'hollows-ui/svelte' export let data</script><Hollow name="user-inbox" empty={!data.messages.length}> <MessageList messages={data.messages} /></Hollow>import type { PageServerLoad } from './$types'export const load: PageServerLoad = async () => { const messages = await fetchMessages() return { messages }}Streaming SSR
Hollows works with React 18 streaming and Suspense. Wrap your data-fetching component in Suspense, and use the Hollow component inside.
import { Suspense } from 'react'import { Hollow } from 'hollows-ui/react'export default function InboxPage() { return ( <Suspense fallback={<InboxSkeleton />}> <InboxContent /> </Suspense> )}async function InboxContent() { const messages = await getMessages() return ( <Hollow name="user-inbox" empty={messages.length === 0}> <MessageList messages={messages} /> </Hollow> )}The empty state streams as part of the Suspense resolution. There is no flash of content -- the user sees either the skeleton or the final state (content or empty).
Static Generation
For statically generated pages where the empty/non-empty state is known at build time, Hollows renders the correct output during static generation. The client-side JavaScript only needs to handle dynamic transitions.
import { Hollow } from 'hollows-ui/react'// Static generation — empty state is baked into the HTMLexport default async function BlogPage() { const posts = await getPosts() return ( <Hollow name="blog-posts" empty={posts.length === 0}> <PostGrid posts={posts} /> </Hollow> )}// Revalidate every 60 secondsexport const revalidate = 60