Spinner
A loading spinner component that indicates an ongoing process or data fetching operation.
Usage
Basic Spinner
import { Spinner } from "@prisma-docs/eclipse";
export function MyComponent() {
return <Spinner />;
}Live Example:
Custom Size
import { Spinner } from "@prisma-docs/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:
Error State
import { Spinner } from "@prisma-docs/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:
Error State with Message
import { Spinner } from "@prisma-docs/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:
Failed to load. Retrying...
In Button
import { Spinner } from "@prisma-docs/eclipse";
import { Button } from "@prisma-docs/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:
Centered in Container
import { Spinner } from "@prisma-docs/eclipse";
export function CenteredSpinner() {
return (
<div className="flex items-center justify-center h-32">
<Spinner className="size-8" />
</div>
);
}Live Example:
With Text
import { Spinner } from "@prisma-docs/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:
Loading your data...
Full Page Loading
import { Spinner } from "@prisma-docs/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>
);
}Spinner Props
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")- All standard SVG attributes are supported
Features
- ✅ Built on Lucide React's Loader2 icon
- ✅ Smooth rotation animation
- ✅ Accessible with proper ARIA attributes
- ✅ Customizable size via Tailwind classes
- ✅ Built-in error state for failure scenarios
- ✅ Lightweight and performant
- ✅ Works in any context (buttons, cards, pages)
- ✅ Eclipse design system styling
Best Practices
- Use appropriate size for the context (small in buttons, larger for page loading)
- Always pair with descriptive text or
aria-labelfor accessibility - Consider placement - centered for full-page, inline for components
- Show spinners only when necessary - avoid overuse
- Disable interactive elements while loading (buttons, forms, etc.)
- Provide feedback when loading completes
- Consider showing skeleton loaders for better UX on longer operations
- Use appropriate z-index for overlay spinners
- Ensure sufficient contrast with background
Common Patterns
Async Button
import { Spinner } from "@prisma-docs/eclipse";
import { Button } from "@prisma-docs/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-docs/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>{/* render data */}</div>;
}Error State with Retry
import { Spinner } from "@prisma-docs/eclipse";
import { Button } from "@prisma-docs/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>{/* render data */}</div>;
}Inline Loading
import { Spinner } from "@prisma-docs/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-docs/eclipse";
import { Card, CardContent } from "@prisma-docs/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-docs/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-docs/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>
);
}Accessibility
- Has
role="status"to announce loading state to screen readers - Includes
aria-label="Loading"by default for screen reader users - Should be paired with visible text when possible
- Disable interactive elements while loading
- Announce completion of loading operations
- Use proper color contrast
- Consider
aria-liveregions for dynamic content updates - Provide meaningful loading messages
Size Guidelines
- Extra Small (size-4): Inline text, small buttons, badges
- Small (size-6): Regular buttons, form inputs, list items
- Medium (size-8): Cards, sections, modal content
- Large (size-12): Full page loading, main content areas
- Custom: Use arbitrary values like
className="w-16 h-16"for specific needs
Performance
The Spinner component is optimized for performance:
- Uses CSS animations (GPU-accelerated)
- Minimal re-renders
- No JavaScript animation loops
- Lightweight SVG icon
- Can be conditionally rendered without performance impact
Integration Examples
With React Query
import { Spinner } from "@prisma-docs/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>{/* render data */}</div>;
}With Next.js Loading UI
import { Spinner } from "@prisma-docs/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-docs/eclipse";
import { Button } from "@prisma-docs/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}>
{/* form fields */}
<Button type="submit" disabled={submitting}>
{submitting ? (
<>
<Spinner className="mr-2" />
Sending...
</>
) : (
'Send Message'
)}
</Button>
</form>
);
}Styling
The Spinner uses Eclipse design system principles:
- Animation: Smooth 1s linear rotation
- Default Size: 1rem (size-4)
- Accessibility: Proper ARIA attributes
- Customization: Fully customizable via className
Override styles as needed:
<Spinner className="size-8 text-blue-500 animate-spin-slow" />