01 / 51
All 51 in 51 Tailwind CSS Buttons
- 01Tailwind Submit Button (Form Primary)
- 02Tailwind Primary and Secondary Button Pair
- 03Tailwind Ghost / Outline Button
- 04Tailwind Icon-Only Button with Accessible Tooltip
- 05Tailwind Gradient CTA Button
- 06Tailwind Loading Spinner Button (State Transition)
- 07Tailwind Disabled Button State (Full State Reference)
- 08Tailwind Button Group Component (Segmented Radio-Like)
- 09Tailwind Add to Cart Button
- 10Tailwind Download Progress Button
- 11Tailwind Like Heart Button
- 12Tailwind Confirm Delete Button
- 13Tailwind Play Pause Button
- 14Tailwind Subscribe Confetti Button
- 15Tailwind Magnetic Mercury Ripple Button
- 16Tailwind Brutalist Glitch Button
- 17Tailwind Neon Plasma Burst Button
- 18Tailwind Clay Press Button
- 19Tailwind Editorial Ink Spread Button
- 20Tailwind Particle Shard Burst Button
- 21Tailwind Brushed Brass Plate Button
- 22Tailwind Washi Paper Crane Button
- 23Tailwind Frosted Ice Cube Button
- 24Tailwind Vintage Typewriter Key Button
- 25Tailwind Lacquered Walnut Wood Button
- 26Tailwind Pharma Gel Capsule Button
- 27Tailwind Aged Copper Coin Button
- 28Tailwind Origami Fortune Cookie Button
- 29Tailwind Holographic Foil Sticker Button
- 30Tailwind Brutalist Concrete Slab Button
- 31Tailwind Mercury Bead Morph Button
- 32Tailwind Soft Pill Modern SaaS Button
- 33Tailwind Masking Tape Label Button
- 34Tailwind Obsidian Glass Shard Button
- 35Tailwind Wine Velvet Ribbon Button
- 36Tailwind LED Segment Display Button
- 37Tailwind Cassette Mixtape Label Button
- 38Tailwind Chalkboard Menu Button
- 39Tailwind Vinyl Record Sleeve Ripple Button
- 40Tailwind Polaroid Snapshot Button
- 41Tailwind Airline Boarding Pass Button
- 42Tailwind Aged Brass Subway Token Button
- 43Tailwind Vintage Rx Pharmacy Label Button
- 44Tailwind Bakelite Radio Dial Toggle Button
- 45Tailwind Regal Postage Stamp Button
- 46Tailwind Victorian Cigar Band Foil Button
- 47Tailwind Vintage Calculator Key Button
- 48Tailwind Toolbox Latch Spotlight Button
- 49Tailwind Receipt Paper Total Count-Up Button
- 50Tailwind IBM Punch Card Magnetic Follow Button
- 51Tailwind Brass Periscope Lens Button
Tailwind Submit Button (Form Primary)
The button every form ends with, ported: a two-layer elevation shadow that reads as physical depth, a masked sheen that sweeps once on hover, a real 1px press travel, and a focus ring that survives on any background. The port's win is that all four states now sit in one class list — hover:, active:, focus-visible: and disabled: read in the order a user meets them.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-01 w-full min-h-svh grid place-items-center bg-tb01-bg font-tb01-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="submit" class="group relative isolate overflow-hidden cursor-pointer inline-grid grid-flow-col items-center gap-2.5 min-h-13 px-7 border-0 rounded-[0.9rem] text-base font-semibold tracking-[-0.01em] text-tb01-on-accent bg-[linear-gradient(180deg,var(--color-tb01-accent-lift),var(--color-tb01-accent))] transition-[translate,box-shadow,filter] duration-200 ease-tb01-out before:content-[''] before:absolute before:inset-0 before:-z-10 before:pointer-events-none before:-translate-x-[125%] before:bg-[linear-gradient(105deg,transparent_35%,rgba(255,255,255,0.42)_50%,transparent_65%)] before:transition-transform before:duration-[800ms] before:ease-tb01-out hover:-translate-y-0.5 hover:saturate-[1.06] hover:before:translate-x-[125%] focus-visible:before:translate-x-[125%] active:translate-y-px focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb01-ring disabled:cursor-not-allowed disabled:grayscale disabled:opacity-50 disabled:translate-y-0 disabled:shadow-none motion-reduce:transition-none motion-reduce:before:hidden shadow-[inset_0_1px_0_rgba(255,255,255,0.38),0_1px_2px_var(--color-tb01-shadow-soft),0_10px_24px_-8px_var(--color-tb01-shadow-wide)] active:shadow-[inset_0_2px_6px_var(--color-tb01-shadow-press)] focus-visible:shadow-[inset_0_1px_0_rgba(255,255,255,0.38),0_0_0_6px_var(--color-tb01-halo)]">
<span>Create account</span>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" class="w-[1.15rem] h-[1.15rem] transition-transform duration-300 ease-tb01-out group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0"><path d="M4 12h14"/><path d="M12.5 5.5 19 12l-6.5 6.5"/></svg>
</button>
</div><div class="tb-01 w-full min-h-svh grid place-items-center bg-tb01-bg font-tb01-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="submit" class="group relative isolate overflow-hidden cursor-pointer inline-grid grid-flow-col items-center gap-2.5 min-h-13 px-7 border-0 rounded-[0.9rem] text-base font-semibold tracking-[-0.01em] text-tb01-on-accent bg-[linear-gradient(180deg,var(--color-tb01-accent-lift),var(--color-tb01-accent))] transition-[translate,box-shadow,filter] duration-200 ease-tb01-out before:content-[''] before:absolute before:inset-0 before:-z-10 before:pointer-events-none before:-translate-x-[125%] before:bg-[linear-gradient(105deg,transparent_35%,rgba(255,255,255,0.42)_50%,transparent_65%)] before:transition-transform before:duration-[800ms] before:ease-tb01-out hover:-translate-y-0.5 hover:saturate-[1.06] hover:before:translate-x-[125%] focus-visible:before:translate-x-[125%] active:translate-y-px focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb01-ring disabled:cursor-not-allowed disabled:grayscale disabled:opacity-50 disabled:translate-y-0 disabled:shadow-none motion-reduce:transition-none motion-reduce:before:hidden shadow-[inset_0_1px_0_rgba(255,255,255,0.38),0_1px_2px_var(--color-tb01-shadow-soft),0_10px_24px_-8px_var(--color-tb01-shadow-wide)] active:shadow-[inset_0_2px_6px_var(--color-tb01-shadow-press)] focus-visible:shadow-[inset_0_1px_0_rgba(255,255,255,0.38),0_0_0_6px_var(--color-tb01-halo)]">
<span>Create account</span>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" class="w-[1.15rem] h-[1.15rem] transition-transform duration-300 ease-tb01-out group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0"><path d="M4 12h14"/><path d="M12.5 5.5 19 12l-6.5 6.5"/></svg>
</button>
</div>@import "tailwindcss";
@theme {
--color-tb01-bg: #f2f1ee;
--color-tb01-bg: oklch(0.965 0.006 95);
--color-tb01-accent: #16a34a;
--color-tb01-accent: oklch(0.58 0.15 152);
--color-tb01-accent-lift: #2dbb60;
--color-tb01-accent-lift: oklch(0.645 0.148 152);
--color-tb01-on-accent: #ffffff;
--color-tb01-ring: #0f7538;
--color-tb01-ring: oklch(0.45 0.13 152);
--color-tb01-shadow-soft: #0b3d2061;
--color-tb01-shadow-soft: oklch(0.32 0.09 155 / 0.38);
--color-tb01-shadow-wide: #0b3d208c;
--color-tb01-shadow-wide: oklch(0.32 0.09 155 / 0.55);
--color-tb01-shadow-press: #0b3d2073;
--color-tb01-shadow-press: oklch(0.32 0.09 155 / 0.45);
--color-tb01-halo: #16a34a38;
--color-tb01-halo: oklch(0.58 0.15 152 / 0.22);
--font-tb01-sans: "Schibsted Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb01-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-01 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Dark scheme re-points the same tokens; @theme cannot hold a media query. */
@media (prefers-color-scheme: dark) {
.tb-01:not([data-theme="light"]) {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
--color-tb01-shadow-soft: oklch(0 0 0 / 0.5);
--color-tb01-shadow-wide: oklch(0 0 0 / 0.7);
}
}
.tb-01[data-theme="dark"] {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
}
@media (forced-colors: active) {
.tb-01 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-01 button::before {
display: none;
}
}@import "tailwindcss";
@theme {
--color-tb01-bg: #f2f1ee;
--color-tb01-bg: oklch(0.965 0.006 95);
--color-tb01-accent: #16a34a;
--color-tb01-accent: oklch(0.58 0.15 152);
--color-tb01-accent-lift: #2dbb60;
--color-tb01-accent-lift: oklch(0.645 0.148 152);
--color-tb01-on-accent: #ffffff;
--color-tb01-ring: #0f7538;
--color-tb01-ring: oklch(0.45 0.13 152);
--color-tb01-shadow-soft: #0b3d2061;
--color-tb01-shadow-soft: oklch(0.32 0.09 155 / 0.38);
--color-tb01-shadow-wide: #0b3d208c;
--color-tb01-shadow-wide: oklch(0.32 0.09 155 / 0.55);
--color-tb01-shadow-press: #0b3d2073;
--color-tb01-shadow-press: oklch(0.32 0.09 155 / 0.45);
--color-tb01-halo: #16a34a38;
--color-tb01-halo: oklch(0.58 0.15 152 / 0.22);
--font-tb01-sans: "Schibsted Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb01-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-01 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Dark scheme re-points the same tokens; @theme cannot hold a media query. */
@media (prefers-color-scheme: dark) {
.tb-01:not([data-theme="light"]) {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
--color-tb01-shadow-soft: oklch(0 0 0 / 0.5);
--color-tb01-shadow-wide: oklch(0 0 0 / 0.7);
}
}
.tb-01[data-theme="dark"] {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
}
@media (forced-colors: active) {
.tb-01 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-01 button::before {
display: none;
}
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tb01-bg: #f2f1ee;
--color-tb01-bg: oklch(0.965 0.006 95);
--color-tb01-accent: #16a34a;
--color-tb01-accent: oklch(0.58 0.15 152);
--color-tb01-accent-lift: #2dbb60;
--color-tb01-accent-lift: oklch(0.645 0.148 152);
--color-tb01-on-accent: #ffffff;
--color-tb01-ring: #0f7538;
--color-tb01-ring: oklch(0.45 0.13 152);
--color-tb01-shadow-soft: #0b3d2061;
--color-tb01-shadow-soft: oklch(0.32 0.09 155 / 0.38);
--color-tb01-shadow-wide: #0b3d208c;
--color-tb01-shadow-wide: oklch(0.32 0.09 155 / 0.55);
--color-tb01-shadow-press: #0b3d2073;
--color-tb01-shadow-press: oklch(0.32 0.09 155 / 0.45);
--color-tb01-halo: #16a34a38;
--color-tb01-halo: oklch(0.58 0.15 152 / 0.22);
--font-tb01-sans: "Schibsted Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb01-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-01 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Dark scheme re-points the same tokens; @theme cannot hold a media query. */
@media (prefers-color-scheme: dark) {
.tb-01:not([data-theme="light"]) {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
--color-tb01-shadow-soft: oklch(0 0 0 / 0.5);
--color-tb01-shadow-wide: oklch(0 0 0 / 0.7);
}
}
.tb-01[data-theme="dark"] {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
}
@media (forced-colors: active) {
.tb-01 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-01 button::before {
display: none;
}
}@import "tailwindcss";
@theme {
--color-tb01-bg: #f2f1ee;
--color-tb01-bg: oklch(0.965 0.006 95);
--color-tb01-accent: #16a34a;
--color-tb01-accent: oklch(0.58 0.15 152);
--color-tb01-accent-lift: #2dbb60;
--color-tb01-accent-lift: oklch(0.645 0.148 152);
--color-tb01-on-accent: #ffffff;
--color-tb01-ring: #0f7538;
--color-tb01-ring: oklch(0.45 0.13 152);
--color-tb01-shadow-soft: #0b3d2061;
--color-tb01-shadow-soft: oklch(0.32 0.09 155 / 0.38);
--color-tb01-shadow-wide: #0b3d208c;
--color-tb01-shadow-wide: oklch(0.32 0.09 155 / 0.55);
--color-tb01-shadow-press: #0b3d2073;
--color-tb01-shadow-press: oklch(0.32 0.09 155 / 0.45);
--color-tb01-halo: #16a34a38;
--color-tb01-halo: oklch(0.58 0.15 152 / 0.22);
--font-tb01-sans: "Schibsted Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb01-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-01 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Dark scheme re-points the same tokens; @theme cannot hold a media query. */
@media (prefers-color-scheme: dark) {
.tb-01:not([data-theme="light"]) {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
--color-tb01-shadow-soft: oklch(0 0 0 / 0.5);
--color-tb01-shadow-wide: oklch(0 0 0 / 0.7);
}
}
.tb-01[data-theme="dark"] {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
}
@media (forced-colors: active) {
.tb-01 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-01 button::before {
display: none;
}
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Utilities: layout, the lift/press travel, the sheen transform, the focus ring and every state variant. CSS: the dark-scheme token re-point (a media query cannot live in
@theme) and theforced-colorsblock, which repaints the button and its pseudo-element together. - The three-layer elevation shadow stays one arbitrary value rather than three
shadow-*utilities, because Tailwind composesbox-shadowas a single property — splitting it would just mean the last one wins. - The gradient fill needed a second token:
color-mix(in oklab, accent 88%, white)has no utility form inside an arbitrary gradient, so the lighter stop became--color-tb01-accent-liftin@theme. Two tokens, one intent — and both are now available asbg-*utilities elsewhere. - A11y fix from the CSS original: the SVG nudge was
:hover-only; it is nowgroup-hover:with amotion-reduce:reset, and the sheen fires onfocus-visible:as well as hover, which the original did only for the sheen and not the arrow.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Submit Button (Form Primary)
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-submit-button-form-primary/
The button every form ends with, ported: a two-layer elevation shadow that reads as physical depth, a masked sheen that sweeps once on hover, a real 1px press travel, and a focus ring that survives on any background. The port's win is that all four states now sit in one class list — hover:, active:, focus-visible: and disabled: read in the order a user meets them.
## HTML
```html
<div class="tb-01 w-full min-h-svh grid place-items-center bg-tb01-bg font-tb01-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="submit" class="group relative isolate overflow-hidden cursor-pointer inline-grid grid-flow-col items-center gap-2.5 min-h-13 px-7 border-0 rounded-[0.9rem] text-base font-semibold tracking-[-0.01em] text-tb01-on-accent bg-[linear-gradient(180deg,var(--color-tb01-accent-lift),var(--color-tb01-accent))] transition-[translate,box-shadow,filter] duration-200 ease-tb01-out before:content-[''] before:absolute before:inset-0 before:-z-10 before:pointer-events-none before:-translate-x-[125%] before:bg-[linear-gradient(105deg,transparent_35%,rgba(255,255,255,0.42)_50%,transparent_65%)] before:transition-transform before:duration-[800ms] before:ease-tb01-out hover:-translate-y-0.5 hover:saturate-[1.06] hover:before:translate-x-[125%] focus-visible:before:translate-x-[125%] active:translate-y-px focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb01-ring disabled:cursor-not-allowed disabled:grayscale disabled:opacity-50 disabled:translate-y-0 disabled:shadow-none motion-reduce:transition-none motion-reduce:before:hidden shadow-[inset_0_1px_0_rgba(255,255,255,0.38),0_1px_2px_var(--color-tb01-shadow-soft),0_10px_24px_-8px_var(--color-tb01-shadow-wide)] active:shadow-[inset_0_2px_6px_var(--color-tb01-shadow-press)] focus-visible:shadow-[inset_0_1px_0_rgba(255,255,255,0.38),0_0_0_6px_var(--color-tb01-halo)]">
<span>Create account</span>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" class="w-[1.15rem] h-[1.15rem] transition-transform duration-300 ease-tb01-out group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0"><path d="M4 12h14"/><path d="M12.5 5.5 19 12l-6.5 6.5"/></svg>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb01-bg: #f2f1ee;
--color-tb01-bg: oklch(0.965 0.006 95);
--color-tb01-accent: #16a34a;
--color-tb01-accent: oklch(0.58 0.15 152);
--color-tb01-accent-lift: #2dbb60;
--color-tb01-accent-lift: oklch(0.645 0.148 152);
--color-tb01-on-accent: #ffffff;
--color-tb01-ring: #0f7538;
--color-tb01-ring: oklch(0.45 0.13 152);
--color-tb01-shadow-soft: #0b3d2061;
--color-tb01-shadow-soft: oklch(0.32 0.09 155 / 0.38);
--color-tb01-shadow-wide: #0b3d208c;
--color-tb01-shadow-wide: oklch(0.32 0.09 155 / 0.55);
--color-tb01-shadow-press: #0b3d2073;
--color-tb01-shadow-press: oklch(0.32 0.09 155 / 0.45);
--color-tb01-halo: #16a34a38;
--color-tb01-halo: oklch(0.58 0.15 152 / 0.22);
--font-tb01-sans: "Schibsted Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb01-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-01 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Dark scheme re-points the same tokens; @theme cannot hold a media query. */
@media (prefers-color-scheme: dark) {
.tb-01:not([data-theme="light"]) {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
--color-tb01-shadow-soft: oklch(0 0 0 / 0.5);
--color-tb01-shadow-wide: oklch(0 0 0 / 0.7);
}
}
.tb-01[data-theme="dark"] {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
}
@media (forced-colors: active) {
.tb-01 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-01 button::before {
display: none;
}
}
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Submit Button (Form Primary)
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-submit-button-form-primary/
The button every form ends with, ported: a two-layer elevation shadow that reads as physical depth, a masked sheen that sweeps once on hover, a real 1px press travel, and a focus ring that survives on any background. The port's win is that all four states now sit in one class list — hover:, active:, focus-visible: and disabled: read in the order a user meets them.
## HTML
```html
<div class="tb-01 w-full min-h-svh grid place-items-center bg-tb01-bg font-tb01-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="submit" class="group relative isolate overflow-hidden cursor-pointer inline-grid grid-flow-col items-center gap-2.5 min-h-13 px-7 border-0 rounded-[0.9rem] text-base font-semibold tracking-[-0.01em] text-tb01-on-accent bg-[linear-gradient(180deg,var(--color-tb01-accent-lift),var(--color-tb01-accent))] transition-[translate,box-shadow,filter] duration-200 ease-tb01-out before:content-[''] before:absolute before:inset-0 before:-z-10 before:pointer-events-none before:-translate-x-[125%] before:bg-[linear-gradient(105deg,transparent_35%,rgba(255,255,255,0.42)_50%,transparent_65%)] before:transition-transform before:duration-[800ms] before:ease-tb01-out hover:-translate-y-0.5 hover:saturate-[1.06] hover:before:translate-x-[125%] focus-visible:before:translate-x-[125%] active:translate-y-px focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb01-ring disabled:cursor-not-allowed disabled:grayscale disabled:opacity-50 disabled:translate-y-0 disabled:shadow-none motion-reduce:transition-none motion-reduce:before:hidden shadow-[inset_0_1px_0_rgba(255,255,255,0.38),0_1px_2px_var(--color-tb01-shadow-soft),0_10px_24px_-8px_var(--color-tb01-shadow-wide)] active:shadow-[inset_0_2px_6px_var(--color-tb01-shadow-press)] focus-visible:shadow-[inset_0_1px_0_rgba(255,255,255,0.38),0_0_0_6px_var(--color-tb01-halo)]">
<span>Create account</span>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" class="w-[1.15rem] h-[1.15rem] transition-transform duration-300 ease-tb01-out group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0"><path d="M4 12h14"/><path d="M12.5 5.5 19 12l-6.5 6.5"/></svg>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb01-bg: #f2f1ee;
--color-tb01-bg: oklch(0.965 0.006 95);
--color-tb01-accent: #16a34a;
--color-tb01-accent: oklch(0.58 0.15 152);
--color-tb01-accent-lift: #2dbb60;
--color-tb01-accent-lift: oklch(0.645 0.148 152);
--color-tb01-on-accent: #ffffff;
--color-tb01-ring: #0f7538;
--color-tb01-ring: oklch(0.45 0.13 152);
--color-tb01-shadow-soft: #0b3d2061;
--color-tb01-shadow-soft: oklch(0.32 0.09 155 / 0.38);
--color-tb01-shadow-wide: #0b3d208c;
--color-tb01-shadow-wide: oklch(0.32 0.09 155 / 0.55);
--color-tb01-shadow-press: #0b3d2073;
--color-tb01-shadow-press: oklch(0.32 0.09 155 / 0.45);
--color-tb01-halo: #16a34a38;
--color-tb01-halo: oklch(0.58 0.15 152 / 0.22);
--font-tb01-sans: "Schibsted Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb01-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-01 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
/* Dark scheme re-points the same tokens; @theme cannot hold a media query. */
@media (prefers-color-scheme: dark) {
.tb-01:not([data-theme="light"]) {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
--color-tb01-shadow-soft: oklch(0 0 0 / 0.5);
--color-tb01-shadow-wide: oklch(0 0 0 / 0.7);
}
}
.tb-01[data-theme="dark"] {
--color-tb01-bg: oklch(0.17 0.012 165);
--color-tb01-accent: oklch(0.72 0.17 152);
--color-tb01-accent-lift: oklch(0.78 0.15 152);
--color-tb01-on-accent: oklch(0.18 0.05 155);
}
@media (forced-colors: active) {
.tb-01 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
}
.tb-01 button::before {
display: none;
}
}
```How this works
The state machine moves into the markup. The CSS version spread four states across five rules, far from the element. Tailwind states them where the button is:
<button class="… hover:-translate-y-0.5 hover:saturate-[1.06]
active:translate-y-px active:shadow-[inset_0_2px_6px_…]
focus-visible:outline-2 focus-visible:outline-offset-[3px]
disabled:cursor-not-allowed disabled:grayscale disabled:opacity-50">Reading top to bottom you get the whole behaviour, and it is provably impossible for the hover style to acquire a partner the focus style never got.
The sheen stays a pseudo-element, but it is now three utilities on the button itself rather than a detached ::before rule: before:content-[''], before:absolute before:inset-0 before:-z-10, and the gradient as an arbitrary before:bg-[linear-gradient(105deg,…)]. The gradient is the one piece worth keeping arbitrary — it is a rendering recipe, not a design value.
Tokens become utilities. --color-tb01-accent in @theme mints bg-tb01-accent, outline-tb01-accent and the color-mix() targets in one entry, which is exactly the single-token re-theme the CSS version advertised.
Make it yours
- Re-theme through
--color-tb01-accentin@theme: fill, shadow tint, focus halo and sheen mix all read it. - Press feel:
active:translate-y-pxis app-like;active:translate-y-0.5reads as a chunky consumer CTA. - Delete the
<svg>and thegap-2.5collapses on its own — no other edit needed. - Full-width mobile submit: add
w-full sm:w-auto. The internal grid already centres its contents. rounded-fullfor a pill,rounded-mdfor a dense enterprise form.
Gotchas — read before shipping
- A bare
<button>inside a form defaults totype="submit". Tailwind changes nothing about that — always write the type explicitly. overflow-hiddenclips the sheen but would also clip an inset focus ring; keep the indicator onoutline-*(drawn outside the box) as this port does.disabled:only matches the realdisabledattribute. A[aria-disabled]button needsaria-disabled:variants instead, and still fires clicks.- Arbitrary shadows are long, so put them last in the class list; a diff that touches the shadow then touches one line, not the whole attribute.
before:content-['']is mandatory — without it the pseudo-element does not generate and the sheen silently never appears. This is the number-one Tailwind pseudo-element bug.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
The floor is Tailwind v4's own generated sheet (cascade layers, @property, color-mix()). Individual transform utilities, :focus-visible and multi-layer shadows are all baseline well below it.