Pure TailwindMIT licensed

Tailwind Neumorphic Image Card Hover Effect

Soft UI is two shadows going in opposite directions, and the port's win is that they become tokens: --shadow-tih08-out and --shadow-tih08-in in @theme mint shadow-tih08-out and shadow-tih08-in, so the raised and pressed states are named rather than re-typed as eight-value strings on every card and button.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tih-08 w-full min-h-svh grid place-items-center bg-tih08-nm font-tih08-sans px-6 py-10" aria-label="Neumorphic image card hover demo">
  <div class="grid w-full max-w-195 gap-6 grid-cols-1 sm:grid-cols-3">
    <article class="group p-3 rounded-[20px] bg-tih08-nm shadow-tih08-out transition-[box-shadow,scale] duration-400 ease-tih08 hover:shadow-tih08-in hover:scale-98 motion-reduce:transition-none">
      <div class="relative aspect-square overflow-hidden rounded-xl shadow-tih08-well">
        <div class="absolute inset-0 grid place-items-center bg-[image:url('https://picsum.photos/seed/tih08-controller/600/600'),linear-gradient(135deg,oklch(0.22_0.09_285),oklch(0.31_0.14_280),oklch(0.45_0.19_275))] bg-cover bg-center bg-no-repeat transition-transform duration-400 ease-tih08 group-hover:scale-104 motion-reduce:transition-none motion-reduce:group-hover:scale-100">
        </div>
      </div>
      <div class="pt-3 px-1">
        <h3 class="mb-0.75 text-[13px] font-bold text-tih08-text">Phantom Controller</h3>
        <p class="text-[11px] text-tih08-muted">Haptic &middot; Wireless &middot; 40h</p>
        <div class="flex items-center justify-between mt-2.5">
          <span class="text-[15px] font-bold text-tih08-acc tabular-nums">$89</span>
          <button type="button" class="px-3 py-1.25 rounded-full text-[10px] font-bold tracking-[0.06em] text-tih08-text bg-tih08-nm shadow-tih08-btn cursor-pointer transition-shadow duration-200 hover:shadow-tih08-in focus-visible:outline-2 focus-visible:outline-tih08-acc focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
        </div>
      </div>
    </article>
    <article class="group p-3 rounded-[20px] bg-tih08-nm shadow-tih08-out transition-[box-shadow,scale] duration-400 ease-tih08 hover:shadow-tih08-in hover:scale-98 motion-reduce:transition-none">
      <div class="relative aspect-square overflow-hidden rounded-xl shadow-tih08-well">
        <div class="absolute inset-0 grid place-items-center bg-[image:url('https://picsum.photos/seed/tih08-headphones/600/600'),linear-gradient(135deg,oklch(0.29_0.07_240),oklch(0.38_0.1_238),oklch(0.75_0.12_230))] bg-cover bg-center bg-no-repeat transition-transform duration-400 ease-tih08 group-hover:scale-104 motion-reduce:transition-none motion-reduce:group-hover:scale-100">
        </div>
      </div>
      <div class="pt-3 px-1">
        <h3 class="mb-0.75 text-[13px] font-bold text-tih08-text">AquaTone Studio</h3>
        <p class="text-[11px] text-tih08-muted">ANC &middot; Hi-Res &middot; 60h</p>
        <div class="flex items-center justify-between mt-2.5">
          <span class="text-[15px] font-bold text-tih08-acc tabular-nums">$229</span>
          <button type="button" class="px-3 py-1.25 rounded-full text-[10px] font-bold tracking-[0.06em] text-tih08-text bg-tih08-nm shadow-tih08-btn cursor-pointer transition-shadow duration-200 hover:shadow-tih08-in focus-visible:outline-2 focus-visible:outline-tih08-acc focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
        </div>
      </div>
    </article>
    <article class="group p-3 rounded-[20px] bg-tih08-nm shadow-tih08-out transition-[box-shadow,scale] duration-400 ease-tih08 hover:shadow-tih08-in hover:scale-98 motion-reduce:transition-none">
      <div class="relative aspect-square overflow-hidden rounded-xl shadow-tih08-well">
        <div class="absolute inset-0 grid place-items-center bg-[image:url('https://picsum.photos/seed/tih08-laptop/600/600'),linear-gradient(135deg,oklch(0.25_0.13_305),oklch(0.4_0.2_300),oklch(0.63_0.21_300))] bg-cover bg-center bg-no-repeat transition-transform duration-400 ease-tih08 group-hover:scale-104 motion-reduce:transition-none motion-reduce:group-hover:scale-100">
        </div>
      </div>
      <div class="pt-3 px-1">
        <h3 class="mb-0.75 text-[13px] font-bold text-tih08-text">NovaPad Ultra</h3>
        <p class="text-[11px] text-tih08-muted">OLED &middot; 12-core &middot; 18h</p>
        <div class="flex items-center justify-between mt-2.5">
          <span class="text-[15px] font-bold text-tih08-acc tabular-nums">$1,299</span>
          <button type="button" class="px-3 py-1.25 rounded-full text-[10px] font-bold tracking-[0.06em] text-tih08-text bg-tih08-nm shadow-tih08-btn cursor-pointer transition-shadow duration-200 hover:shadow-tih08-in focus-visible:outline-2 focus-visible:outline-tih08-acc focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
        </div>
      </div>
    </article>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tih08-nm: #1e1e2a;
  --color-tih08-nm: oklch(0.22 0.02 285);
  --color-tih08-text: #e2e8f0;
  --color-tih08-text: oklch(0.92 0.01 250);
  --color-tih08-muted: #64748b;
  --color-tih08-muted: oklch(0.6 0.035 255);
  --color-tih08-acc: #6366f1;
  --color-tih08-acc: oklch(0.62 0.2 275);
  --font-tih08-sans: 'Space Grotesk', system-ui, sans-serif;
  --ease-tih08: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  --shadow-tih08-out: 6px 6px 16px oklch(0 0 0 / 0.45), -4px -4px 12px oklch(1 0 0 / 0.05);
  --shadow-tih08-in: inset 4px 4px 10px oklch(0 0 0 / 0.45), inset -3px -3px 8px oklch(1 0 0 / 0.05);
  --shadow-tih08-well: inset 2px 2px 6px oklch(0 0 0 / 0.45), inset -1px -1px 4px oklch(1 0 0 / 0.05);
  --shadow-tih08-btn: 3px 3px 8px oklch(0 0 0 / 0.45), -2px -2px 6px oklch(1 0 0 / 0.05);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the stage is correctly sized before the browser build compiles. */

