Button Library · Interactive
Loading
Click: a spinner runs while the work runs.
States
Default
Hover
Active
Focus
Code
loading.html
<button class="btn-loading" type="button" data-busy="Saving…"> <span>Save changes</span> </button>
loading.css
.btn-loading {
--btn-bg: #7c5cff;
--btn-fg: #ffffff;
--btn-font: inherit;
--btn-size: 15px;
--btn-weight: 600;
--btn-px: 22px;
--btn-py: 11px;
--btn-w: auto;
--btn-h: auto;
--btn-bw: 0px;
--btn-line: #8b6cff;
--btn-radius: 10px;
--btn-shadow: none;
--btn-speed: 180ms;
}
.btn-loading {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5em;
box-sizing: border-box;
width: var(--btn-w);
height: var(--btn-h);
padding: var(--btn-py) var(--btn-px);
font-family: var(--btn-font);
font-size: var(--btn-size);
font-weight: var(--btn-weight);
line-height: 1.2;
color: var(--btn-fg);
background: var(--btn-bg);
border: var(--btn-bw) solid var(--btn-line);
border-radius: var(--btn-radius);
box-shadow: var(--btn-shadow);
cursor: pointer;
text-decoration: none;
white-space: nowrap;
transition: all var(--btn-speed) ease;
}
.btn-loading:focus-visible {
outline: 2px solid var(--btn-bg);
outline-offset: 2px;
}
.btn-loading:disabled {
opacity: 0.5;
pointer-events: none;
}
.btn-loading__icon {
width: 1.15em;
height: 1.15em;
flex-shrink: 0;
}
.btn-loading[data-state=busy]::before {
content: "";
width: 1em;
height: 1em;
box-sizing: border-box;
border-style: solid;
border-width: 2px;
border-color: currentColor transparent currentColor currentColor;
border-radius: 50%;
animation: btn-spin 0.7s linear infinite;
}
.btn-loading[data-state=busy] {
opacity: 0.8;
cursor: progress;
}
@keyframes btn-spin {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.btn-loading,
.btn-loading::before {
animation: none !important;
transition: none !important;
}
}
loading.js
document.querySelectorAll(".btn-loading").forEach((el) => {
const label = el.querySelector("span");
const idle = label.textContent;
const { busy, done } = el.dataset;
let timer = 0;
const show = (state, text, next, wait) => {
el.dataset.state = state;
label.textContent = text;
timer = window.setTimeout(next, wait);
};
const reset = () => {
delete el.dataset.state;
label.textContent = idle;
};
const finish = () => (done ? show("done", done, reset, 1800) : reset());
const start = () => {
if (el.dataset.state) return;
// Demo timer: run your real work here and call finish() when it ends.
if (busy) show("busy", busy, finish, 1500);
else finish();
};
el.addEventListener("click", start);
return () => {
clearTimeout(timer);
el.removeEventListener("click", start);
};
});
loading.tsx
import { useEffect, useRef } from "react";
import type React from "react";
import "./loading.css"; // the CSS tab
function attach(el: HTMLElement) {
const label = el.querySelector("span") as HTMLElement;
const idle = label.textContent;
const { busy, done } = el.dataset;
let timer = 0;
const show = (state: string, text: string, next: () => void, wait: number) => {
el.dataset.state = state;
label.textContent = text;
timer = window.setTimeout(next, wait);
};
const reset = () => {
delete el.dataset.state;
label.textContent = idle;
};
const finish = () => (done ? show("done", done, reset, 1800) : reset());
const start = () => {
if (el.dataset.state) return;
// Demo timer: run your real work here and call finish() when it ends.
if (busy) show("busy", busy, finish, 1500);
else finish();
};
el.addEventListener("click", start);
return () => {
clearTimeout(timer);
el.removeEventListener("click", start);
};
}
export function BtnLoading({ children = "Save changes", ...props }: React.ComponentProps<"button">) {
const ref = useRef<HTMLButtonElement>(null);
useEffect(() => attach(ref.current!), []);
return (
<button className="btn-loading" type="button" data-busy="Saving…" ref={ref} {...props}>
<span>{children}</span>
</button>
);
}
loading.html
<button class="btn-loading relative inline-flex items-center justify-center gap-[0.5em] [box-sizing:border-box] [padding:11px_22px] [font-family:inherit] text-[15px] font-[600] [line-height:1.2] text-[#ffffff] bg-[#7c5cff] [border:0px_solid_#8b6cff] rounded-[10px] [box-shadow:none] cursor-pointer no-underline whitespace-nowrap [transition:all_180ms_ease] focus-visible:[outline:2px_solid_#7c5cff] focus-visible:[outline-offset:2px] disabled:[opacity:0.5] disabled:[pointer-events:none] [&[data-state=busy]]:before:content-[''] [&[data-state=busy]]:before:w-[1em] [&[data-state=busy]]:before:h-[1em] [&[data-state=busy]]:before:[box-sizing:border-box] [&[data-state=busy]]:before:[border-style:solid] [&[data-state=busy]]:before:[border-width:2px] [&[data-state=busy]]:before:[border-color:currentColor_transparent_currentColor_currentColor] [&[data-state=busy]]:before:rounded-[50%] [&[data-state=busy]]:before:[animation:btn-spin_0.7s_linear_infinite] [&[data-state=busy]]:[opacity:0.8] [&[data-state=busy]]:[cursor:progress] motion-reduce:[transition:none!important] motion-reduce:before:[animation:none!important]" type="button" data-busy="Saving…">
<span>Save changes</span>
</button>
<!-- A class cannot hold keyframes: keep this in your CSS. -->
<style>
@keyframes btn-spin {
to { transform: rotate(360deg); }
}
</style>
<!-- Needs the script from the JS tab. -->