CSS Button Generator
Every state, contrast-checked. CSS or Tailwind.
4.49:1 passes AA for large text only — below 4.5:1 for normal button labels.
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 18px;
font-size: 14px;
font-weight: 600;
line-height: 1.2;
color: #ffffff;
background: #d8401f;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s, transform 0.1s, box-shadow 0.15s, filter 0.15s;
}
.btn:hover:not(:disabled) {
filter: brightness(0.92);
}
.btn:active:not(:disabled) {
transform: translateY(1px);
filter: brightness(0.88);
}
.btn:focus-visible {
outline: 2px solid #d8401f;
outline-offset: 2px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
The four states most button CSS forgets
Most button snippets give you a default state and leave you to invent the rest. A button is five states, and the ones that get skipped — focus and disabled — are exactly the ones that decide whether the control is usable by keyboard or legible when inactive. All five are written here whether you look at them or not.
How to use it
-
Pick a variant
Solid, Outline, Ghost, Dark, Pill or Block. Each is a complete starting point with its own sensible hover behaviour.
-
Shape it
Padding, font size, weight, corner radius and border width. The preview updates with the exact values the CSS will contain.
-
Check every state
Switch between default, hover, active, focus and disabled in the preview. All five are written into the output whether you look at them or not.
-
Watch the contrast reading
The label contrast ratio is shown live and flags anything below WCAG AA before you ship it.
-
Copy the code
Full CSS with :hover, :active, :focus-visible and :disabled rules, or the equivalent Tailwind class string.
Disabled is an attribute, not a class
Style :disabled rather than .is-disabled. The pseudo-class follows
the actual HTML attribute, which means the styling can never drift out of sync with whether
the button really is inert. And remember to guard your hover rule with
:not(:disabled), or a dead button will still respond to the cursor.
Never delete the focus ring
outline: none with nothing to replace it is the single most common accessibility
failure in button CSS. The reason people reach for it is that plain :focus shows
the ring on mouse clicks too, which looks wrong. :focus-visible solves that
properly: the ring appears for keyboard users and stays out of the way for everyone else.
Contrast, checked as you go
A button label needs 4.5:1 against its background to pass WCAG AA at normal sizes. That is easy to fail with a mid-tone brand colour and white text, and almost impossible to judge by eye. The ratio above the preview updates with every change and tells you which side of the line you are on.
Laying buttons out
A row of buttons is a flexbox problem, not a grid one — use the flexbox generator for the container and let each button size itself.
Frequently asked questions
How do I style a disabled button in CSS?
:disabled pseudo-class: .btn:disabled { opacity: 0.5; cursor: not-allowed; }. Use the pseudo-class rather than a class name so the styling can never drift out of sync with whether the button really is inert. Also guard your hover rule with :not(:disabled).Why does cursor: not-allowed not work on a disabled button?
pointer-events: none is also set, which stops the element receiving any pointer interaction including the cursor change. Remove it — the disabled attribute already blocks clicks on its own.How do I make an outline button in CSS?
Should I use :focus or :focus-visible?
:focus-visible. Plain :focus also shows the ring when you click with a mouse, which is why people delete outlines entirely and break keyboard navigation. :focus-visible shows the indicator only when the browser judges one is needed.How do I make a close button in CSS?
× character or icon. The important part is the accessible name: the glyph means nothing to a screen reader, so add aria-label="Close".