40 / 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 Polaroid Snapshot Button
An instant photo mid-development: the image starts dark, flat and green-shifted, then blooms to full colour as you hover — a pure filter transition, exactly how the chemistry behaves. The frame tilts, lifts off the table, and the caption is hand-written on the wide bottom border.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-40 w-full min-h-svh grid place-items-center font-tb40-hand p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="group cursor-pointer border-0 bg-transparent p-0 [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-[6px] focus-visible:outline-tb40-accent disabled:cursor-not-allowed disabled:grayscale-[0.6]">
<span class="grid gap-2 w-56 pt-3 px-3 pb-1.5 rounded-[0.15rem] bg-[linear-gradient(170deg,var(--color-tb40-card),var(--color-tb40-card-lo))] shadow-[0_14px_24px_-14px_#000000b3,inset_0_0_0_1px_#0000000f] -rotate-[2.2deg] transition-[rotate,translate,box-shadow] duration-[450ms] ease-tb40-out group-hover:-rotate-[0.5deg] group-hover:-translate-y-1 group-hover:shadow-[0_22px_34px_-14px_#000000bf] group-focus-visible:-rotate-[0.5deg] group-focus-visible:-translate-y-1 group-active:-rotate-[1.6deg] group-active:translate-y-px motion-reduce:transition-none motion-reduce:group-hover:rotate-[-2.2deg] motion-reduce:group-hover:translate-y-0">
<span class="relative block aspect-square overflow-hidden bg-tb40-well">
<img src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=800&q=70&auto=format&fit=crop" alt="" width="800" height="800" loading="lazy" decoding="async" class="block size-full object-cover scale-[1.06] brightness-[0.34] contrast-[0.7] saturate-[0.32] hue-rotate-[28deg] blur-[1.2px] transition-[filter,scale] duration-[1600ms] ease-[cubic-bezier(0.3,0.7,0.3,1)] group-hover:scale-100 group-hover:brightness-100 group-hover:contrast-[1.04] group-hover:saturate-[1.06] group-hover:hue-rotate-0 group-hover:blur-none group-focus-visible:scale-100 group-focus-visible:brightness-100 group-focus-visible:contrast-[1.04] group-focus-visible:saturate-[1.06] group-focus-visible:hue-rotate-0 group-focus-visible:blur-none motion-reduce:transition-none motion-reduce:filter-none motion-reduce:scale-100">
<span aria-hidden="true" class="absolute inset-0 pointer-events-none shadow-[inset_0_0_2.2rem_#00000073,inset_0_0_0.3rem_#ffffff26]"></span>
</span>
<span class="px-1 pt-0.5 pb-1.5 text-xl leading-tight text-center text-tb40-ink">shake it once — open album</span>
</span>
</button>
</div><div class="tb-40 w-full min-h-svh grid place-items-center font-tb40-hand p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="group cursor-pointer border-0 bg-transparent p-0 [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-[6px] focus-visible:outline-tb40-accent disabled:cursor-not-allowed disabled:grayscale-[0.6]">
<span class="grid gap-2 w-56 pt-3 px-3 pb-1.5 rounded-[0.15rem] bg-[linear-gradient(170deg,var(--color-tb40-card),var(--color-tb40-card-lo))] shadow-[0_14px_24px_-14px_#000000b3,inset_0_0_0_1px_#0000000f] -rotate-[2.2deg] transition-[rotate,translate,box-shadow] duration-[450ms] ease-tb40-out group-hover:-rotate-[0.5deg] group-hover:-translate-y-1 group-hover:shadow-[0_22px_34px_-14px_#000000bf] group-focus-visible:-rotate-[0.5deg] group-focus-visible:-translate-y-1 group-active:-rotate-[1.6deg] group-active:translate-y-px motion-reduce:transition-none motion-reduce:group-hover:rotate-[-2.2deg] motion-reduce:group-hover:translate-y-0">
<span class="relative block aspect-square overflow-hidden bg-tb40-well">
<img src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=800&q=70&auto=format&fit=crop" alt="" width="800" height="800" loading="lazy" decoding="async" class="block size-full object-cover scale-[1.06] brightness-[0.34] contrast-[0.7] saturate-[0.32] hue-rotate-[28deg] blur-[1.2px] transition-[filter,scale] duration-[1600ms] ease-[cubic-bezier(0.3,0.7,0.3,1)] group-hover:scale-100 group-hover:brightness-100 group-hover:contrast-[1.04] group-hover:saturate-[1.06] group-hover:hue-rotate-0 group-hover:blur-none group-focus-visible:scale-100 group-focus-visible:brightness-100 group-focus-visible:contrast-[1.04] group-focus-visible:saturate-[1.06] group-focus-visible:hue-rotate-0 group-focus-visible:blur-none motion-reduce:transition-none motion-reduce:filter-none motion-reduce:scale-100">
<span aria-hidden="true" class="absolute inset-0 pointer-events-none shadow-[inset_0_0_2.2rem_#00000073,inset_0_0_0.3rem_#ffffff26]"></span>
</span>
<span class="px-1 pt-0.5 pb-1.5 text-xl leading-tight text-center text-tb40-ink">shake it once — open album</span>
</span>
</button>
</div>@import "tailwindcss";
@theme {
--color-tb40-bg: #2a2723;
--color-tb40-bg: oklch(0.27 0.012 70);
--color-tb40-card: #fbfaf5;
--color-tb40-card: oklch(0.98 0.006 90);
--color-tb40-card-lo: #e8e5dc;
--color-tb40-card-lo: oklch(0.92 0.008 90);
--color-tb40-ink: #2a2520;
--color-tb40-ink: oklch(0.26 0.012 70);
--color-tb40-well: #14161a;
--color-tb40-well: oklch(0.18 0.012 265);
--color-tb40-accent: #3b7ea1;
--color-tb40-accent: oklch(0.58 0.09 235);
--font-tb40-hand: "Caveat", ui-rounded, cursive;
--ease-tb40-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-40 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(34rem 22rem at 50% 40%, rgba(255,255,255,.06), transparent 72%), repeating-linear-gradient(94deg, rgba(0,0,0,.04) 0 2px, transparent 2px 12px), var(--color-tb40-bg);
}
@media (prefers-color-scheme: light) {
.tb-40:not([data-theme="dark"]) {
--color-tb40-bg: oklch(0.34 0.012 70);
}
}
.tb-40[data-theme="light"] {
--color-tb40-bg: oklch(0.34 0.012 70);
}
@media (forced-colors: active) {
.tb-40 button > span {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
rotate: 0deg;
}
.tb-40 button span:last-child {
color: ButtonText;
}
.tb-40 button span[aria-hidden] {
display: none;
}
}@import "tailwindcss";
@theme {
--color-tb40-bg: #2a2723;
--color-tb40-bg: oklch(0.27 0.012 70);
--color-tb40-card: #fbfaf5;
--color-tb40-card: oklch(0.98 0.006 90);
--color-tb40-card-lo: #e8e5dc;
--color-tb40-card-lo: oklch(0.92 0.008 90);
--color-tb40-ink: #2a2520;
--color-tb40-ink: oklch(0.26 0.012 70);
--color-tb40-well: #14161a;
--color-tb40-well: oklch(0.18 0.012 265);
--color-tb40-accent: #3b7ea1;
--color-tb40-accent: oklch(0.58 0.09 235);
--font-tb40-hand: "Caveat", ui-rounded, cursive;
--ease-tb40-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-40 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(34rem 22rem at 50% 40%, rgba(255,255,255,.06), transparent 72%), repeating-linear-gradient(94deg, rgba(0,0,0,.04) 0 2px, transparent 2px 12px), var(--color-tb40-bg);
}
@media (prefers-color-scheme: light) {
.tb-40:not([data-theme="dark"]) {
--color-tb40-bg: oklch(0.34 0.012 70);
}
}
.tb-40[data-theme="light"] {
--color-tb40-bg: oklch(0.34 0.012 70);
}
@media (forced-colors: active) {
.tb-40 button > span {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
rotate: 0deg;
}
.tb-40 button span:last-child {
color: ButtonText;
}
.tb-40 button span[aria-hidden] {
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-tb40-bg: #2a2723;
--color-tb40-bg: oklch(0.27 0.012 70);
--color-tb40-card: #fbfaf5;
--color-tb40-card: oklch(0.98 0.006 90);
--color-tb40-card-lo: #e8e5dc;
--color-tb40-card-lo: oklch(0.92 0.008 90);
--color-tb40-ink: #2a2520;
--color-tb40-ink: oklch(0.26 0.012 70);
--color-tb40-well: #14161a;
--color-tb40-well: oklch(0.18 0.012 265);
--color-tb40-accent: #3b7ea1;
--color-tb40-accent: oklch(0.58 0.09 235);
--font-tb40-hand: "Caveat", ui-rounded, cursive;
--ease-tb40-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-40 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(34rem 22rem at 50% 40%, rgba(255,255,255,.06), transparent 72%), repeating-linear-gradient(94deg, rgba(0,0,0,.04) 0 2px, transparent 2px 12px), var(--color-tb40-bg);
}
@media (prefers-color-scheme: light) {
.tb-40:not([data-theme="dark"]) {
--color-tb40-bg: oklch(0.34 0.012 70);
}
}
.tb-40[data-theme="light"] {
--color-tb40-bg: oklch(0.34 0.012 70);
}
@media (forced-colors: active) {
.tb-40 button > span {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
rotate: 0deg;
}
.tb-40 button span:last-child {
color: ButtonText;
}
.tb-40 button span[aria-hidden] {
display: none;
}
}@import "tailwindcss";
@theme {
--color-tb40-bg: #2a2723;
--color-tb40-bg: oklch(0.27 0.012 70);
--color-tb40-card: #fbfaf5;
--color-tb40-card: oklch(0.98 0.006 90);
--color-tb40-card-lo: #e8e5dc;
--color-tb40-card-lo: oklch(0.92 0.008 90);
--color-tb40-ink: #2a2520;
--color-tb40-ink: oklch(0.26 0.012 70);
--color-tb40-well: #14161a;
--color-tb40-well: oklch(0.18 0.012 265);
--color-tb40-accent: #3b7ea1;
--color-tb40-accent: oklch(0.58 0.09 235);
--font-tb40-hand: "Caveat", ui-rounded, cursive;
--ease-tb40-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-40 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(34rem 22rem at 50% 40%, rgba(255,255,255,.06), transparent 72%), repeating-linear-gradient(94deg, rgba(0,0,0,.04) 0 2px, transparent 2px 12px), var(--color-tb40-bg);
}
@media (prefers-color-scheme: light) {
.tb-40:not([data-theme="dark"]) {
--color-tb40-bg: oklch(0.34 0.012 70);
}
}
.tb-40[data-theme="light"] {
--color-tb40-bg: oklch(0.34 0.012 70);
}
@media (forced-colors: active) {
.tb-40 button > span {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
rotate: 0deg;
}
.tb-40 button span:last-child {
color: ButtonText;
}
.tb-40 button span[aria-hidden] {
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=Caveat:wght@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=Caveat:wght@600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Utilities: all of it — the five-term filter chain and its five-term reset, the tilt, the lift, the vignette, the frame's asymmetric padding. CSS: tokens and the stage's linen weave. Another zero-component-CSS port.
- This is the demo where a long class list is genuinely better than the CSS it compiles to.
filter: brightness(.34) contrast(.7) saturate(.32) hue-rotate(28deg) blur(1.2px)must be retyped in full to change one term; five utilities can each be edited alone, and the hover state is visibly the same five terms reset. - The frame is padding, not a border —
pt-3 px-3 pb-1.5on a grid. Getting that wrong is what makes most CSS Polaroids look like a card with a caption. motion-reduce:filter-noneis a single class replacing a whole reduced-motion block. The photo starts developed, the meaning survives, and no state is lost.
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 Polaroid Snapshot Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-polaroid-snapshot-button/
An instant photo mid-development: the image starts dark, flat and green-shifted, then blooms to full colour as you hover — a pure filter transition, exactly how the chemistry behaves. The frame tilts, lifts off the table, and the caption is hand-written on the wide bottom border.
## HTML
```html
<div class="tb-40 w-full min-h-svh grid place-items-center font-tb40-hand p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="group cursor-pointer border-0 bg-transparent p-0 [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-[6px] focus-visible:outline-tb40-accent disabled:cursor-not-allowed disabled:grayscale-[0.6]">
<span class="grid gap-2 w-56 pt-3 px-3 pb-1.5 rounded-[0.15rem] bg-[linear-gradient(170deg,var(--color-tb40-card),var(--color-tb40-card-lo))] shadow-[0_14px_24px_-14px_#000000b3,inset_0_0_0_1px_#0000000f] -rotate-[2.2deg] transition-[rotate,translate,box-shadow] duration-[450ms] ease-tb40-out group-hover:-rotate-[0.5deg] group-hover:-translate-y-1 group-hover:shadow-[0_22px_34px_-14px_#000000bf] group-focus-visible:-rotate-[0.5deg] group-focus-visible:-translate-y-1 group-active:-rotate-[1.6deg] group-active:translate-y-px motion-reduce:transition-none motion-reduce:group-hover:rotate-[-2.2deg] motion-reduce:group-hover:translate-y-0">
<span class="relative block aspect-square overflow-hidden bg-tb40-well">
<img src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=800&q=70&auto=format&fit=crop" alt="" width="800" height="800" loading="lazy" decoding="async" class="block size-full object-cover scale-[1.06] brightness-[0.34] contrast-[0.7] saturate-[0.32] hue-rotate-[28deg] blur-[1.2px] transition-[filter,scale] duration-[1600ms] ease-[cubic-bezier(0.3,0.7,0.3,1)] group-hover:scale-100 group-hover:brightness-100 group-hover:contrast-[1.04] group-hover:saturate-[1.06] group-hover:hue-rotate-0 group-hover:blur-none group-focus-visible:scale-100 group-focus-visible:brightness-100 group-focus-visible:contrast-[1.04] group-focus-visible:saturate-[1.06] group-focus-visible:hue-rotate-0 group-focus-visible:blur-none motion-reduce:transition-none motion-reduce:filter-none motion-reduce:scale-100">
<span aria-hidden="true" class="absolute inset-0 pointer-events-none shadow-[inset_0_0_2.2rem_#00000073,inset_0_0_0.3rem_#ffffff26]"></span>
</span>
<span class="px-1 pt-0.5 pb-1.5 text-xl leading-tight text-center text-tb40-ink">shake it once — open album</span>
</span>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb40-bg: #2a2723;
--color-tb40-bg: oklch(0.27 0.012 70);
--color-tb40-card: #fbfaf5;
--color-tb40-card: oklch(0.98 0.006 90);
--color-tb40-card-lo: #e8e5dc;
--color-tb40-card-lo: oklch(0.92 0.008 90);
--color-tb40-ink: #2a2520;
--color-tb40-ink: oklch(0.26 0.012 70);
--color-tb40-well: #14161a;
--color-tb40-well: oklch(0.18 0.012 265);
--color-tb40-accent: #3b7ea1;
--color-tb40-accent: oklch(0.58 0.09 235);
--font-tb40-hand: "Caveat", ui-rounded, cursive;
--ease-tb40-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-40 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(34rem 22rem at 50% 40%, rgba(255,255,255,.06), transparent 72%), repeating-linear-gradient(94deg, rgba(0,0,0,.04) 0 2px, transparent 2px 12px), var(--color-tb40-bg);
}
@media (prefers-color-scheme: light) {
.tb-40:not([data-theme="dark"]) {
--color-tb40-bg: oklch(0.34 0.012 70);
}
}
.tb-40[data-theme="light"] {
--color-tb40-bg: oklch(0.34 0.012 70);
}
@media (forced-colors: active) {
.tb-40 button > span {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
rotate: 0deg;
}
.tb-40 button span:last-child {
color: ButtonText;
}
.tb-40 button span[aria-hidden] {
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 Polaroid Snapshot Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-polaroid-snapshot-button/
An instant photo mid-development: the image starts dark, flat and green-shifted, then blooms to full colour as you hover — a pure filter transition, exactly how the chemistry behaves. The frame tilts, lifts off the table, and the caption is hand-written on the wide bottom border.
## HTML
```html
<div class="tb-40 w-full min-h-svh grid place-items-center font-tb40-hand p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="group cursor-pointer border-0 bg-transparent p-0 [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-[6px] focus-visible:outline-tb40-accent disabled:cursor-not-allowed disabled:grayscale-[0.6]">
<span class="grid gap-2 w-56 pt-3 px-3 pb-1.5 rounded-[0.15rem] bg-[linear-gradient(170deg,var(--color-tb40-card),var(--color-tb40-card-lo))] shadow-[0_14px_24px_-14px_#000000b3,inset_0_0_0_1px_#0000000f] -rotate-[2.2deg] transition-[rotate,translate,box-shadow] duration-[450ms] ease-tb40-out group-hover:-rotate-[0.5deg] group-hover:-translate-y-1 group-hover:shadow-[0_22px_34px_-14px_#000000bf] group-focus-visible:-rotate-[0.5deg] group-focus-visible:-translate-y-1 group-active:-rotate-[1.6deg] group-active:translate-y-px motion-reduce:transition-none motion-reduce:group-hover:rotate-[-2.2deg] motion-reduce:group-hover:translate-y-0">
<span class="relative block aspect-square overflow-hidden bg-tb40-well">
<img src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=800&q=70&auto=format&fit=crop" alt="" width="800" height="800" loading="lazy" decoding="async" class="block size-full object-cover scale-[1.06] brightness-[0.34] contrast-[0.7] saturate-[0.32] hue-rotate-[28deg] blur-[1.2px] transition-[filter,scale] duration-[1600ms] ease-[cubic-bezier(0.3,0.7,0.3,1)] group-hover:scale-100 group-hover:brightness-100 group-hover:contrast-[1.04] group-hover:saturate-[1.06] group-hover:hue-rotate-0 group-hover:blur-none group-focus-visible:scale-100 group-focus-visible:brightness-100 group-focus-visible:contrast-[1.04] group-focus-visible:saturate-[1.06] group-focus-visible:hue-rotate-0 group-focus-visible:blur-none motion-reduce:transition-none motion-reduce:filter-none motion-reduce:scale-100">
<span aria-hidden="true" class="absolute inset-0 pointer-events-none shadow-[inset_0_0_2.2rem_#00000073,inset_0_0_0.3rem_#ffffff26]"></span>
</span>
<span class="px-1 pt-0.5 pb-1.5 text-xl leading-tight text-center text-tb40-ink">shake it once — open album</span>
</span>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb40-bg: #2a2723;
--color-tb40-bg: oklch(0.27 0.012 70);
--color-tb40-card: #fbfaf5;
--color-tb40-card: oklch(0.98 0.006 90);
--color-tb40-card-lo: #e8e5dc;
--color-tb40-card-lo: oklch(0.92 0.008 90);
--color-tb40-ink: #2a2520;
--color-tb40-ink: oklch(0.26 0.012 70);
--color-tb40-well: #14161a;
--color-tb40-well: oklch(0.18 0.012 265);
--color-tb40-accent: #3b7ea1;
--color-tb40-accent: oklch(0.58 0.09 235);
--font-tb40-hand: "Caveat", ui-rounded, cursive;
--ease-tb40-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-40 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(34rem 22rem at 50% 40%, rgba(255,255,255,.06), transparent 72%), repeating-linear-gradient(94deg, rgba(0,0,0,.04) 0 2px, transparent 2px 12px), var(--color-tb40-bg);
}
@media (prefers-color-scheme: light) {
.tb-40:not([data-theme="dark"]) {
--color-tb40-bg: oklch(0.34 0.012 70);
}
}
.tb-40[data-theme="light"] {
--color-tb40-bg: oklch(0.34 0.012 70);
}
@media (forced-colors: active) {
.tb-40 button > span {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
rotate: 0deg;
}
.tb-40 button span:last-child {
color: ButtonText;
}
.tb-40 button span[aria-hidden] {
display: none;
}
}
```How this works
v4 composes filter utilities. Five separate classes become one filter chain, and the hover state resets them individually:
class="brightness-[0.34] contrast-[0.7] saturate-[0.32] hue-rotate-[28deg] blur-[1.2px]
group-hover:brightness-100 group-hover:contrast-[1.04]
group-hover:saturate-[1.06] group-hover:hue-rotate-0 group-hover:blur-none"In CSS this was two long filter: declarations where changing one term meant retyping all five. Here each term is independently editable — the rare case where a long class list is strictly more maintainable than the property it compiles to.
The hue rotation is the authentic part. Instant film passes through a green cast before it settles; without hue-rotate the reveal reads as a brightness slider.
Nothing is duplicated. One image, no second copy, no JS — and under reduced motion the photo is simply already developed.
Make it yours
- Development speed: 1600ms reads as vintage film, 400ms as a digital preview.
hue-rotate-[28deg]is instant film;-20degreads as a cyanotype.- The bottom border has room for two caption lines at 1.125rem.
- Start developed and use hover for a zoom instead, if the reveal is too playful.
- The vignette is an inset shadow — deepen it for older film.
Gotchas — read before shipping
- A photo unreadable until hover is an accessibility problem if the image carries information. Here it is decorative and the button's label is text.
- Animating
filteris GPU-bound but not free. One large image is fine; a grid of twelve is not. blur-*inside a filter chain enlarges the paint area — keepoverflow-hiddenon the window or edges bleed.- Never put the accessible name only in the caption.
- Always set
width/heighton the<img>or the frame reflows when the photo lands.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
CSS filter functions and their transitions work in every modern engine; individual rotate/scale utilities are well below Tailwind v4's floor.