buttons
Keyboard Button
A tactile 3D keyboard keycap style button with realistic shadows and perspective active press physics.
buttonkeyboardkeycap3dtactileshadow
▶Preview
🖲Usage
example-usage.tsx
import { KeyboardButton } from "@/components/kanso/keyboard-button"
export default function KeyboardButtonDemo() {
return (
<div className="flex gap-4">
<KeyboardButton variantColor="dark">cmd</KeyboardButton>
<KeyboardButton variantColor="blue">enter</KeyboardButton>
</div>
)
}↓Installation
1
Create folder & copy source
Create a folder named kanso inside your project's components directory (i.e. components/kanso/). Copy the source code shown in the next section, and paste it into a file named keyboard-button.tsx inside it.
2
Required helper files
Ensure your project has the following helper files configured:
- →
lib/utils - →
components/ui/button
<>Source Code
keyboard-button.tsx
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface KeyboardButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Visual style variant for the keycap color */
variantColor?: 'dark' | 'light' | 'blue';
/** Size variant */
size?: 'sm' | 'md' | 'lg';
/** Key symbol or icon displayed at the top */
icon?: React.ReactNode;
}
const KeyboardButton = React.forwardRef<HTMLButtonElement, KeyboardButtonProps>(
(
{
className,
variantColor = 'dark',
size = 'lg',
icon,
children,
type = 'button',
...props
},
ref
) => {
return (
<button
ref={ref}
type={type}
className={cn(
// Base mechanical layout & perspective (fully native to prevent default hover shade pollution)
'flex flex-col items-start justify-between cursor-pointer relative select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2',
// Easing / Spring transition
'transition-all duration-300 [transition-timing-function:cubic-bezier(0.34,1.56,0.64,1)]',
// 3D Tilt perspective transform & dynamics
'[transform:perspective(150px)_rotateX(6deg)_translateY(0)]',
'hover:-translate-y-[4px] hover:[transform:perspective(150px)_rotateX(8deg)_translateZ(4px)]',
'active:translate-y-[2px] active:[transform:perspective(150px)_rotateX(2deg)_translateZ(-2px)] active:duration-75',
// Size styles
size === 'sm' &&
'h-[45px] min-w-[45px] text-[8px] p-2 rounded-t-[10px] rounded-b-[8px]',
size === 'md' &&
'h-[55px] min-w-[55px] text-[9px] p-2.5 rounded-t-[12px] rounded-b-[10px]',
size === 'lg' &&
'h-[65px] min-w-[70px] text-[11px] p-3 rounded-t-[15px] rounded-b-[12px]',
// before backdrop shading (the physical keyboard well shadow / depth)
"before:content-[''] before:absolute before:inset-0 before:bg-[linear-gradient(to_right,rgba(0,0,0,0.15),rgba(0,0,0,0)),linear-gradient(to_bottom,rgba(0,0,0,0.15),rgba(0,0,0,0))] before:bg-[position:bottom_right,bottom_right] before:bg-[size:100%_100%,100%_100%] before:bg-no-repeat before:z-[-1] before:transition-all",
size === 'sm' && 'before:rounded-[10px]',
size === 'md' && 'before:rounded-[12px]',
size === 'lg' && 'before:rounded-[15px]',
// after gloss shading (keycap bevel / lighting reflection)
"after:content-[''] after:absolute after:inset-0 after:bg-gradient-to-b after:from-white/10 after:to-black/30 after:z-[-1] after:transition-all",
size === 'sm' && 'after:rounded-[10px]',
size === 'md' && 'after:rounded-[12px]',
size === 'lg' && 'after:rounded-[15px]',
// Color & Shading Variations
variantColor === 'dark' && [
'bg-zinc-900 text-zinc-100 border border-zinc-950/50',
// Default flat shadow
'shadow-[0_2px_0px_#09090b,0_4px_6px_rgba(0,0,0,0.4),inset_0_1px_0px_rgba(255,255,255,0.15),inset_0_-4px_0px_#18181b]',
// Hover state (taller shadow, baseline glow, no gray overlays)
'hover:bg-zinc-850 hover:shadow-[0_5px_0px_#09090b,0_8px_16px_rgba(0,0,0,0.5),inset_0_1.5px_0px_rgba(255,255,255,0.25),inset_0_-4px_0px_#18181b]',
// Active state (depressed shadow)
'active:shadow-[0_1px_0px_#09090b,0_2px_4px_rgba(0,0,0,0.3),inset_0_0.5px_0px_rgba(255,255,255,0.05),inset_0_-1px_0px_#18181b]',
'after:shadow-[inset_2px_2px_0px_rgba(255,255,255,0.05)]',
],
variantColor === 'light' && [
'bg-zinc-100 text-zinc-800 border border-zinc-200/60',
// Default flat shadow
'shadow-[0_2px_0px_#d4d4d8,0_4px_6px_rgba(0,0,0,0.06),inset_0_1px_0px_rgba(255,255,255,0.7),inset_0_-4px_0px_#e4e4e7]',
// Hover state (brighter white base, clean gray-beige under-shadow, no black overlay)
'hover:bg-white hover:text-zinc-900 hover:border-zinc-300 hover:shadow-[0_5px_0px_#c8c8cc,0_8px_16px_rgba(0,0,0,0.08),inset_0_1.5px_0px_rgba(255,255,255,1),inset_0_-4px_0px_#e4e4e7]',
// Active state (depressed shadow)
'active:shadow-[0_1px_0px_#d4d4d8,0_2px_4px_rgba(0,0,0,0.04),inset_0_0.5px_0px_rgba(255,255,255,0.3),inset_0_-1px_0px_#e4e4e7]',
'after:from-white/30 after:to-black/5',
'after:shadow-[inset_2px_2px_0px_rgba(255,255,255,0.4)]',
],
variantColor === 'blue' && [
'bg-blue-600 text-white border border-blue-700/60',
// Default flat shadow
'shadow-[0_2px_0px_#1d4ed8,0_4px_6px_rgba(29,78,216,0.3),inset_0_1px_0px_rgba(255,255,255,0.3),inset_0_-4px_0px_#2563eb]',
// Hover state (brighter blue, rich deep blue shadow, no dark gray overlay)
'hover:bg-blue-500 hover:shadow-[0_5px_0px_#1e40af,0_8px_16px_rgba(29,78,216,0.45),inset_0_1.5px_0px_rgba(255,255,255,0.45),inset_0_-4px_0px_#2563eb]',
// Active state (depressed shadow)
'active:shadow-[0_1px_0px_#1d4ed8,0_2px_4px_rgba(29,78,216,0.2),inset_0_0.5px_0px_rgba(255,255,255,0.15),inset_0_-1px_0px_#2563eb]',
'after:shadow-[inset_2px_2px_0px_rgba(255,255,255,0.15)]',
],
className
)}
{...props}
>
{icon && (
<div className="flex items-center justify-center text-[7px] font-normal tracking-wide lowercase opacity-60 shrink-0 self-start h-4">
{icon}
</div>
)}
<span
className={cn(
'font-semibold tracking-tight uppercase self-end mt-auto',
size === 'sm' && 'text-[8px]',
size === 'md' && 'text-[9px]',
size === 'lg' && 'text-[10px]'
)}
>
{children}
</span>
</button>
);
}
);
KeyboardButton.displayName = 'KeyboardButton';
export { KeyboardButton };