Back
Learn how to build a button that smoothly transitions to a loading state with a spinner animation to provide user feedback during asynchronous operations.
<div class="button-container"> <button class="loading-button" onclick="this.classList.toggle('is-loading')"> <span class="button-text">Submit</span> <span class="button-spinner"></span> </button> </div>Note: The onclick handler is included for demo purposes. In a real application, you would typically add the is-loading class via JavaScript when performing an asynchronous operation.
.button-container { display: flex; justify-content: center; align-items: center; }Next, we define the loading button with CSS variables for easy customization:
.loading-button { --button-bg: #4f46e5; --button-hover-bg: #4338ca; --button-active-bg: #3730a3; --button-text: white; --button-spinner-color: white; --button-padding-x: 24px; --button-padding-y: 12px; --button-font-size: 16px; --button-radius: 6px; --button-spinner-size: 20px; --button-min-width: 120px; position: relative; font-family: system-ui, -apple-system, sans-serif; font-weight: 500; font-size: var(--button-font-size); color: var(--button-text); background-color: var(--button-bg); border: none; border-radius: var(--button-radius); padding: var(--button-padding-y) var(--button-padding-x); min-width: var(--button-min-width); cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease; overflow: hidden; } .loading-button:hover { background-color: var(--button-hover-bg); } .loading-button:active { background-color: var(--button-active-bg); transform: translateY(1px); } /* Focus state for accessibility */ .loading-button:focus-visible { outline: 2px solid rgba(79, 70, 229, 0.5); outline-offset: 2px; }The key properties here: - We set a min-width to ensure the button doesn't change size when the text is hidden - We use position: relative to allow absolute positioning of the spinner - We add transitions for smooth state changes - We include hover, active, and focus states for interactive feedback Next, we style the text and spinner elements:
/* Text and spinner container */ .button-text, .button-spinner { display: inline-flex; align-items: center; justify-content: center; transition: opacity 0.2s ease, transform 0.2s ease; } /* Spinner styling */ .button-spinner { position: absolute; top: 50%; left: 50%; width: var(--button-spinner-size); height: var(--button-spinner-size); margin-top: calc(var(--button-spinner-size) / -2); margin-left: calc(var(--button-spinner-size) / -2); border: 2px solid var(--button-spinner-color); border-top-color: transparent; border-radius: 50%; opacity: 0; transform: scale(0.5); animation: spinner 0.8s linear infinite; }The button-spinner is: - Absolutely positioned in the center of the button - Initially hidden with opacity: 0 and scaled down - Styled as a circular border with a transparent segment - Set up with a rotation animation that will play when visible Now we define the loading state styles:
/* Loading state changes */ .loading-button.is-loading { cursor: wait; } .loading-button.is-loading .button-text { opacity: 0; transform: translateY(8px); } .loading-button.is-loading .button-spinner { opacity: 1; transform: scale(1); } /* Spinner animation */ @keyframes spinner { to { transform: rotate(360deg); } }When the loading state is active: - The cursor changes to indicate the waiting state - The text fades out and moves down slightly - The spinner fades in and scales up to its full size - The spinner animation makes it rotate continuously Finally, we add variations for different colors and sizes:
/* Different color variants */ .loading-button.primary { --button-bg: #3b82f6; --button-hover-bg: #2563eb; --button-active-bg: #1d4ed8; } .loading-button.success { --button-bg: #10b981; --button-hover-bg: #059669; --button-active-bg: #047857; } .loading-button.danger { --button-bg: #ef4444; --button-hover-bg: #dc2626; --button-active-bg: #b91c1c; } /* Different size variants */ .loading-button.small { --button-padding-x: 16px; --button-padding-y: 8px; --button-font-size: 14px; --button-spinner-size: 16px; --button-min-width: 100px; } .loading-button.large { --button-padding-x: 32px; --button-padding-y: 16px; --button-font-size: 18px; --button-spinner-size: 24px; --button-min-width: 140px; }These variations allow for: - Different color schemes to match UI needs (primary, success, danger) - Different size options to fit various layout requirements This loading button offers several advantages: - User feedback: Clearly communicates when an asynchronous operation is in progress. - Prevent multiple submissions: Visual feedback discourages users from clicking multiple times. - Consistent dimensions: The button maintains its size during state transitions to prevent layout shifts. - Smooth transitions: Animations make the state change feel polished and intentional. - Accessible design: Proper focus states and cursor changes improve usability. This button style is perfect for form submissions, data processing operations, API calls, or any scenario where you need to indicate that an action is being processed. The spinner provides clear feedback that the system is working, improving the perceived performance of your application.
<div class="button-container"> <button class="loading-button" onclick="this.classList.toggle('is-loading')"> <span class="button-text">Submit</span> <span class="button-spinner"></span> </button> </div> <style> .button-container { display: flex; justify-content: center; align-items: center; } .loading-button { --button-bg: #4f46e5; --button-hover-bg: #4338ca; --button-active-bg: #3730a3; --button-text: white; --button-spinner-color: white; --button-padding-x: 24px; --button-padding-y: 12px; --button-font-size: 16px; --button-radius: 6px; --button-spinner-size: 20px; --button-min-width: 120px; position: relative; font-family: system-ui, -apple-system, sans-serif; font-weight: 500; font-size: var(--button-font-size); color: var(--button-text); background-color: var(--button-bg); border: none; border-radius: var(--button-radius); padding: var(--button-padding-y) var(--button-padding-x); min-width: var(--button-min-width); cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease; overflow: hidden; } .loading-button:hover { background-color: var(--button-hover-bg); } .loading-button:active { background-color: var(--button-active-bg); transform: translateY(1px); } /* Focus state for accessibility */ .loading-button:focus-visible { outline: 2px solid rgba(79, 70, 229, 0.5); outline-offset: 2px; } /* Text and spinner container */ .button-text, .button-spinner { display: inline-flex; align-items: center; justify-content: center; transition: opacity 0.2s ease, transform 0.2s ease; } /* Spinner styling */ .button-spinner { position: absolute; top: 50%; left: 50%; width: var(--button-spinner-size); height: var(--button-spinner-size); margin-top: calc(var(--button-spinner-size) / -2); margin-left: calc(var(--button-spinner-size) / -2); border: 2px solid var(--button-spinner-color); border-top-color: transparent; border-radius: 50%; opacity: 0; transform: scale(0.5); animation: spinner 0.8s linear infinite; } /* Loading state changes */ .loading-button.is-loading { cursor: wait; } .loading-button.is-loading .button-text { opacity: 0; transform: translateY(8px); } .loading-button.is-loading .button-spinner { opacity: 1; transform: scale(1); } /* Spinner animation */ @keyframes spinner { to { transform: rotate(360deg); } } /* Different color variants */ .loading-button.primary { --button-bg: #3b82f6; --button-hover-bg: #2563eb; --button-active-bg: #1d4ed8; } .loading-button.success { --button-bg: #10b981; --button-hover-bg: #059669; --button-active-bg: #047857; } .loading-button.danger { --button-bg: #ef4444; --button-hover-bg: #dc2626; --button-active-bg: #b91c1c; } /* Different size variants */ .loading-button.small { --button-padding-x: 16px; --button-padding-y: 8px; --button-font-size: 14px; --button-spinner-size: 16px; --button-min-width: 100px; } .loading-button.large { --button-padding-x: 32px; --button-padding-y: 16px; --button-font-size: 18px; --button-spinner-size: 24px; --button-min-width: 140px; } </style>Thank you for reading this article.
Comments
No comments yet. Be the first to comment!