.tih-08 {
  width: 100%;
  min-height: 100svh;
}

.tih-08 ::selection {
  background: oklch(0.62 0.2 275);
  color: #fff;
}
/* Safari inset-shadow-against-radius stability fix: a zero-alpha hairline border.
   No utility expresses "border that exists only to stabilise a repaint". */

.tih-08 [class*="rounded-"] {
  border: 1px solid transparent;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tih08-nm: #1e1e2a;
  --color-tih08-nm: oklch(0.22 0.02 285);
  --color-tih08-text: #e2e8f0;
  --color-tih08-text: oklch(0.92 0.01 250);
  --color-tih08-muted: #64748b;
  --color-tih08-muted: oklch(0.6 0.035 255);
  --color-tih08-acc: #6366f1;
  --color-tih08-acc: oklch(0.62 0.2 275);
  --font-tih08-sans: 'Space Grotesk', system-ui, sans-serif;
  --ease-tih08: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  --shadow-tih08-out: 6px 6px 16px oklch(0 0 0 / 0.45), -4px -4px 12px oklch(1 0 0 / 0.05);
  --shadow-tih08-in: inset 4px 4px 10px oklch(0 0 0 / 0.45), inset -3px -3px 8px oklch(1 0 0 / 0.05);
  --shadow-tih08-well: inset 2px 2px 6px oklch(0 0 0 / 0.45), inset -1px -1px 4px oklch(1 0 0 / 0.05);
  --shadow-tih08-btn: 3px 3px 8px oklch(0 0 0 / 0.45), -2px -2px 6px oklch(1 0 0 / 0.05);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the stage is correctly sized before the browser build compiles. */

.tih-08 {
  width: 100%;
  min-height: 100svh;
}

.tih-08 ::selection {
  background: oklch(0.62 0.2 275);
  color: #fff;
}
/* Safari inset-shadow-against-radius stability fix: a zero-alpha hairline border.
   No utility expresses "border that exists only to stabilise a repaint". */

.tih-08 [class*="rounded-"] {
  border: 1px solid transparent;
}
3Fonts used by this demooptional — skip if you have your own type stack
<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=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • Four shadow strings become four @theme entries and therefore four utilities. This is the collection's best argument for @theme beyond colour: hover:shadow-tih08-in says what it means, and an eight-value arbitrary string does not.
  • The page and card share bg-tih08-nm, so the neumorphic illusion's single hard requirement is enforced by the token system rather than by discipline.
  • transition-[box-shadow,scale] replaces the CSS version's box-shadow, transform pair — v4's scale-* compiles to the individual scale property, so that is the property to name.
  • The button's raised/pressed pair reuses two of the same tokens, so a light-direction change propagates to it for free.
  • The focus-visible: outline is added by the port. Soft UI has no contrast affordance at all, and shipping it without a focus ring is the actual accessibility failure — not the shadows.
  • Only the root rule, ::selection and the Safari hairline shim remain in CSS; the shim is a bug workaround, which is exactly the category that should not be a utility.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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 Neumorphic Image Card Hover Effect
Source: https://codefronts.com/motion/tailwind-css-image-hover-effects/tailwind-neumorphic-image-card-hover-effect/

Soft UI is two shadows going in opposite directions, and the port's win is that they become tokens: --shadow-tih08-out and --shadow-tih08-in in @theme mint shadow-tih08-out and shadow-tih08-in, so the raised and pressed states are named rather than re-typed as eight-value strings on every card and button.
## HTML
```html
<section class="tih-08 w-full min-h-svh grid place-items-center bg-tih08-nm font-tih08-sans px-6 py-10" aria-label="Neumorphic image card hover demo">
  <div class="grid w-full max-w-195 gap-6 grid-cols-1 sm:grid-cols-3">
    <article class="group p-3 rounded-[20px] bg-tih08-nm shadow-tih08-out transition-[box-shadow,scale] duration-400 ease-tih08 hover:shadow-tih08-in hover:scale-98 motion-reduce:transition-none">
      <div class="relative aspect-square overflow-hidden rounded-xl shadow-tih08-well">
        <div class="absolute inset-0 grid place-items-center bg-[image:url('https://picsum.photos/seed/tih08-controller/600/600'),linear-gradient(135deg,oklch(0.22_0.09_285),oklch(0.31_0.14_280),oklch(0.45_0.19_275))] bg-cover bg-center bg-no-repeat transition-transform duration-400 ease-tih08 group-hover:scale-104 motion-reduce:transition-none motion-reduce:group-hover:scale-100">
        </div>
      </div>
      <div class="pt-3 px-1">
        <h3 class="mb-0.75 text-[13px] font-bold text-tih08-text">Phantom Controller</h3>
        <p class="text-[11px] text-tih08-muted">Haptic &middot; Wireless &middot; 40h</p>
        <div class="flex items-center justify-between mt-2.5">
          <span class="text-[15px] font-bold text-tih08-acc tabular-nums">$89</span>
          <button type="button" class="px-3 py-1.25 rounded-full text-[10px] font-bold tracking-[0.06em] text-tih08-text bg-tih08-nm shadow-tih08-btn cursor-pointer transition-shadow duration-200 hover:shadow-tih08-in focus-visible:outline-2 focus-visible:outline-tih08-acc focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
        </div>
      </div>
    </article>
    <article class="group p-3 rounded-[20px] bg-tih08-nm shadow-tih08-out transition-[box-shadow,scale] duration-400 ease-tih08 hover:shadow-tih08-in hover:scale-98 motion-reduce:transition-none">
      <div class="relative aspect-square overflow-hidden rounded-xl shadow-tih08-well">
        <div class="absolute inset-0 grid place-items-center bg-[image:url('https://picsum.photos/seed/tih08-headphones/600/600'),linear-gradient(135deg,oklch(0.29_0.07_240),oklch(0.38_0.1_238),oklch(0.75_0.12_230))] bg-cover bg-center bg-no-repeat transition-transform duration-400 ease-tih08 group-hover:scale-104 motion-reduce:transition-none motion-reduce:group-hover:scale-100">
        </div>
      </div>
      <div class="pt-3 px-1">
        <h3 class="mb-0.75 text-[13px] font-bold text-tih08-text">AquaTone Studio</h3>
        <p class="text-[11px] text-tih08-muted">ANC &middot; Hi-Res &middot; 60h</p>
        <div class="flex items-center justify-between mt-2.5">
          <span class="text-[15px] font-bold text-tih08-acc tabular-nums">$229</span>
          <button type="button" class="px-3 py-1.25 rounded-full text-[10px] font-bold tracking-[0.06em] text-tih08-text bg-tih08-nm shadow-tih08-btn cursor-pointer transition-shadow duration-200 hover:shadow-tih08-in focus-visible:outline-2 focus-visible:outline-tih08-acc focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
        </div>
      </div>
    </article>
    <article class="group p-3 rounded-[20px] bg-tih08-nm shadow-tih08-out transition-[box-shadow,scale] duration-400 ease-tih08 hover:shadow-tih08-in hover:scale-98 motion-reduce:transition-none">
      <div class="relative aspect-square overflow-hidden rounded-xl shadow-tih08-well">
        <div class="absolute inset-0 grid place-items-center bg-[image:url('https://picsum.photos/seed/tih08-laptop/600/600'),linear-gradient(135deg,oklch(0.25_0.13_305),oklch(0.4_0.2_300),oklch(0.63_0.21_300))] bg-cover bg-center bg-no-repeat transition-transform duration-400 ease-tih08 group-hover:scale-104 motion-reduce:transition-none motion-reduce:group-hover:scale-100">
        </div>
      </div>
      <div class="pt-3 px-1">
        <h3 class="mb-0.75 text-[13px] font-bold text-tih08-text">NovaPad Ultra</h3>
        <p class="text-[11px] text-tih08-muted">OLED &middot; 12-core &middot; 18h</p>
        <div class="flex items-center justify-between mt-2.5">
          <span class="text-[15px] font-bold text-tih08-acc tabular-nums">$1,299</span>
          <button type="button" class="px-3 py-1.25 rounded-full text-[10px] font-bold tracking-[0.06em] text-tih08-text bg-tih08-nm shadow-tih08-btn cursor-pointer transition-shadow duration-200 hover:shadow-tih08-in focus-visible:outline-2 focus-visible:outline-tih08-acc focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
        </div>
      </div>
    </article>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tih08-nm: #1e1e2a;
  --color-tih08-nm: oklch(0.22 0.02 285);
  --color-tih08-text: #e2e8f0;
  --color-tih08-text: oklch(0.92 0.01 250);
  --color-tih08-muted: #64748b;
  --color-tih08-muted: oklch(0.6 0.035 255);
  --color-tih08-acc: #6366f1;
  --color-tih08-acc: oklch(0.62 0.2 275);
  --font-tih08-sans: 'Space Grotesk', system-ui, sans-serif;
  --ease-tih08: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  --shadow-tih08-out: 6px 6px 16px oklch(0 0 0 / 0.45), -4px -4px 12px oklch(1 0 0 / 0.05);
  --shadow-tih08-in: inset 4px 4px 10px oklch(0 0 0 / 0.45), inset -3px -3px 8px oklch(1 0 0 / 0.05);
  --shadow-tih08-well: inset 2px 2px 6px oklch(0 0 0 / 0.45), inset -1px -1px 4px oklch(1 0 0 / 0.05);
  --shadow-tih08-btn: 3px 3px 8px oklch(0 0 0 / 0.45), -2px -2px 6px oklch(1 0 0 / 0.05);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the stage is correctly sized before the browser build compiles. */

.tih-08 {
  width: 100%;
  min-height: 100svh;
}

.tih-08 ::selection {
  background: oklch(0.62 0.2 275);
  color: #fff;
}
/* Safari inset-shadow-against-radius stability fix: a zero-alpha hairline border.
   No utility expresses "border that exists only to stabilise a repaint". */

.tih-08 [class*="rounded-"] {
  border: 1px solid transparent;
}
```

How this works

Shadows as design tokens. This is the one demo where @theme earns its keep beyond colour:

@theme {
  --shadow-tih08-out: 6px 6px 16px oklch(0 0 0/0.45), -4px -4px 12px oklch(1 0 0/0.05);
  --shadow-tih08-in: inset 4px 4px 10px oklch(0 0 0/0.45), inset -3px -3px 8px oklch(1 0 0/0.05);
}

The markup then reads shadow-tih08-out hover:shadow-tih08-in. An arbitrary value would work, but the eight-value string would appear five times and the light direction would eventually drift on one of them.

The illusion depends on one colour. The card background and the page background are the same token, so there is no mismatch to introduce. This is the failure mode of every neumorphic component library, and a shared token removes it.

Press is shadow plus scale. hover:scale-98 is a real 2% shrink on the compositor, which is what makes the inset shadow read as a push rather than a repaint.

Make it yours

  • Light direction: flip the signs inside the two shadow tokens — both states update together, which is the point of tokenising them.
  • Depth: lower the dark alpha to 0.3 for lighter surfaces. Edit the token, not the markup.
  • Spring press: ease-[cubic-bezier(0.34,1.56,0.64,1)] for a tactile overshoot.
  • Coloured neumorphism: tint the light shadow with a desaturated surface hue instead of white.
  • Retheme through --color-tih08-nm — and remember to keep the section and the card on that one token.

Gotchas — read before shipping

  • The card background MUST equal the page background. Two tokens is one token too many here.
  • Neumorphism has no border and no contrast affordance; it fails WCAG on its own. The focus-visible: outline and the text labels are doing the accessibility work, and they are not optional.
  • Multi-value shadows in @theme need commas and no newlines in the value — a wrapped token value silently fails to parse.
  • hover:scale-98 on a card containing a button means the button scales too. Keep the press subtle or the hit target moves under the cursor.
  • Safari below 14 can clip inset shadows against rounded-*; a 0-alpha 1px border is the stability fix, and it stays in the stylesheet because no utility expresses that intent.
  • The browser CDN build compiles in the page and is for prototyping only. Ship the CLI or a plugin build.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

box-shadow with inset is universally supported; the visual result depends on display calibration more than on engine.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…