Production-ready components using modern CSS and semantic HTML.
Exact tile button pattern with nested structure and staggered timing.
Behavior:
- Default: outer shell raised, inner flat
- Hover: outer flattens FIRST (immediate), inner becomes pressed SECOND (150ms delay)
- Unhover: reverse sequence (inner releases first, outer raises with delay)
- Text/icon moves down 2.4px on hover
<!-- Primary button with icon -->
<a href="#contact" class="btn-tile btn-tile--primary">
<span class="btn-tile__inner">
<i class="fa-solid fa-arrow-right" aria-hidden="true"></i>
<span>Get in touch</span>
</span>
</a>
<!-- Ghost button (secondary action) -->
<a href="#work" class="btn-tile btn-tile--ghost">
<span class="btn-tile__inner">
<span>View work</span>
</span>
</a>
<!-- Submit button -->
<button type="submit" class="btn-tile btn-tile--primary">
<span class="btn-tile__inner">
<i class="fa-solid fa-paper-plane" aria-hidden="true"></i>
<span>Send message</span>
</span>
</button>/* Outer shell */
.btn-tile {
--depth: 1;
position: relative;
display: inline-flex;
padding: 0.1rem;
border-radius: var(--radius-sm);
background: var(--silver-100);
text-decoration: none;
cursor: pointer;
border: none;
font: inherit;
/* Raised state */
box-shadow:
var(--shadow-source)
calc(0.33rem * var(--depth))
calc(0.33rem * var(--depth))
0.6rem;
/* Return animation: 150ms delay before transition */
transition: box-shadow 0.15s 0.15s ease-in;
}
.btn-tile::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
box-shadow:
var(--light-source)
calc(-0.33rem * var(--depth))
calc(-0.33rem * var(--depth))
0.6rem;
transition: box-shadow 0.15s 0.15s ease-in;
}
/* Inner tile - starts FLAT */
.btn-tile__inner {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-6);
min-block-size: var(--min-touch-target);
border-radius: calc(var(--radius-sm) - 2px);
background: var(--silver-100);
color: var(--ink-900);
font-weight: 500;
/* No shadow initially */
box-shadow: inset var(--shadow-source) 0 0 0;
transition: box-shadow 0.15s ease-in;
}
.btn-tile__inner::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
box-shadow: inset var(--light-source) 0 0 0;
transition: box-shadow 0.15s ease-in;
}
/* Text/icon transition */
.btn-tile__inner span,
.btn-tile__inner i {
transition: transform 0.3s ease-in-out;
}
/* HOVER: Outer flattens immediately */
.btn-tile:hover {
box-shadow: var(--shadow-source) 0 0 0;
transition: box-shadow 0.15s ease-out;
}
.btn-tile:hover::before {
box-shadow: var(--light-source) 0 0 0;
transition: box-shadow 0.15s ease-out;
}
/* HOVER: Inner becomes pressed (150ms delay) */
.btn-tile:hover .btn-tile__inner {
box-shadow:
inset var(--shadow-source)
calc(0.25rem * var(--depth))
calc(0.25rem * var(--depth))
0.6rem;
transition: box-shadow 0.15s 0.15s ease-out;
}
.btn-tile:hover .btn-tile__inner::before {
box-shadow:
inset var(--light-source)
calc(-0.25rem * var(--depth))
calc(-0.25rem * var(--depth))
0.6rem;
transition: box-shadow 0.15s 0.15s ease-out;
}
/* Text moves down on press */
.btn-tile:hover .btn-tile__inner span,
.btn-tile:hover .btn-tile__inner i {
transform: translateY(0.1516rem); /* 2.4px */
}
.btn-tile:focus-visible {
outline: 2px solid var(--signal-focus);
outline-offset: 2px;
}
/* Primary: dark inner */
.btn-tile--primary .btn-tile__inner {
background: var(--ink-900);
color: var(--white);
}
/* Ghost: transparent, no shadows */
.btn-tile--ghost {
background: transparent;
box-shadow: none;
padding: 0;
}
.btn-tile--ghost::before {
display: none;
}
.btn-tile--ghost .btn-tile__inner {
background: transparent;
box-shadow: none;
}
.btn-tile--ghost .btn-tile__inner::before {
display: none;
}
.btn-tile--ghost:hover {
box-shadow: none;
}
.btn-tile--ghost:hover .btn-tile__inner {
background: var(--silver-200);
box-shadow: none;
}Neumorphic inset input field.
<div class="input-group">
<label for="email" class="input-label">Email address</label>
<input
type="email"
id="email"
class="input"
placeholder="you@example.com"
required
>
<span class="input-error" aria-live="polite"></span>
</div>.input-group {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.input-label {
font-size: var(--text-sm);
font-weight: 500;
color: var(--ink-700);
}
.input {
/* Reset */
appearance: none;
border: none;
font: inherit;
/* Base */
--depth: 2;
position: relative;
padding: var(--space-3) var(--space-4);
min-block-size: var(--min-touch-target);
border: 1px solid var(--silver-400);
border-radius: var(--radius-sm);
background: var(--silver-100);
color: var(--ink-900);
/* Neumorphic inset */
box-shadow:
inset var(--shadow-source)
calc(0.25rem * var(--depth))
calc(0.25rem * var(--depth))
0.6rem;
transition:
border-color var(--duration-instant),
box-shadow var(--duration-instant);
}
.input::placeholder {
color: var(--ink-500);
}
/* Focus */
.input:focus {
outline: none;
border-color: var(--signal-focus);
box-shadow:
inset var(--shadow-source) 2px 2px 4px,
0 0 0 2px oklch(45% 0.2 260 / 0.2);
}
/* Invalid (only when not empty) */
.input:invalid:not(:placeholder-shown) {
border-color: var(--signal-error);
}
/* Error message */
.input-error {
font-size: var(--text-sm);
color: var(--signal-error);
min-block-size: 1.25em;
}
/* Textarea variant */
textarea.input {
min-block-size: 8rem;
resize: vertical;
}Elevated container with optional interactivity.
<article class="card">
<header class="card__header">
<h3 class="card__title">Card Title</h3>
</header>
<div class="card__body">
<p>Card content goes here.</p>
</div>
<footer class="card__footer">
<button class="btn btn--ghost">Cancel</button>
<button class="btn btn--primary">Confirm</button>
</footer>
</article>
<!-- Interactive card (link) -->
<a href="/details" class="card card--interactive">
<div class="card__body">
<h3 class="card__title">Clickable Card</h3>
<p>This entire card is a link.</p>
</div>
</a>.card {
--depth: 2;
position: relative;
padding: var(--space-6);
border-radius: var(--radius-md);
background: var(--white);
/* Neumorphic raised */
box-shadow:
var(--shadow-source)
calc(0.33rem * var(--depth))
calc(0.33rem * var(--depth))
0.6rem;
}
.card::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
box-shadow:
var(--light-source)
calc(-0.33rem * var(--depth))
calc(-0.33rem * var(--depth))
0.6rem;
}
.card__header {
margin-block-end: var(--space-4);
}
.card__title {
font-size: var(--text-lg);
font-weight: 600;
color: var(--ink-900);
margin: 0;
}
.card__body {
color: var(--ink-700);
}
.card__body > * + * {
margin-block-start: var(--space-2);
}
.card__footer {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
margin-block-start: var(--space-6);
padding-block-start: var(--space-4);
border-block-start: 1px solid var(--silver-200);
}
/* Interactive variant */
.card--interactive {
cursor: pointer;
text-decoration: none;
color: inherit;
transition: transform var(--duration-quick) var(--ease-subtle);
}
.card--interactive:hover {
--depth: 4;
transform: translateY(-2px);
}
.card--interactive:focus-visible {
outline: 2px solid var(--signal-focus);
outline-offset: 2px;
}Neumorphic tiles with up/down/button states (from original design).
<div class="tile up">Up</div>
<div class="tile down">Down</div>
<div class="tile padded up">
<div class="tile down">Up & Down</div>
</div>
<div class="tile button">
<div class="tile"><span>Button</span></div>
</div>
<!-- Circular variants -->
<div class="tile tile--circle up">Up</div>
<div class="tile tile--circle down">Down</div>
<div class="tile tile--circle button"><span>Button</span></div>.tile {
--depth: 1;
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 6rem;
height: 6rem;
border-radius: var(--radius-md);
background: var(--silver-100);
font-family: var(--font-display);
text-transform: uppercase;
text-align: center;
line-height: 0.9;
color: var(--ink-700);
}
.tile::before,
.tile::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
}
.tile--circle {
border-radius: 50%;
}
.tile--circle::before {
border-radius: 50%;
}
.padded {
padding: 0.1rem;
}
/* Raised (up) */
.up {
box-shadow:
var(--shadow-source)
calc(0.33rem * var(--depth))
calc(0.33rem * var(--depth))
0.6rem;
}
.up::before {
box-shadow:
var(--light-source)
calc(-0.33rem * var(--depth))
calc(-0.33rem * var(--depth))
0.6rem;
}
/* Inset (down) */
.down {
box-shadow:
inset var(--shadow-source)
calc(0.25rem * var(--depth))
calc(0.25rem * var(--depth))
0.6rem;
}
.down::before {
box-shadow:
inset var(--light-source)
calc(-0.25rem * var(--depth))
calc(-0.25rem * var(--depth))
0.6rem;
}
/* Button (interactive) */
.button {
cursor: pointer;
box-shadow:
var(--shadow-source)
calc(0.33rem * var(--depth))
calc(0.33rem * var(--depth))
0.6rem;
transition: box-shadow var(--duration-quick) var(--duration-quick) ease-in;
}
.button::before {
box-shadow:
var(--light-source)
calc(-0.33rem * var(--depth))
calc(-0.33rem * var(--depth))
0.6rem;
transition: box-shadow var(--duration-quick) var(--duration-quick) ease-in;
}
.button span {
transition: transform var(--duration-standard) ease-in-out;
}
.button > .tile {
box-shadow: inset var(--shadow-source) 0 0 0;
transition: box-shadow var(--duration-quick) ease-in;
}
.button > .tile::before {
box-shadow: inset var(--light-source) 0 0 0;
transition: box-shadow var(--duration-quick) ease-in;
}
.button:hover {
box-shadow: var(--shadow-source) 0 0 0;
transition: box-shadow var(--duration-quick) ease-out;
}
.button:hover::before {
box-shadow: var(--light-source) 0 0 0;
transition: box-shadow var(--duration-quick) ease-out;
}
.button:hover > .tile {
box-shadow:
inset var(--shadow-source)
calc(0.25rem * var(--depth))
calc(0.25rem * var(--depth))
0.6rem;
transition: box-shadow var(--duration-quick) var(--duration-quick) ease-out;
}
.button:hover > .tile::before {
box-shadow:
inset var(--light-source)
calc(-0.25rem * var(--depth))
calc(-0.25rem * var(--depth))
0.6rem;
transition: box-shadow var(--duration-quick) var(--duration-quick) ease-out;
}
.button:hover span {
transform: translateY(2px);
}Navigation links styled as subtle neumorphic pills that raise on hover and press on active.
<nav class="nav" aria-label="Main navigation">
<a href="/" class="nav__logo" aria-label="Home">
<img src="/logo.svg" alt="" width="32" height="32">
</a>
<ul class="nav__links" role="list">
<li><a href="/about" class="nav__link">About</a></li>
<li><a href="/work" class="nav__link">Work</a></li>
<li><a href="/contact" class="nav__link nav__link--active" aria-current="page">Contact</a></li>
</ul>
</nav>.nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-8);
padding: var(--space-4) var(--container-padding);
}
.nav__logo {
display: flex;
align-items: center;
}
.nav__links {
display: flex;
gap: var(--space-2);
margin: 0;
padding: 0;
list-style: none;
}
/* Neumorphic pill links */
.nav__link {
--depth: 1;
position: relative;
display: inline-flex;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-700);
text-decoration: none;
font-weight: 500;
transition:
color var(--duration-instant),
background var(--duration-quick),
box-shadow var(--duration-quick);
}
.nav__link::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
box-shadow: var(--light-source) 0 0 0;
transition: box-shadow var(--duration-quick);
}
/* Hover: raised pill */
.nav__link:hover {
color: var(--ink-900);
background: var(--silver-100);
box-shadow:
var(--shadow-source)
calc(0.2rem * var(--depth))
calc(0.2rem * var(--depth))
0.4rem;
}
.nav__link:hover::before {
box-shadow:
var(--light-source)
calc(-0.2rem * var(--depth))
calc(-0.2rem * var(--depth))
0.4rem;
}
/* Active: pressed/inset pill */
.nav__link--active {
background: var(--silver-100);
box-shadow:
inset var(--shadow-source)
calc(0.15rem * var(--depth))
calc(0.15rem * var(--depth))
0.3rem;
}
.nav__link--active::before {
box-shadow:
inset var(--light-source)
calc(-0.15rem * var(--depth))
calc(-0.15rem * var(--depth))
0.3rem;
}
.nav__link:focus-visible {
outline: 2px solid var(--signal-focus);
outline-offset: 2px;
}