Skip to content
ColorKit

Free CSS button builder

CSS button generator that grades every state

A button is five buttons — resting, hovered, pressed, focused and disabled — and four of them are where the bugs live. Free, no account, nothing to install: give it one fill and it derives the other states in CIE Lab, then grades the label on each of them and checks the focus ring against the button and the page behind it, live, as you drag. The hit area is measured from the real preview node rather than guessed from the padding.

  • 100% free
  • No signup
  • 5 states
  • 7 live WCAG checks
  • Copy or download .css
Live — hover it, hold it down, and press Tab to see the focus ring
:hover
:active
:focus-visible
:disabled
Measured: Below the 44 px AAA target
Starting point
Fill

States — derived from the fill, not typed twice

Shadow

Focus ring and disabled

Focus indicator
Disabled treatment

Contrast, one state at a time

Label on the resting fill6.28:1SC 1.4.3 4.5:1 pass
Label on the hover fill7.53:1SC 1.4.3 4.5:1 pass
Label on the pressed fill8.76:1SC 1.4.3 4.5:1 pass
Label on the disabled fill3.14:14.5:1 target, exempt
Inner ring against the button6.28:1SC 1.4.11 3:1 pass
Outer ring against the page11.42:1SC 1.4.11 3:1 pass
Button edge against the page6.28:1SC 1.4.11 3:1 pass

Every non-exempt state clears its threshold.

CSS

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 11px 20px;
  border: 1px solid transparent;
  border-radius: 8px;
  background: #4f46e5;
  color: #ffffff;
  font-family: inherit;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12);
  transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease, box-shadow 150ms ease, transform 150ms ease;
}

@media (hover: hover) {
  .btn:hover:not(:disabled):not([aria-disabled="true"]) {
    background: #483ec7;
    box-shadow: 0 2px 4px rgba(15, 23, 42, 0.16);
  }
}

.btn:active:not(:disabled):not([aria-disabled="true"]) {
  background: #4238ae;
  box-shadow: 0 1px 1px rgba(15, 23, 42, 0.2);
  transform: translateY(1px);
}

.btn:focus-visible {
  outline: 3px solid transparent;
  outline-offset: 2px;
  box-shadow: 0 0 0 2px #ffffff, 0 0 0 5px #312e81, 0 1px 2px rgba(15, 23, 42, 0.12);
}

.btn:disabled,
.btn[aria-disabled="true"] {
  background: #e0d4fb;
  color: #7a7780;
  box-shadow: none;
  transform: none;
  cursor: not-allowed;
}

@media (prefers-reduced-motion: reduce) {
  .btn {
    transition: none;
  }

  .btn:active {
    transform: none;
  }
}

7 rule blocks · 1281 characters · flat fill · ring 3px at 2px offset

Press Ctrl+V anywhere on the page to drop in a hex code or a background declaration to recolor the button — no need to click the field first.

How to generate a CSS button

Three steps from one color to a rule set with all five states and their contrast verdicts.

  1. Set the resting button, then let the states follow it

    Type a label, drop in a fill — a hex code, an rgb() value, a CSS color name, or paste a whole background declaration — and set padding, radius, border width and type size. Five starting points cover the shapes you are likely to be after: a solid primary, an outline secondary, a gradient CTA, a destructive button with the press travel switched off, and a 44px neutral pill. The hover and pressed fills are not separate color pickers: they are the resting fill moved through Lab by the shift sliders, so one color change carries the whole set.

  2. Decide how focus and disabled look, because they are the two that get skipped

    Choose a single outline or the two-tone ring, whose inner band lands on the button and outer band on the page so the indicator survives both surfaces. Set the page color to the real background the button will sit on — a ring tuned on white can vanish on a slate panel. For disabled, compare opacity: 0.5 against muted tokens and the dashed ghost: opacity multiplies the label, the border and the shadow down together, and the panel shows you what ratio that leaves.

  3. Read the verdicts, fix the ring, then copy or download the rule set

    The table grades the label against the resting, hover, pressed and disabled fills and the ring against everything it touches, updating on every drag. Fix the ring walks the ring color toward black or white until it clears 3:1 without abandoning the hue. Rename the selector from .btn to whatever your codebase calls it, then take the CSS by clipboard or as a button.css file.

Technical specifications

States generatedFive — resting, :hover, :active, :focus-visible and disabled, plus a prefers-reduced-motion block that drops the transition and the press travel
State derivationHover and pressed fills are the base color mixed toward black or white in CIE Lab, −60% to +40%, so a light and a dark button darken by the same perceived amount
Contrast checks per changeUp to 7 — the label on four fills under SC 1.4.3, the ring against the button and the page under SC 1.4.11, and an advisory reading for the button edge
Focus indicatorSingle outline with offset −4px to +10px, or a two-tone box-shadow ring; widths 1-8px, with a warning below the 2 CSS pixel minimum in SC 2.4.13
Disabled treatments3 — opacity: 0.5 composited over the page so you can read the real ratio, muted tokens held above 3:1, and a dashed ghost outline
Hit areaMeasured from the live button with a ResizeObserver and compared to 24 × 24px (SC 2.5.8, AA) and 44 × 44px (SC 2.5.5, AAA) — not estimated from the padding
Input limitsLabels up to 120 characters with a warning past 48, selectors up to 60 characters with braces and quotes rejected, pasted text over 2,000 characters ignored
Output5 to 7 rule blocks depending on the options, free of attribution comments, on the clipboard or as button.css — generated in the tab, so nothing you type reaches a server

Frequently asked questions

What is the difference between :focus and :focus-visible on a button?

