Skip to main content

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

npm install stride-ds

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

1

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>
  );
}
2

Add to Root Layout

Import and use in your Next.js layout:
src/app/layout.tsx
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>
  );
}
3

Import Styles

Add Stride styles to your layout or globals.css:
import "stride-ds/styles";
Or in your CSS:
src/app/globals.css
@import "stride-ds/styles";

Server vs Client Components

Understanding the Distinction

Server Components run on the server only:
app/page.tsx
// ✅ 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

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:
app/dashboard/page.tsx
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:
app/dashboard/error.tsx
"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

src/lib/ssr-utils.tsx
import { useIsSSR } from 'react-aria';

export const useSSRSafeId = () => {
  const isSSR = useIsSSR();
  return isSSR;
};

ClientOnly Component

Render content only on the client:
src/lib/ssr-utils.tsx
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:
src/lib/ssr-utils.tsx
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>
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>
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:
app/contact/page.tsx
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>
  );
}

Performance Optimization

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:
  1. Is SSRProvider wrapping your app?
  2. Are you using Date.now() or Math.random()?
  3. 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

Build docs developers (and LLMs) love