Skip to main content

Getting Started

Get up and running with Spezi Web Design System in minutes.

Quick Start with Template

The fastest way to start is using our template application, which includes all the necessary dependencies, configuration, boilerplate code and examples.

# Clone the template repository
git clone https://github.com/StanfordSpezi/spezi-web-template-application.git my-app
cd my-app

# Install dependencies
npm install

# Start development server
npm run dev

The template includes:

  • Pre-configured tech stack
  • Spezi Web pre-configured
  • Examples components and pages
  • Routing and layout structure
  • Authentication patterns

View Template Repository →

Manual Installation

If you're adding Spezi to an existing project:

1. Install the Package

npm install @stanfordspezi/spezi-web-design-system

2. Install Tailwind

This package is built with Tailwind CSS. You'll need to configure Tailwind CSS to use Spezi styles. Read more about setting up Tailwind CSS in the Tailwind CSS Documentation.

Simplified Tailwind installation guide

Install Tailwind CSS and PostCSS:

npm install tailwindcss @tailwindcss/postcss postcss --save-dev

Create postcss.config.mjs file:

export default {
plugins: {
"@tailwindcss/postcss": {},
}
}

Create globals.css file:

@import "tailwindcss";

Ensure your globals.css is imported in your application. You can import it in the root file and add it to your head tag.

3. Configure Spezi

In the file where you're importing tailwindcss (globals.css from the previous step), replace it with the Spezi imports:

/* This file automatically imports Tailwind CSS, Spezi CSS variables and custom utilities. */
@import '@stanfordspezi/spezi-web-design-system/tailwind.css';
/* This file applies global base styles, like background color, selection colors or cursor pointers. Separated from main file in case you want to opt-out from it. */
@import '@stanfordspezi/spezi-web-design-system/base.css';
/* ⚠️ Important!
* This file is required for Tailwind to generate CSS for Spezi components.
* Ensure that this path points to the correct `node_modules` location.
* It's relative to your `globals.css` file.
* If globals is in the root directory, it's `../node_modules/...`.
*/
@source '../node_modules/@stanfordspezi/spezi-web-design-system/dist/**/*.js';

Ensure that your @source points to the correct node_modules location.

4. Setup SpeziProvider

SpeziProvider provides global values to your Spezi components.

It allows configuring:

  • router objects
  • theme
  • localization messages

Wrap your entire application with the SpeziProvider.

With Tanstack Router

import { SpeziProvider, SpeziContextRouter } from "@stanfordspezi/spezi-web-design-system";
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";

const routerProps: SpeziContextRouter = {
Link: ({ href, ...props }) => <Link to={href} {...props} />,
};

const Root = () => (
<SpeziProvider router={routerProps}>
<Outlet />
</SpeziProvider>
);

export const Route = createRootRoute({
component: Root,
});

With Next.js

"use client";
import Link from "next/link";
import { SpeziProvider, SpeziContextRouter } from "@stanfordspezi/spezi-web-design-system";

const routerProps: SpeziContextRouter = {
Link: ({ href, ...props }) => <Link href={href ?? "#"} {...props} />,
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<SpeziProvider router={routerProps}>{children}</SpeziProvider>
</body>
</html>
);
}

Basic Usage

Importing

By root

You can import everything from the root of the package:

import { Button, Input, Card } from '@stanfordspezi/spezi-web-design-system';

This approach guarantees simplicity.

By module

You can also import specific modules:

import { Button } from '@stanfordspezi/spezi-web-design-system/components/Button';
import { Input } from '@stanfordspezi/spezi-web-design-system/components/Input';
import { Card } from '@stanfordspezi/spezi-web-design-system/components/Card';

This approach might reduce bundle size, but it's more verbose. In Next.JS it might be necessary to import components individually if you want to use them on the server-side. Root includes all components, as well as those clients-only.

Using Components

Import and use components to compose your UI:

import {
Card,
CardHeader,
CardTitle,
Notification,
Badge,
Tooltip,
Button,
} from "@stanfordspezi/spezi-web-design-system";

const App = () => (
<main className="flex-center h-screen w-screen">
<Card className="min-w-lg">
<CardHeader>
<CardTitle>Notifications</CardTitle>
<Tooltip tooltip="You have 1 unread notification">
<Badge variant="destructiveLight" size="sm">
1
</Badge>
</Tooltip>
</CardHeader>
<Notification
title="Appointment scheduled"
message="Your appointment scheduled at 10:00 AM"
time={new Date()}
isRead={false}
actions={
<Button size="xs" variant="outline">
Reschedule
</Button>
}
/>
<Notification
title="New message"
message="You have a new message from Dr. Smith"
time={new Date(Date.now() - 1000 * 60 * 60 * 24 * 12)}
isRead={true}
/>
<footer className="flex gap-2 p-5">
<Button>Mark all as read</Button>
<Button variant="secondary">See more</Button>
</footer>
</Card>
</main>
);

Form Handling

Use the built-in form utilities with React Hook Form and Zod:

import {
Button,
useForm,
Field,
Input,
FormError,
sleep,
} from "@stanfordspezi/spezi-web-design-system";
import { z } from "zod";

const formSchema = z.object({
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
});

// Emulated login call to backend
const login = async ({
email,
password,
}: {
email: string;
password: string;
}) => {
await sleep(1000);
if (Math.random() > 0.5) {
// automatically surfaced through `form.formError`
throw new Error("Invalid credentials");
} else {
// login successful
}
};

const LoginForm = () => {
const form = useForm({
formSchema,
defaultValues: {
email: "",
password: "",
},
});

const onSubmit = form.handleSubmit(async (form) => {
await login(form);
// proceed with a success path
alert("Login worked, redirect...");
});

return (
<form onSubmit={onSubmit}>
<pre className="mb-4 text-xs">
{JSON.stringify(form.watch(), null, 2)}
</pre>
<FormError formError={form.formError} />
{/* Form errors, labels, name, aria attribute are automatically appended using Field component */}
<Field
control={form.control}
name="email"
label="Email"
render={({ field }) => <Input {...field} />}
/>
<Field
control={form.control}
name="password"
label="Password"
render={({ field }) => <Input {...field} type="password" />}
/>
<Button type="submit" isPending={form.formState.isSubmitting}>
Login
</Button>
</form>
);
};

const App = () => (
<main className="flex-center h-screen">
<div className="w-lg">
<LoginForm />
</div>
</main>
);

Using Molecules

Molecules are higher-level components that provide complex, yet common UI patterns:

import { DashboardLayout } from '@stanfordspezi/spezi-web-design-system/molecules/DashboardLayout';
import { Notifications } from '@stanfordspezi/spezi-web-design-system/molecules/Notifications';

function Dashboard() {
return (
<DashboardLayout
navigation={[
{ label: 'Home', href: '/' },
{ label: 'Settings', href: '/settings' },
]}
>
<Notifications />
<h1>Dashboard Content</h1>
</DashboardLayout>
);
}

Utilities

Use helper utilities throughout your app:

import {
cn,
formatDate,
useOpenState,
} from "@stanfordspezi/spezi-web-design-system";

const MyComponent = () => {
const dialog = useOpenState();

return (
<div className={cn("p-4", dialog.isOpen && "bg-destructive")}>
<p>Date: {formatDate(new Date())}</p>
<button onClick={dialog.open}>Open Dialog</button>
{dialog.isOpen && <div>Dialog</div>}
</div>
);
};

Next Steps

Production Examples

See real-world implementations:

Need Help?