:focus matches whenever the button holds focus, including after a mouse click, while :focus-visible matches only when the browser judges an indicator to be useful — keyboard navigation, or a pointer interaction it heuristically decides needs one. That is why a ring drawn with :focus appears to stick after every click and gets ripped out with outline: none by the next developer, which is how buttons lose their keyboard indicator entirely. Style :focus-visible and leave :focus alone; Safari has supported it since 15.4, so there is no longer a fallback worth writing.

Does the focus ring need 3:1 against the button or against the page?

Against whatever is adjacent to it, which depends on the offset. With outline-offset above zero the gap shows the page background through it, so the ring only borders the page and one 3:1 check applies; at offset 0 the ring touches the fill on one side and the page on the other and owes 3:1 to both; a negative offset puts it inside the button, where only the fill matters. The two-tone ring is the way out of the trade-off: a light inner band against a dark button and a dark outer band against a light page, each one meeting SC 1.4.11 on the surface it actually sits on.

Is opacity: 0.5 an acceptable disabled style?

It conforms, because SC 1.4.3 and SC 1.4.11 both exempt inactive controls, and it is still usually the wrong choice. Opacity composites the whole element — fill, label, border and shadow — over the page at once, so a white label on a mid indigo drops to roughly 2:1 and the border you were relying on to keep the shape readable fades with it. Muted tokens set the disabled fill and label independently, which is why this generator keeps the disabled label above 3:1 by construction and prints the ratio next to it.

Should the markup use disabled or aria-disabled="true"?

Use disabled when the control genuinely cannot be operated, and aria-disabled when you want it focusable so a keyboard user can find it and be told why. A disabled attribute removes the button from the tab order and swallows all events, which is fine for a toggle inside a form and hostile for a Submit button that is off until a field is filled. The exported CSS targets both, and pairs the hover and active rules with :not([aria-disabled="true"]) so an aria-disabled button does not light up under the cursor.

Why does the hover style stay stuck after I tap the button on a phone?

Because a touch browser has no way to un-hover: it synthesises a hover on tap and leaves it applied until you touch something else. Wrapping the hover rule in @media (hover: hover) restricts it to pointers that can actually hover, which is the checkbox above the CSS panel and is on by default. The pressed state does not need the guard — :active fires and clears correctly on touch, and is the state a phone user is really getting.

Why does my gradient button jump between states instead of fading?

Because a gradient is a background-image, and CSS interpolates images only in the narrow cases the spec defines — two gradients of different colors are not among them. The transition applies fine to background-color, border-color and box-shadow, and does nothing at all to the fill, which is why gradient mode prints a warning under the output instead of pretending. The workarounds are a background-position slide across an oversized gradient, or a pseudo-element holding the second gradient whose opacity you animate.

Why does the button's shadow disappear the moment it receives focus?

Because the two-tone ring is built out of box-shadow, and box-shadow is a single property — declaring it in :focus-visible replaces the resting shadow rather than adding to it. The generator handles this by re-appending your resting shadow to the end of the focus declaration, so a raised button stays raised while focused. If you hand-write a ring this way, remember the same rule applies to any inset highlight you had on the button.

About button states and the two criteria nobody checks

Most button CSS is written once, for the state nobody has to interact with. The resting style gets a designer, a token and a review; the hover fill gets whatever the darkest swatch in the palette happened to be, the pressed state gets a copy of the hover fill, and focus and disabled arrive as an afterthought during the accessibility audit six months later. That order is backwards, because the states are where the contrast fails. A white label reads at 8.6:1 on a mid indigo and can drop under 4.5:1 the moment a hover lightens the fill, and nothing in a browser will tell you — the ratio moved, the design did not.

Two success criteria govern a button and they pull in opposite directions. SC 1.4.3 asks for 4.5:1 between the label and its background at every size below 24px, or 18.66px when bold, which is the one everyone knows. SC 1.4.11 Non-text Contrast asks for 3:1 on the visual information needed to identify a control and its state, and the focus ring is exactly that. The trap is the word adjacent: a ring drawn with outline-offset above zero borders the page, a ring at offset 0 borders the fill on one side and the page on the other, and a ring tuned against a white page silently fails on the slate panel your marketing team drops the same button into. That is the argument for the two-tone ring, and it is the argument for setting the page color in this tool before you judge anything. WCAG 2.2 added SC 2.4.13 Focus Appearance on top, which wants at least 2 CSS pixels of indicator, and SC 2.5.8, which puts a 24 × 24px floor under the hit area — the reason the preview reports its measured size rather than its padding. If you want the same maths on a pair of colors outside a button, the color contrast checker is the standalone version.

What other button generators get wrong is scope. They export a single rule with a hard-coded hover color, no focus block at all, and a disabled state that is one line of opacity — which conforms, since inactive controls are exempt, but hides the fact that compositing at 50% takes a white label on indigo down to about 2:1 and takes the border with it. They also treat the button as a decorated rectangle, which is why the parts worth tuning carefully belong to their own tools: the corner geometry, including the eight-value slash syntax, comes from the border radius generator; a real elevation stack instead of the four presets here comes from the box shadow generator; a fill with more than two stops, or a conic or radial one, is the job of the gradient generator and its syntax-first companion, the css gradient generator. Two shapes that regularly get attached to a button live elsewhere too: a chevron or a skewed edge cut into the button belongs to the clip path generator, and the little pointer on the popover a button opens comes from the css arrow generator. Paste any of those values back in here and the state maths runs over them.

Where the states are computed

Every number on this page is worked out by JavaScript running in the tab you are reading it in. Nothing you type, paste or open is uploaded, logged or kept, which is also why the tools carry on working after you disconnect from the network.

The preview is a real DOM button with the generated stylesheet injected into this page, so the states you are judging are rendered by your own browser engine — nothing is screenshotted, and the label you type never leaves the tab.