Spinner
Loading spinner component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring smooth animations, multiple variants, and accessibility features.
Powered by
'use client';import { Spinner } from '@/components/ui/shadcn-io/spinner';const Example = () => <Spinner />;export default Example;
import { LoaderCircleIcon, LoaderIcon, LoaderPinwheelIcon, type LucideProps,} from 'lucide-react';import { cn } from '@/lib/utils';type SpinnerVariantProps = Omit<SpinnerProps, 'variant'>;const Default = ({ className, ...props }: SpinnerVariantProps) => ( <LoaderIcon className={cn('animate-spin', className)} {...props} />);const Circle = ({ className, ...props }: SpinnerVariantProps) => ( <LoaderCircleIcon className={cn('animate-spin', className)} {...props} />);const Pinwheel = ({ className, ...props }: SpinnerVariantProps) => ( <LoaderPinwheelIcon className={cn('animate-spin', className)} {...props} />);const CircleFilled = ({ className, size = 24, ...props}: SpinnerVariantProps) => ( <div className="relative" style={{ width: size, height: size }}> <div className="absolute inset-0 rotate-180"> <LoaderCircleIcon className={cn('animate-spin', className, 'text-foreground opacity-20')} size={size} {...props} /> </div> <LoaderCircleIcon className={cn('relative animate-spin', className)} size={size} {...props} /> </div>);const Ellipsis = ({ size = 24, ...props }: SpinnerVariantProps) => { return ( <svg height={size} viewBox="0 0 24 24" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <circle cx="4" cy="12" fill="currentColor" r="2"> <animate attributeName="cy" begin="0;ellipsis3.end+0.25s" calcMode="spline" dur="0.6s" id="ellipsis1" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" /> </circle> <circle cx="12" cy="12" fill="currentColor" r="2"> <animate attributeName="cy" begin="ellipsis1.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" /> </circle> <circle cx="20" cy="12" fill="currentColor" r="2"> <animate attributeName="cy" begin="ellipsis1.begin+0.2s" calcMode="spline" dur="0.6s" id="ellipsis3" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" /> </circle> </svg> );};const Ring = ({ size = 24, ...props }: SpinnerVariantProps) => ( <svg height={size} stroke="currentColor" viewBox="0 0 44 44" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <g fill="none" fillRule="evenodd" strokeWidth="2"> <circle cx="22" cy="22" r="1"> <animate attributeName="r" begin="0s" calcMode="spline" dur="1.8s" keySplines="0.165, 0.84, 0.44, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 20" /> <animate attributeName="stroke-opacity" begin="0s" calcMode="spline" dur="1.8s" keySplines="0.3, 0.61, 0.355, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 0" /> </circle> <circle cx="22" cy="22" r="1"> <animate attributeName="r" begin="-0.9s" calcMode="spline" dur="1.8s" keySplines="0.165, 0.84, 0.44, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 20" /> <animate attributeName="stroke-opacity" begin="-0.9s" calcMode="spline" dur="1.8s" keySplines="0.3, 0.61, 0.355, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 0" /> </circle> </g> </svg>);const Bars = ({ size = 24, ...props }: SpinnerVariantProps) => ( <svg height={size} viewBox="0 0 24 24" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <style>{` .spinner-bar { animation: spinner-bars-animation .8s linear infinite; animation-delay: -.8s; } .spinner-bars-2 { animation-delay: -.65s; } .spinner-bars-3 { animation-delay: -0.5s; } @keyframes spinner-bars-animation { 0% { y: 1px; height: 22px; } 93.75% { y: 5px; height: 14px; opacity: 0.2; } } `}</style> <rect className="spinner-bar" fill="currentColor" height="22" width="6" x="1" y="1" /> <rect className="spinner-bar spinner-bars-2" fill="currentColor" height="22" width="6" x="9" y="1" /> <rect className="spinner-bar spinner-bars-3" fill="currentColor" height="22" width="6" x="17" y="1" /> </svg>);const Infinite = ({ size = 24, ...props }: SpinnerVariantProps) => ( <svg height={size} preserveAspectRatio="xMidYMid" viewBox="0 0 100 100" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <path d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z" fill="none" stroke="currentColor" strokeDasharray="205.271142578125 51.317785644531256" strokeLinecap="round" strokeWidth="10" style={{ transform: 'scale(0.8)', transformOrigin: '50px 50px', }} > <animate attributeName="stroke-dashoffset" dur="2s" keyTimes="0;1" repeatCount="indefinite" values="0;256.58892822265625" /> </path> </svg>);export type SpinnerProps = LucideProps & { variant?: | 'default' | 'circle' | 'pinwheel' | 'circle-filled' | 'ellipsis' | 'ring' | 'bars' | 'infinite';};export const Spinner = ({ variant, ...props }: SpinnerProps) => { switch (variant) { case 'circle': return <Circle {...props} />; case 'pinwheel': return <Pinwheel {...props} />; case 'circle-filled': return <CircleFilled {...props} />; case 'ellipsis': return <Ellipsis {...props} />; case 'ring': return <Ring {...props} />; case 'bars': return <Bars {...props} />; case 'infinite': return <Infinite {...props} />; default: return <Default {...props} />; }};
Installation
npx shadcn@latest add https://www.shadcn.io/registry/spinner.json
npx shadcn@latest add https://www.shadcn.io/registry/spinner.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/spinner.json
bunx shadcn@latest add https://www.shadcn.io/registry/spinner.json
Features
- Smooth animations - CSS-based loading indicators with 60fps performance using Tailwind CSS animations
- Multiple variants - Different spinner styles and sizes for various loading states using JavaScript
- Customizable appearance - Colors, sizes, and timing controls with TypeScript configuration options
- Accessibility features - Screen reader announcements and reduced motion support for React applications
- Performance optimized - Lightweight CSS animations without JavaScript overhead for Next.js projects
- Icon integration - Built with Lucide icons for consistent design using shadcn/ui theming
- Open source - Free loading spinner component with complete customization support
Examples
Variants
'use client';import { Spinner, type SpinnerProps } from '@/components/ui/shadcn-io/spinner';const variants: SpinnerProps['variant'][] = [ 'default', 'circle', 'pinwheel', 'circle-filled', 'ellipsis', 'ring', 'bars', 'infinite',];const Example = () => ( <div className="grid h-screen w-full grid-cols-4 items-center justify-center gap-8"> {variants.map((variant) => ( <div className="flex flex-col items-center justify-center gap-4" key={variant} > <Spinner key={variant} variant={variant} /> <span className="font-mono text-muted-foreground text-xs"> {variant} </span> </div> ))} </div>);export default Example;
import { LoaderCircleIcon, LoaderIcon, LoaderPinwheelIcon, type LucideProps,} from 'lucide-react';import { cn } from '@/lib/utils';type SpinnerVariantProps = Omit<SpinnerProps, 'variant'>;const Default = ({ className, ...props }: SpinnerVariantProps) => ( <LoaderIcon className={cn('animate-spin', className)} {...props} />);const Circle = ({ className, ...props }: SpinnerVariantProps) => ( <LoaderCircleIcon className={cn('animate-spin', className)} {...props} />);const Pinwheel = ({ className, ...props }: SpinnerVariantProps) => ( <LoaderPinwheelIcon className={cn('animate-spin', className)} {...props} />);const CircleFilled = ({ className, size = 24, ...props}: SpinnerVariantProps) => ( <div className="relative" style={{ width: size, height: size }}> <div className="absolute inset-0 rotate-180"> <LoaderCircleIcon className={cn('animate-spin', className, 'text-foreground opacity-20')} size={size} {...props} /> </div> <LoaderCircleIcon className={cn('relative animate-spin', className)} size={size} {...props} /> </div>);const Ellipsis = ({ size = 24, ...props }: SpinnerVariantProps) => { return ( <svg height={size} viewBox="0 0 24 24" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <circle cx="4" cy="12" fill="currentColor" r="2"> <animate attributeName="cy" begin="0;ellipsis3.end+0.25s" calcMode="spline" dur="0.6s" id="ellipsis1" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" /> </circle> <circle cx="12" cy="12" fill="currentColor" r="2"> <animate attributeName="cy" begin="ellipsis1.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" /> </circle> <circle cx="20" cy="12" fill="currentColor" r="2"> <animate attributeName="cy" begin="ellipsis1.begin+0.2s" calcMode="spline" dur="0.6s" id="ellipsis3" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" /> </circle> </svg> );};const Ring = ({ size = 24, ...props }: SpinnerVariantProps) => ( <svg height={size} stroke="currentColor" viewBox="0 0 44 44" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <g fill="none" fillRule="evenodd" strokeWidth="2"> <circle cx="22" cy="22" r="1"> <animate attributeName="r" begin="0s" calcMode="spline" dur="1.8s" keySplines="0.165, 0.84, 0.44, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 20" /> <animate attributeName="stroke-opacity" begin="0s" calcMode="spline" dur="1.8s" keySplines="0.3, 0.61, 0.355, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 0" /> </circle> <circle cx="22" cy="22" r="1"> <animate attributeName="r" begin="-0.9s" calcMode="spline" dur="1.8s" keySplines="0.165, 0.84, 0.44, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 20" /> <animate attributeName="stroke-opacity" begin="-0.9s" calcMode="spline" dur="1.8s" keySplines="0.3, 0.61, 0.355, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 0" /> </circle> </g> </svg>);const Bars = ({ size = 24, ...props }: SpinnerVariantProps) => ( <svg height={size} viewBox="0 0 24 24" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <style>{` .spinner-bar { animation: spinner-bars-animation .8s linear infinite; animation-delay: -.8s; } .spinner-bars-2 { animation-delay: -.65s; } .spinner-bars-3 { animation-delay: -0.5s; } @keyframes spinner-bars-animation { 0% { y: 1px; height: 22px; } 93.75% { y: 5px; height: 14px; opacity: 0.2; } } `}</style> <rect className="spinner-bar" fill="currentColor" height="22" width="6" x="1" y="1" /> <rect className="spinner-bar spinner-bars-2" fill="currentColor" height="22" width="6" x="9" y="1" /> <rect className="spinner-bar spinner-bars-3" fill="currentColor" height="22" width="6" x="17" y="1" /> </svg>);const Infinite = ({ size = 24, ...props }: SpinnerVariantProps) => ( <svg height={size} preserveAspectRatio="xMidYMid" viewBox="0 0 100 100" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <path d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z" fill="none" stroke="currentColor" strokeDasharray="205.271142578125 51.317785644531256" strokeLinecap="round" strokeWidth="10" style={{ transform: 'scale(0.8)', transformOrigin: '50px 50px', }} > <animate attributeName="stroke-dashoffset" dur="2s" keyTimes="0;1" repeatCount="indefinite" values="0;256.58892822265625" /> </path> </svg>);export type SpinnerProps = LucideProps & { variant?: | 'default' | 'circle' | 'pinwheel' | 'circle-filled' | 'ellipsis' | 'ring' | 'bars' | 'infinite';};export const Spinner = ({ variant, ...props }: SpinnerProps) => { switch (variant) { case 'circle': return <Circle {...props} />; case 'pinwheel': return <Pinwheel {...props} />; case 'circle-filled': return <CircleFilled {...props} />; case 'ellipsis': return <Ellipsis {...props} />; case 'ring': return <Ring {...props} />; case 'bars': return <Bars {...props} />; case 'infinite': return <Infinite {...props} />; default: return <Default {...props} />; }};
Customization
'use client';import { Spinner } from '@/components/ui/shadcn-io/spinner';const Example = () => <Spinner className="text-blue-500" size={64} />;export default Example;
import { LoaderCircleIcon, LoaderIcon, LoaderPinwheelIcon, type LucideProps,} from 'lucide-react';import { cn } from '@/lib/utils';type SpinnerVariantProps = Omit<SpinnerProps, 'variant'>;const Default = ({ className, ...props }: SpinnerVariantProps) => ( <LoaderIcon className={cn('animate-spin', className)} {...props} />);const Circle = ({ className, ...props }: SpinnerVariantProps) => ( <LoaderCircleIcon className={cn('animate-spin', className)} {...props} />);const Pinwheel = ({ className, ...props }: SpinnerVariantProps) => ( <LoaderPinwheelIcon className={cn('animate-spin', className)} {...props} />);const CircleFilled = ({ className, size = 24, ...props}: SpinnerVariantProps) => ( <div className="relative" style={{ width: size, height: size }}> <div className="absolute inset-0 rotate-180"> <LoaderCircleIcon className={cn('animate-spin', className, 'text-foreground opacity-20')} size={size} {...props} /> </div> <LoaderCircleIcon className={cn('relative animate-spin', className)} size={size} {...props} /> </div>);const Ellipsis = ({ size = 24, ...props }: SpinnerVariantProps) => { return ( <svg height={size} viewBox="0 0 24 24" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <circle cx="4" cy="12" fill="currentColor" r="2"> <animate attributeName="cy" begin="0;ellipsis3.end+0.25s" calcMode="spline" dur="0.6s" id="ellipsis1" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" /> </circle> <circle cx="12" cy="12" fill="currentColor" r="2"> <animate attributeName="cy" begin="ellipsis1.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" /> </circle> <circle cx="20" cy="12" fill="currentColor" r="2"> <animate attributeName="cy" begin="ellipsis1.begin+0.2s" calcMode="spline" dur="0.6s" id="ellipsis3" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" /> </circle> </svg> );};const Ring = ({ size = 24, ...props }: SpinnerVariantProps) => ( <svg height={size} stroke="currentColor" viewBox="0 0 44 44" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <g fill="none" fillRule="evenodd" strokeWidth="2"> <circle cx="22" cy="22" r="1"> <animate attributeName="r" begin="0s" calcMode="spline" dur="1.8s" keySplines="0.165, 0.84, 0.44, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 20" /> <animate attributeName="stroke-opacity" begin="0s" calcMode="spline" dur="1.8s" keySplines="0.3, 0.61, 0.355, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 0" /> </circle> <circle cx="22" cy="22" r="1"> <animate attributeName="r" begin="-0.9s" calcMode="spline" dur="1.8s" keySplines="0.165, 0.84, 0.44, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 20" /> <animate attributeName="stroke-opacity" begin="-0.9s" calcMode="spline" dur="1.8s" keySplines="0.3, 0.61, 0.355, 1" keyTimes="0; 1" repeatCount="indefinite" values="1; 0" /> </circle> </g> </svg>);const Bars = ({ size = 24, ...props }: SpinnerVariantProps) => ( <svg height={size} viewBox="0 0 24 24" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <style>{` .spinner-bar { animation: spinner-bars-animation .8s linear infinite; animation-delay: -.8s; } .spinner-bars-2 { animation-delay: -.65s; } .spinner-bars-3 { animation-delay: -0.5s; } @keyframes spinner-bars-animation { 0% { y: 1px; height: 22px; } 93.75% { y: 5px; height: 14px; opacity: 0.2; } } `}</style> <rect className="spinner-bar" fill="currentColor" height="22" width="6" x="1" y="1" /> <rect className="spinner-bar spinner-bars-2" fill="currentColor" height="22" width="6" x="9" y="1" /> <rect className="spinner-bar spinner-bars-3" fill="currentColor" height="22" width="6" x="17" y="1" /> </svg>);const Infinite = ({ size = 24, ...props }: SpinnerVariantProps) => ( <svg height={size} preserveAspectRatio="xMidYMid" viewBox="0 0 100 100" width={size} xmlns="http://www.w3.org/2000/svg" {...props} > <title>Loading...</title> <path d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z" fill="none" stroke="currentColor" strokeDasharray="205.271142578125 51.317785644531256" strokeLinecap="round" strokeWidth="10" style={{ transform: 'scale(0.8)', transformOrigin: '50px 50px', }} > <animate attributeName="stroke-dashoffset" dur="2s" keyTimes="0;1" repeatCount="indefinite" values="0;256.58892822265625" /> </path> </svg>);export type SpinnerProps = LucideProps & { variant?: | 'default' | 'circle' | 'pinwheel' | 'circle-filled' | 'ellipsis' | 'ring' | 'bars' | 'infinite';};export const Spinner = ({ variant, ...props }: SpinnerProps) => { switch (variant) { case 'circle': return <Circle {...props} />; case 'pinwheel': return <Pinwheel {...props} />; case 'circle-filled': return <CircleFilled {...props} />; case 'ellipsis': return <Ellipsis {...props} />; case 'ring': return <Ring {...props} />; case 'bars': return <Bars {...props} />; case 'infinite': return <Infinite {...props} />; default: return <Default {...props} />; }};
Use Cases
- Form submissions - Loading states for button and form processing feedback
- Data fetching - API call indicators and content loading placeholders
- File uploads - Progress indication for document and media uploads
- Page transitions - Route changes and component mounting states
Implementation
Built with CSS animations and Lucide icons. Works with loading states and form libraries. Supports size variants and color customization with Tailwind utilities.
Rating
Interactive star rating component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring keyboard navigation, hover effects, and form integration.
Stars Scrolling Wheel
Animated stars scrolling wheel with number counter and completion effects. Perfect for React applications requiring achievement displays with Next.js integration and TypeScript support.