Open demo

CSS code

HTML
<div class="demo-grid">
  <div class="cell"><b>Rest</b><button class="st" type="button">Save changes</button></div>
  <div class="cell"><b>Hover</b><button class="st is-hover" type="button">Save changes</button></div>
  <div class="cell"><b>Focus visible</b><button class="st is-focus" type="button">Save changes</button></div>
  <div class="cell"><b>Active</b><button class="st is-active" type="button">Save changes</button></div>
  <div class="cell"><b>Disabled</b><button class="st" type="button" disabled>Save changes</button></div>
  <div class="cell"><b>Toggled</b><button class="st" type="button" aria-pressed="true">Following</button></div>
  <div class="cell"><b>Busy</b><button class="st" type="button" aria-busy="true">Saving</button></div>
</div>
CSS
/* One button, every state. The .is-* classes force a state for the demo grid; the real pseudo-classes
   (:hover, :focus-visible, :active, :disabled) and ARIA attributes style the same way. */
.st{width:100%;border:0;border-radius:10px;padding:11px 20px;font:inherit;font-size:.88rem;font-weight:700;
  line-height:normal;background:#4f46e5;color:#fff;cursor:pointer;
  transition:background-color .18s,box-shadow .18s,transform .1s}
.st:hover:not(:disabled),.st.is-hover{background:#4338ca}
.st:focus-visible,.st.is-focus{outline:0;box-shadow:0 0 0 3px #fff,0 0 0 5px #4f46e5}
.st:active:not(:disabled),.st.is-active{background:#3730a3;transform:translateY(1px)}
.st:disabled{background:#e4e7ec;color:#98a2b3;cursor:not-allowed}
.st[aria-pressed=true]{background:#101828}
.st[aria-busy=true]{background:#5558e8;display:inline-flex;align-items:center;justify-content:center;gap:9px;cursor:progress}
.st[aria-busy=true]::before{content:"";width:14px;height:14px;border-radius:50%;
  border:2px solid rgba(255,255,255,.4);border-top-color:#fff;animation:st-spin .7s linear infinite}
@keyframes st-spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){
  .st{transition:none}
  .st:active:not(:disabled),.st.is-active{transform:none}
  .st[aria-busy=true]::before{animation-duration:2.6s}
}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="demo-grid">
  <div class="cell"><b>Rest</b><button class="btn st" type="button">Save changes</button></div>
  <div class="cell"><b>Hover</b><button class="btn st is-hover" type="button">Save changes</button></div>
  <div class="cell"><b>Focus visible</b><button class="btn st is-focus" type="button">Save changes</button></div>
  <div class="cell"><b>Active</b><button class="btn st active" type="button">Save changes</button></div>
  <div class="cell"><b>Disabled</b><button class="btn st" type="button" disabled>Save changes</button></div>
  <div class="cell"><b>Toggled</b><button class="btn st" type="button" aria-pressed="true">Following</button></div>
  <div class="cell"><b>Busy</b><button class="btn st" type="button" aria-busy="true"><span class="spinner-border" aria-hidden="true"></span>Saving</button></div>
</div>
CSS
/* One .btn, every state, set through the --bs-btn-* variables. .is-hover and .is-focus force a state for the
   demo grid; .active is Bootstrap's own forced-press class. */
.st{--bs-btn-padding-x:20px;--bs-btn-padding-y:11px;--bs-btn-font-size:.88rem;--bs-btn-font-weight:700;
  --bs-btn-line-height:normal;--bs-btn-border-width:0;--bs-btn-border-radius:10px;
  --bs-btn-color:#fff;--bs-btn-bg:#4f46e5;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#4338ca;
  --bs-btn-active-color:#fff;--bs-btn-active-bg:#3730a3;
  --bs-btn-disabled-color:#98a2b3;--bs-btn-disabled-bg:#e4e7ec;--bs-btn-disabled-opacity:1;
  --bs-btn-focus-box-shadow:0 0 0 3px #fff,0 0 0 5px #4f46e5;
  width:100%;transition:background-color .18s,box-shadow .18s,transform .1s}
.st.is-hover{background-color:var(--bs-btn-hover-bg)}
.st.is-focus{box-shadow:var(--bs-btn-focus-box-shadow)}
.st.active,.st:active{transform:translateY(1px)}
.st[aria-pressed=true]{--bs-btn-bg:#101828;--bs-btn-active-bg:#101828}
.st[aria-busy=true]{--bs-btn-bg:#5558e8;display:inline-flex;align-items:center;justify-content:center;gap:9px;cursor:progress}
.st .spinner-border{--bs-spinner-width:14px;--bs-spinner-height:14px;--bs-spinner-border-width:2px;
  --bs-spinner-animation-speed:.7s;border-color:rgba(255,255,255,.4);border-top-color:#fff}
@media (prefers-reduced-motion:reduce){
  .st{transition:none}
  .st.active,.st:active{transform:none}
  .st .spinner-border{--bs-spinner-animation-speed:2.6s}
}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="grid grid-cols-[repeat(auto-fit,minmax(132px,1fr))] gap-x-[18px] gap-y-[22px]">
  <div class="flex flex-col items-center gap-2.5"><b class="text-[.72rem] font-bold tracking-[.07em] text-[#667085] uppercase">Rest</b><button type="button" class="w-full cursor-pointer rounded-[10px] px-5 py-[11px] text-[.88rem] font-bold text-white transition-[background-color,box-shadow,transform] duration-[180ms] motion-reduce:transition-none bg-[#4f46e5] enabled:hover:bg-[#4338ca] focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_#4f46e5] focus-visible:outline-none enabled:active:translate-y-px enabled:active:bg-[#3730a3] motion-reduce:enabled:active:translate-y-0">Save changes</button></div>
  <div class="flex flex-col items-center gap-2.5"><b class="text-[.72rem] font-bold tracking-[.07em] text-[#667085] uppercase">Hover</b><button type="button" class="w-full cursor-pointer rounded-[10px] px-5 py-[11px] text-[.88rem] font-bold text-white transition-[background-color,box-shadow,transform] duration-[180ms] motion-reduce:transition-none enabled:hover:bg-[#4338ca] focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_#4f46e5] focus-visible:outline-none enabled:active:translate-y-px enabled:active:bg-[#3730a3] motion-reduce:enabled:active:translate-y-0 bg-[#4338ca]">Save changes</button></div>
  <div class="flex flex-col items-center gap-2.5"><b class="text-[.72rem] font-bold tracking-[.07em] text-[#667085] uppercase">Focus visible</b><button type="button" class="w-full cursor-pointer rounded-[10px] px-5 py-[11px] text-[.88rem] font-bold text-white transition-[background-color,box-shadow,transform] duration-[180ms] motion-reduce:transition-none bg-[#4f46e5] enabled:hover:bg-[#4338ca] focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_#4f46e5] focus-visible:outline-none enabled:active:translate-y-px enabled:active:bg-[#3730a3] motion-reduce:enabled:active:translate-y-0 shadow-[0_0_0_3px_#fff,0_0_0_5px_#4f46e5]">Save changes</button></div>
  <div class="flex flex-col items-center gap-2.5"><b class="text-[.72rem] font-bold tracking-[.07em] text-[#667085] uppercase">Active</b><button type="button" class="w-full cursor-pointer rounded-[10px] px-5 py-[11px] text-[.88rem] font-bold text-white transition-[background-color,box-shadow,transform] duration-[180ms] motion-reduce:transition-none enabled:hover:bg-[#4338ca] focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_#4f46e5] focus-visible:outline-none enabled:active:translate-y-px enabled:active:bg-[#3730a3] motion-reduce:enabled:active:translate-y-0 translate-y-px bg-[#3730a3] motion-reduce:translate-y-0">Save changes</button></div>
  <div class="flex flex-col items-center gap-2.5"><b class="text-[.72rem] font-bold tracking-[.07em] text-[#667085] uppercase">Disabled</b><button type="button" disabled class="w-full cursor-pointer rounded-[10px] px-5 py-[11px] text-[.88rem] font-bold text-white transition-[background-color,box-shadow,transform] duration-[180ms] motion-reduce:transition-none bg-[#4f46e5] enabled:hover:bg-[#4338ca] focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_#4f46e5] focus-visible:outline-none enabled:active:translate-y-px enabled:active:bg-[#3730a3] motion-reduce:enabled:active:translate-y-0 disabled:cursor-not-allowed disabled:bg-[#e4e7ec] disabled:text-[#98a2b3]">Save changes</button></div>
  <div class="flex flex-col items-center gap-2.5"><b class="text-[.72rem] font-bold tracking-[.07em] text-[#667085] uppercase">Toggled</b><button type="button" aria-pressed="true" class="w-full cursor-pointer rounded-[10px] px-5 py-[11px] text-[.88rem] font-bold text-white transition-[background-color,box-shadow,transform] duration-[180ms] motion-reduce:transition-none bg-[#4f46e5] enabled:hover:bg-[#4338ca] focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_#4f46e5] focus-visible:outline-none enabled:active:translate-y-px enabled:active:bg-[#3730a3] motion-reduce:enabled:active:translate-y-0 aria-pressed:bg-[#101828]">Following</button></div>
  <div class="flex flex-col items-center gap-2.5"><b class="text-[.72rem] font-bold tracking-[.07em] text-[#667085] uppercase">Busy</b><button type="button" aria-busy="true" class="w-full cursor-pointer rounded-[10px] px-5 py-[11px] text-[.88rem] font-bold text-white transition-[background-color,box-shadow,transform] duration-[180ms] motion-reduce:transition-none bg-[#4f46e5] enabled:hover:bg-[#4338ca] focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_#4f46e5] focus-visible:outline-none enabled:active:translate-y-px enabled:active:bg-[#3730a3] motion-reduce:enabled:active:translate-y-0 inline-flex cursor-progress items-center justify-center gap-[9px] aria-busy:bg-[#5558e8]"><span aria-hidden="true" class="size-3.5 animate-st-spin rounded-full border-2 border-white/40 border-t-white motion-reduce:animate-st-spin-slow"></span>Saving</button></div>
</div>
CSS
@theme {
  --animate-st-spin: spin .7s linear infinite;
  --animate-st-spin-slow: spin 2.6s linear infinite;
}

About this set

One primary button drawn in each of its seven states, side by side with a caption: rest, hover, focus visible, active, disabled, toggled and busy. It is a reference sheet for checking that a design system covers every state before it ships, and a starting point for your own button. Hover darkens the fill, focus visible adds a two-ring focus indicator, active darkens further and moves the button down one pixel, disabled turns grey, toggled turns near-black and busy shows a spinner beside the label. The first four cells use .is-hover, .is-focus and .is-active classes to force their state on screen, and the same rules also answer the real :hover, :focus-visible and :active pseudo-classes, so every button in the grid responds to the mouse and keyboard as well. The toggled state is read from aria-pressed and the busy state from aria-busy rather than from classes, so what is shown matches what assistive technology announces. The busy spinner is a CSS pseudo-element in plain CSS, a restyled .spinner-border in Bootstrap and a bordered span with animate-spin in Tailwind.

What’s included

  • Seven states: rest, hover, focus visible, active, disabled, toggled, busy
  • Toggled and busy states styled from aria-pressed and aria-busy
  • Busy button keeps its width while the spinner shows
  • Busy fill darkened to #5558e8 so white text reaches 4.5:1
  • Press offset and transitions off under reduced motion

Accessibility

Every cell is a native button with visible text. The toggled button carries aria-pressed="true" and the busy button aria-busy="true", and the Bootstrap spinner is aria-hidden. Keyboard focus draws the same white and indigo double ring shown in the Focus visible cell. Under prefers-reduced-motion the transitions and the one-pixel press offset are removed and the spinner slows from 0.7 to 2.6 seconds per turn rather than stopping, so the busy state stays visible.

Customise it

Change the fills in the .st rules (#4f46e5 rest, #4338ca hover, #3730a3 active, #101828 toggled, #5558e8 busy). In Bootstrap they are --bs-btn-bg, --bs-btn-hover-bg and --bs-btn-active-bg on .st, with the spinner sized through --bs-spinner-width; in Tailwind edit the bg-, aria-pressed: and aria-busy: utilities.