apps/eclipse/content/design-system/components/spinner.mdx
import { Spinner } from "@prisma/eclipse"; import { Button } from "@prisma/eclipse";
Basic Spinner
import { Spinner } from "@prisma/eclipse";
export function MyComponent() {
return <Spinner />;
}
Live Example:
<div className="flex items-center gap-4 my-4"> <Spinner /> </div>Custom Size
import { Spinner } from "@prisma/eclipse";
export function CustomSizeSpinner() {
return (
<div className="flex items-center gap-4">
<Spinner className="size-4" />
<Spinner className="size-6" />
<Spinner className="size-8" />
<Spinner className="size-12" />
</div>
);
}
Live Example:
<div className="flex items-center gap-4 my-4"> <Spinner className="size-4" /> <Spinner className="size-6" /> <Spinner className="size-8" /> <Spinner className="size-12" /> </div>Error State
import { Spinner } from "@prisma/eclipse";
export function ErrorSpinner() {
return (
<div className="flex items-center gap-4">
<Spinner error />
<Spinner error className="size-6" />
<Spinner error className="size-8" />
</div>
);
}
Live Example:
<div className="flex items-center gap-4 my-4"> <Spinner error /> <Spinner error className="size-6" /> <Spinner error className="size-8" /> </div>Error State with Message
import { Spinner } from "@prisma/eclipse";
export function ErrorSpinnerWithMessage() {
return (
<div className="flex items-center gap-2">
<Spinner error className="size-5" />
<span className="text-sm text-foreground-error">
Failed to load. Retrying...
</span>
</div>
);
}
Live Example:
<div className="flex items-center gap-2 my-4"> <Spinner error className="size-5" /> <span className="text-sm text-foreground-error"> Failed to load. Retrying... </span> </div>In Button
import { Spinner } from "@prisma/eclipse";
import { Button } from "@prisma/eclipse";
export function ButtonWithSpinner() {
return (
<Button disabled className="flex items-center gap-0.75">
<Spinner className="mr-2" />
<span></span>Loading...</span>
</Button>
);
}
Live Example:
<div className="flex items-center gap-4 my-4"> <Button disabled className="flex items-center gap-0.75"> <Spinner className="mr-2" /> <span>Loading...</span> </Button> <Button variant="ppg" disabled className="flex items-center gap-0.75"> <Spinner className="mr-2" /> <span>Processing</span> </Button> </div>Centered in Container
import { Spinner } from "@prisma/eclipse";
export function CenteredSpinner() {
return (
<div className="flex items-center justify-center h-32">
<Spinner className="size-8" />
</div>
);
}
Live Example:
<div className="flex items-center justify-center h-32 border rounded-lg my-4"> <Spinner className="size-8" /> </div>With Text
import { Spinner } from "@prisma/eclipse";
export function SpinnerWithText() {
return (
<div className="flex items-center gap-2">
<Spinner />
<span className="text-sm text-foreground-neutral-weak">
Loading your data...
</span>
</div>
);
}
Live Example:
<div className="flex items-center gap-2 my-4"> <Spinner /> <span className="text-sm text-foreground-neutral-weak"> Loading your data... </span> </div>Full Page Loading
import { Spinner } from "@prisma/eclipse";
export function FullPageLoader() {
return (
<div className="fixed inset-0 flex items-center justify-center bg-background-default/80 backdrop-blur-sm">
<div className="flex flex-col items-center gap-4">
<Spinner className="size-12" />
<p className="text-sm text-foreground-neutral-weak">
Loading application...
</p>
</div>
</div>
);
}
The Spinner component accepts all standard SVG element props:
error - Display spinner in error state with different icon (boolean, optional)className - Additional CSS classes (optional)role - ARIA role (default: "status")aria-label - ARIA label (default: "Loading")aria-label for accessibilityAsync Button
import { Spinner } from "@prisma/eclipse";
import { Button } from "@prisma/eclipse";
import { useState } from "react";
export function AsyncButton() {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
try {
await fetch('/api/data');
} finally {
setLoading(false);
}
};
return (
<Button onClick={handleClick} disabled={loading}>
{loading && <Spinner className="mr-2" />}
{loading ? 'Submitting...' : 'Submit'}
</Button>
);
}
Loading State with Error Handling
import { Spinner } from "@prisma/eclipse";
export function DataDisplay({ data, loading, error }) {
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<Spinner className="size-8" />
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center gap-2 text-foreground-error">
<Spinner error className="size-6" />
<span>Error loading data</span>
</div>
);
}
return <div></div>;
}
Error State with Retry
import { Spinner } from "@prisma/eclipse";
import { Button } from "@prisma/eclipse";
import { useState } from "react";
export function DataFetcherWithRetry() {
const [status, setStatus] = useState<'idle' | 'loading' | 'error' | 'success'>('idle');
const handleFetch = async () => {
setStatus('loading');
try {
await fetch('/api/data');
setStatus('success');
} catch (error) {
setStatus('error');
}
};
if (status === 'loading') {
return (
<div className="flex items-center gap-2">
<Spinner />
<span>Loading...</span>
</div>
);
}
if (status === 'error') {
return (
<div className="flex items-center gap-2">
<Spinner error />
<span className="text-foreground-error">Failed to load.</span>
<Button size="lg" onClick={handleFetch}>Retry</Button>
</div>
);
}
return <div></div>;
}
Inline Loading
import { Spinner } from "@prisma/eclipse";
export function InlineLoader({ loading, children }) {
if (loading) {
return (
<span className="inline-flex items-center gap-2">
<Spinner className="size-4" />
<span className="text-sm text-foreground-neutral-weak">Loading...</span>
</span>
);
}
return children;
}
Card Loading
import { Spinner } from "@prisma/eclipse";
import { Card, CardContent } from "@prisma/eclipse";
export function LoadingCard() {
return (
<Card>
<CardContent className="flex items-center justify-center h-48">
<div className="flex flex-col items-center gap-3">
<Spinner className="size-8" />
<p className="text-sm text-foreground-neutral-weak">
Loading content...
</p>
</div>
</CardContent>
</Card>
);
}
Overlay Loading
import { Spinner } from "@prisma/eclipse";
export function LoadingOverlay({ loading, children }) {
return (
<div className="relative">
{children}
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-background-default/80 backdrop-blur-sm rounded-lg">
<Spinner className="size-8" />
</div>
)}
</div>
);
}
Conditional Loading
import { Spinner } from "@prisma/eclipse";
export function ConditionalSpinner({ isLoading, message = "Loading..." }) {
if (!isLoading) return null;
return (
<div className="flex items-center gap-2 p-4">
<Spinner />
<span className="text-sm">{message}</span>
</div>
);
}
role="status" to announce loading state to screen readersaria-label="Loading" by default for screen reader usersaria-live regions for dynamic content updatesclassName="w-16 h-16" for specific needsThe Spinner component is optimized for performance:
With React Query
import { Spinner } from "@prisma/eclipse";
import { useQuery } from "@tanstack/react-query";
export function DataFetcher() {
const { data, isLoading, error } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
});
if (isLoading) {
return (
<div className="flex justify-center p-8">
<Spinner className="size-8" />
</div>
);
}
if (error) return <div>Error: {error.message}</div>;
return <div></div>;
}
With Next.js Loading UI
import { Spinner } from "@prisma/eclipse";
// app/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="flex flex-col items-center gap-4">
<Spinner className="size-12" />
<p className="text-foreground-neutral-weak">Loading page...</p>
</div>
</div>
);
}
With Form Submission
import { Spinner } from "@prisma/eclipse";
import { Button } from "@prisma/eclipse";
import { useState } from "react";
export function ContactForm() {
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setSubmitting(true);
try {
await submitForm();
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<Button type="submit" disabled={submitting}>
{submitting ? (
<>
<Spinner className="mr-2" />
Sending...
</>
) : (
'Send Message'
)}
</Button>
</form>
);
}
The Spinner uses Eclipse design system principles:
Override styles as needed:
<Spinner className="size-8 text-blue-500 animate-spin-slow" />