Overview
Stride Design System is fully compatible with Next.js 15 and React Server Components . Built on React Aria Components, all Stride components handle SSR automatically with proper hydration support.
Next.js 15 Compatibility
Framework Support
Stride works seamlessly with:
✅ Next.js 15 App Router
✅ React Server Components (RSC)
✅ Server Actions
✅ Streaming and Suspense
✅ Turbopack (dev mode)
Installation
SSRProvider Setup
Why SSRProvider?
React Aria Components use auto-generated IDs for accessibility. SSRProvider ensures these IDs match between server and client, preventing hydration mismatches.
Root Layout Configuration
Wrap your app with SSRProvider
Create a Client Provider component: src/components/ClientProviders.tsx
"use client" ;
import { SSRProvider } from "react-aria" ;
import { BrandInitializer } from "stride-ds" ;
interface ClientProvidersProps {
children : React . ReactNode ;
}
export function ClientProviders ({ children } : ClientProvidersProps ) {
return (
< SSRProvider >
< BrandInitializer />
{ children }
</ SSRProvider >
);
}
Add to Root Layout
Import and use in your Next.js layout: import type { Metadata } from "next" ;
import { ClientProviders } from "@/components/ClientProviders" ;
import "stride-ds/styles" ;
import "./globals.css" ;
export const metadata : Metadata = {
title: "My App" ,
description: "Built with Stride Design System" ,
};
export default function RootLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
return (
< html lang = "en" >
< body >
< ClientProviders > { children } </ ClientProviders >
</ body >
</ html >
);
}
Import Styles
Add Stride styles to your layout or globals.css: import "stride-ds/styles" ;
Or in your CSS: @import "stride-ds/styles" ;
Server vs Client Components
Understanding the Distinction
Server Components
Client Components
Server Components run on the server only: // ✅ Server Component (default in App Router)
import { Card , CardHeader , CardTitle , CardContent } from 'stride-ds' ;
export default function Page () {
return (
< Card >
< CardHeader >
< CardTitle > Server-Rendered Card </ CardTitle >
</ CardHeader >
< CardContent >
This content is rendered on the server
</ CardContent >
</ Card >
);
}
Benefits:
Reduced bundle size
Direct database access
Better SEO
Faster initial load
Client Components run on both server (SSR) and client: components/InteractiveCard.tsx
"use client" ;
import { useState } from 'react' ;
import { Button , Card } from 'stride-ds' ;
export function InteractiveCard () {
const [ count , setCount ] = useState ( 0 );
return (
< Card >
< p > Count: { count } </ p >
< Button onPress = { () => setCount ( count + 1 ) } >
Increment
</ Button >
</ Card >
);
}
Use when you need:
Event handlers (onClick, onChange)
State and effects (useState, useEffect)
Browser APIs (localStorage, window)
Interactive components
Stride Component Types
Most Stride components are Client Components (marked with "use client") because they use React Aria Components which require interactivity. However, they still benefit from SSR!
App Router Integration
Page Structure
Here’s how to structure your Next.js 15 pages with Stride:
import { Card , CardHeader , CardTitle , CardContent } from 'stride-ds' ;
import { InteractiveForm } from '@/components/InteractiveForm' ;
import { getServerData } from '@/lib/data' ;
// ✅ This is a Server Component
export default async function DashboardPage () {
// Fetch data on the server
const data = await getServerData ();
return (
< div className = "container mx-auto py-8" >
< h1 className = "text-3xl font-bold mb-6" > Dashboard </ h1 >
{ /* Static cards rendered on server */ }
< div className = "grid grid-cols-3 gap-4 mb-8" >
< Card variant = "elevated" >
< CardHeader >
< CardTitle > Total Users </ CardTitle >
</ CardHeader >
< CardContent >
{ data . totalUsers }
</ CardContent >
</ Card >
< Card variant = "elevated" >
< CardHeader >
< CardTitle > Revenue </ CardTitle >
</ CardHeader >
< CardContent >
$ { data . revenue }
</ CardContent >
</ Card >
</ div >
{ /* Interactive client component */ }
< InteractiveForm initialData = { data } />
</ div >
);
}
Loading States
Leverage Next.js 15 loading states:
app/dashboard/loading.tsx
import { Card , CardHeader , CardContent } from 'stride-ds' ;
export default function Loading () {
return (
< div className = "container mx-auto py-8" >
< Card >
< CardHeader >
< div className = "h-6 w-48 bg-gray-200 rounded animate-pulse" />
</ CardHeader >
< CardContent >
< div className = "h-4 w-full bg-gray-200 rounded animate-pulse" />
</ CardContent >
</ Card >
</ div >
);
}
Error Boundaries
Handle errors with Stride components:
"use client" ;
import { Button , Card , CardHeader , CardTitle , CardContent } from 'stride-ds' ;
export default function Error ({
error ,
reset ,
} : {
error : Error ;
reset : () => void ;
}) {
return (
< Card variant = "elevated" className = "max-w-md mx-auto mt-8" >
< CardHeader >
< CardTitle > Something went wrong </ CardTitle >
</ CardHeader >
< CardContent >
< p className = "text-red-600 mb-4" > { error . message } </ p >
< Button variant = "primary" onPress = { reset } >
Try again
</ Button >
</ CardContent >
</ Card >
);
}
SSR Utilities
Stride provides utilities for SSR-safe operations:
useIsSSR Hook
import { useIsSSR } from 'react-aria' ;
export const useSSRSafeId = () => {
const isSSR = useIsSSR ();
return isSSR ;
};
ClientOnly Component
Render content only on the client:
import React from 'react' ;
import { useIsSSR } from 'react-aria' ;
export const ClientOnly : React . FC <{
children : React . ReactNode ;
fallback ?: React . ReactNode ;
}> = ({ children , fallback = null }) => {
const isSSR = useIsSSR ();
if ( isSSR ) {
return <> { fallback } </> ;
}
return <> { children } </> ;
};
Usage Example
import { ClientOnly } from 'stride-ds' ;
import { BrowserOnlyFeature } from './BrowserOnlyFeature' ;
export function MyComponent () {
return (
< ClientOnly fallback = { < div > Loading... </ div > } >
< BrowserOnlyFeature />
</ ClientOnly >
);
}
Safe Browser APIs
Access browser APIs safely:
export const safeWindow = () => {
return typeof window !== 'undefined' ? window : undefined ;
};
export const safeLocalStorage = {
getItem : ( key : string ) : string | null => {
if ( typeof window === 'undefined' ) return null ;
try {
return localStorage . getItem ( key );
} catch {
return null ;
}
},
setItem : ( key : string , value : string ) : void => {
if ( typeof window === 'undefined' ) return ;
try {
localStorage . setItem ( key , value );
} catch {
// Fail silently
}
},
};
Hydration Best Practices
SSRProvider ensures ID consistency between server and client. // ✅ SSRProvider handles this automatically
< SSRProvider >
< Input label = "Email" /> // Gets consistent ID
</ SSRProvider >
Avoid Date.now() or Math.random()
Don’t use random values that differ between server and client. // ❌ Bad: Causes hydration mismatch
< div id = { `component- ${ Math . random () } ` } />
// ✅ Good: Use React Aria's useId or let SSRProvider handle it
import { useId } from 'react-aria' ;
const id = useId ();
Use ClientOnly for browser-specific content. // ❌ Bad: Different on server vs client
{ typeof window !== 'undefined' && < BrowserFeature /> }
// ✅ Good: Explicit client-only rendering
< ClientOnly >
< BrowserFeature />
</ ClientOnly >
useEffect for Client-Only Code
Put browser-specific code in useEffect. "use client" ;
import { useEffect , useState } from 'react' ;
export function Component () {
const [ mounted , setMounted ] = useState ( false );
useEffect (() => {
setMounted ( true );
// Browser-specific code here
}, []);
if ( ! mounted ) return null ;
return < div > Client content </ div > ;
}
Server Actions with Stride
Use Server Actions with Stride forms:
import { Button , Input , Card } from 'stride-ds' ;
import { ContactForm } from './ContactForm' ;
// Server Action
async function submitContact ( formData : FormData ) {
'use server' ;
const email = formData . get ( 'email' );
const message = formData . get ( 'message' );
// Process on server
await saveToDatabase ({ email , message });
return { success: true };
}
export default function ContactPage () {
return (
< Card className = "max-w-md mx-auto" >
< ContactForm action = { submitContact } />
</ Card >
);
}
app/contact/ContactForm.tsx
"use client" ;
import { Button , Input } from 'stride-ds' ;
import { useFormState } from 'react-dom' ;
export function ContactForm ({ action }) {
const [ state , formAction ] = useFormState ( action , null );
return (
< form action = { formAction } >
< Input
name = "email"
label = "Email"
type = "email"
isRequired
/>
< Input
name = "message"
label = "Message"
isRequired
/>
< Button type = "submit" variant = "primary" >
Send Message
</ Button >
{ state ?. success && < p > Message sent! </ p > }
</ form >
);
}
Streaming Use Suspense boundaries for progressive loading: < Suspense fallback = { < LoadingCard /> } >
< AsyncDataCard />
</ Suspense >
Dynamic Imports Code-split heavy components: const HeavyForm = dynamic (
() => import ( './HeavyForm' ),
{ ssr: false }
);
Partial Prerendering Combine static and dynamic content: export const experimental_ppr = true ;
Server Components First Use Server Components by default, add “use client” only when needed.
Common Issues
Hydration Mismatch
If you see “Hydration failed” errors, check:
Is SSRProvider wrapping your app?
Are you using Date.now() or Math.random()?
Is browser-specific code in useEffect or ClientOnly?
// ❌ Causes hydration error
const timestamp = Date . now ();
// ✅ Fixed with useEffect
const [ timestamp , setTimestamp ] = useState < number | null >( null );
useEffect (() => setTimestamp ( Date . now ()), []);
Missing “use client”
// ❌ Error: useState is not allowed in Server Components
import { useState } from 'react' ;
import { Button } from 'stride-ds' ;
export function Counter () {
const [ count , setCount ] = useState ( 0 );
return < Button onPress = { () => setCount ( count + 1 ) } > { count } </ Button > ;
}
// ✅ Add "use client" directive
"use client" ;
import { useState } from 'react' ;
import { Button } from 'stride-ds' ;
export function Counter () {
const [ count , setCount ] = useState ( 0 );
return < Button onPress = { () => setCount ( count + 1 ) } > { count } </ Button > ;
}
Next Steps
TypeScript Guide Learn about type-safe patterns and exports
Components Explore all available components