Users notice loading times. They notice them even more when nothing on screen tells them something is happening. A page that freezes for 2 seconds with no feedback feels broken. The same page with a loading spinner feels like it is working. And a page with skeleton loaders showing the shape of incoming content feels fast, even if the actual load time is identical.
Loading indicators are not decoration. They are a UX pattern that directly affects whether users wait or leave. The good news is that creating them is pure CSS with zero JavaScript required. Generators do the animation math for you, and the result is a few lines of CSS you can drop into any project.
Spinners vs Skeleton Loaders: When to Use Each
Spinners are good for: - Short loading times (under 3 seconds) - Operations where the layout is not predictable (search results, dynamic content) - Form submissions and button actions (the user triggered something and wants confirmation) - Full-page initial loads
Skeleton loaders are better for: - Content that has a predictable layout (lists, cards, profiles, feeds) - Longer loading times (3+ seconds) - Situations where you want the page to feel responsive immediately - Progressive loading (content that arrives in chunks)
The Loading Spinner Generator creates customizable CSS spinners: dots, circles, bars, and pulse animations. The Skeleton Loader Generator creates placeholder shapes that match your content layout: rectangles for text lines, circles for avatars, and blocks for images.
A practical rule of thumb: if the user can see where the content will appear, use a skeleton loader. If the content location is unknown or the entire page is loading, use a spinner.

Building a CSS Spinner from Scratch
The simplest CSS spinner is a border with one colored side that rotates:
`css
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
`
This creates a clean, circular spinner with one blue segment on a gray ring. The 0.8s duration controls speed. Faster feels urgent, slower feels relaxed. Between 0.6 and 1.2 seconds feels natural for most interfaces.
Dot spinners use multiple elements with staggered animations:
`css
.dots {
display: flex;
gap: 6px;
}
.dots span {
width: 10px;
height: 10px;
background: #3b82f6;
border-radius: 50%;
animation: bounce 1.4s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: 0.2s; }
.dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
`
The Animation Delay Calculator helps you calculate evenly spaced animation delays when you have more than 3 elements. Getting the timing right manually is tedious, especially with 5 or 8 dots.
The simplest CSS spinner is a border with one colored side that rotates: ```css .spinner { width: 40px; height: 40px; border: 4px solid #e5e7eb; border-top-color: #3b82f6; border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } ``` This creates a clean, circular spinner with one blue segment on a gray ring.
Building a Skeleton Loader
A skeleton loader mimics the shape of the content it replaces. For a card with an image, title, and description, the skeleton would be:
`css
.skeleton-card {
padding: 16px;
max-width: 320px;
}
.skeleton-image {
width: 100%;
height: 180px;
background: #e5e7eb;
border-radius: 8px;
}
.skeleton-title {
height: 20px;
width: 70%;
background: #e5e7eb;
border-radius: 4px;
margin-top: 12px;
}
.skeleton-text {
height: 14px;
width: 100%;
background: #e5e7eb;
border-radius: 4px;
margin-top: 8px;
}
`
The shimmer animation that makes skeleton loaders feel alive uses a gradient that slides across each element:
`css
.skeleton-image,
.skeleton-title,
.skeleton-text {
background: linear-gradient(
90deg,
#e5e7eb 25%,
#f3f4f6 50%,
#e5e7eb 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`
The shimmer creates a subtle light sweep across the skeleton, signaling that content is actively loading rather than just missing. This small detail significantly improves perceived performance.

Accessibility Considerations
Loading indicators need to be accessible to all users, including those who use screen readers or have motion sensitivity.
Screen readers: Add aria-label="Loading" and role="status" to spinner containers so screen readers announce the loading state. For skeleton loaders, use aria-hidden="true" on the placeholder elements and add a visually hidden text element that says "Loading content."
Reduced motion: Some users have the prefers-reduced-motion media query enabled because animations cause discomfort or nausea. Respect this setting:
`css
@media (prefers-reduced-motion: reduce) {
.spinner,
.skeleton-image,
.skeleton-title,
.skeleton-text {
animation: none;
}
}
`
This stops all animations for users who prefer reduced motion. The skeleton shapes still appear (which is fine, they are static placeholders), and the spinner becomes a static ring (less informative but non-harmful).
Color contrast: Skeleton loaders use gray rectangles on a white background. Make sure the gray is visible enough to be perceived as a placeholder, but not so dark that it looks like actual content. The standard #e5e7eb on white has sufficient contrast for most situations.
Loading indicators need to be accessible to all users, including those who use screen readers or have motion sensitivity.
FAQ
Should I show a spinner or a skeleton loader for API calls?
It depends on the response time and the content type. For API calls under 2 seconds that return a predictable layout (like a user profile or a list of items), use a skeleton loader. For calls that might take longer or return variable content, a spinner with a message ("Loading your data...") gives better feedback. For very fast operations (under 500ms), you might not need a loading indicator at all.
How do I prevent layout shift when content replaces the skeleton?
Make your skeleton elements the same dimensions as the real content. If your card images are 320x180 pixels, make the skeleton image placeholder exactly that size. If your text lines are 14px tall with 8px spacing, match that in the skeleton. This prevents content from jumping when the real data loads.
Are there performance concerns with CSS animations?
CSS animations using transform and opacity are GPU-accelerated and have negligible performance impact. Avoid animating properties like width, height, top, or left, which trigger layout recalculations. The spinner and shimmer animations shown above all use transform and background-position, which are efficient.
Can I use SVG for loading spinners instead of CSS?
Yes, and SVG spinners have some advantages: they scale perfectly at any size, they can have more complex shapes, and the animation is self-contained in the SVG file. The trade-off is that SVG animations are slightly more verbose to write. For simple spinners, CSS is more convenient. For branded or complex spinners, SVG is worth the extra effort.
JSON Explained: Formatting, Validating, and Converting for Developers
A comprehensive guide to JSON: syntax rules, common errors, formatting tools, JSON Schema validation, and converting between JSON and CSV.
Base64, URL Encoding & HTML Entities Explained
Encode and decode Base64, URLs, and HTML entities instantly. Learn when to use each format, with examples and free converter tools.
Regular Expressions for Beginners: A Practical Guide
Learn regular expression fundamentals, from basic syntax and character classes to practical patterns for matching emails, URLs, and phone numbers.
