Back to Component Explorer
Skeuomorphism Dial
effectsRealistic metallic brushed rotary knob with physical inset shadow dynamics.
#skeuomorphism#knob#dial#physical#realistic
Live Interactive Preview
BASS+2.0dB
17% Level• Drag to Rotate
Component Source Code
Select language in the dropdownSkeuomorphismDial.tsx
1'use client';23import React, { useState, useRef, useEffect } from 'react';4import { Volume2, VolumeX } from 'lucide-react';56export default function SkeuomorphismDial() {7 const [angle, setAngle] = useState(45); // 0 to 270 deg8 const [isDragging, setIsDragging] = useState(false);9 const dialRef = useRef<HTMLDivElement>(null);1011 const calculateAngle = (clientX: number, clientY: number) => {12 if (!dialRef.current) return;13 const rect = dialRef.current.getBoundingClientRect();14 const centerX = rect.left + rect.width / 2;15 const centerY = rect.top + rect.height / 2;16 const deltaX = clientX - centerX;17 const deltaY = clientY - centerY;18 let rad = Math.atan2(deltaY, deltaX);19 let deg = (rad * 180) / Math.PI + 90;20 if (deg < 0) deg += 360;21 if (deg > 300) deg = 0;22 if (deg > 270) deg = 270;23 setAngle(Math.min(270, Math.max(0, Math.round(deg))));24 };2526 const handleMouseDown = (e: React.MouseEvent) => {27 setIsDragging(true);28 calculateAngle(e.clientX, e.clientY);29 };3031 const handleTouchStart = (e: React.TouchEvent) => {32 if (e.touches.length === 1) {33 setIsDragging(true);34 calculateAngle(e.touches[0].clientX, e.touches[0].clientY);35 }36 };3738 useEffect(() => {39 const handleMouseMove = (e: MouseEvent) => {40 if (!isDragging) return;41 calculateAngle(e.clientX, e.clientY);42 };43 const handleTouchMove = (e: TouchEvent) => {44 if (!isDragging || e.touches.length !== 1) return;45 calculateAngle(e.touches[0].clientX, e.touches[0].clientY);46 };47 const handleEnd = () => setIsDragging(false);4849 if (isDragging) {50 window.addEventListener('mousemove', handleMouseMove);51 window.addEventListener('mouseup', handleEnd);52 window.addEventListener('touchmove', handleTouchMove);53 window.addEventListener('touchend', handleEnd);54 }55 return () => {56 window.removeEventListener('mousemove', handleMouseMove);57 window.removeEventListener('mouseup', handleEnd);58 window.removeEventListener('touchmove', handleTouchMove);59 window.removeEventListener('touchend', handleEnd);60 };61 }, [isDragging]);6263 const percentage = Math.round((angle / 270) * 100);64 const bassGain = ((percentage / 100) * 12).toFixed(1);6566 return (67 <div className="flex flex-col items-center justify-center p-4 select-none">68 <div69 ref={dialRef}70 onMouseDown={handleMouseDown}71 onTouchStart={handleTouchStart}72 style={{73 boxShadow: '0 25px 50px rgba(0,0,0,0.5), inset 0 2px 4px rgba(255,255,255,0.6), inset 0 -4px 8px rgba(0,0,0,0.8)'74 }}75 className="relative flex h-44 w-44 sm:h-48 sm:w-48 cursor-grab active:cursor-grabbing items-center justify-center rounded-full border-4 border-slate-700 bg-gradient-to-b from-slate-200 via-slate-400 to-slate-600 shadow-2xl"76 >77 {[0, 30, 60, 90, 120, 150, 180, 210, 240, 270].map((deg) => (78 <div79 key={deg}80 style={{81 transform: `rotate(${deg - 135}deg) translateY(-84px)`82 }}83 className={`absolute h-2.5 w-1 rounded-full transition-colors ${84 deg <= angle ? 'bg-cyan-400 shadow-[0_0_8px_#22d3ee]' : 'bg-slate-600/60'85 }`}86 />87 ))}8889 <div90 style={{91 transform: `rotate(${angle - 135}deg)`,92 transition: isDragging ? 'none' : 'transform 0.1s cubic-bezier(0.4, 0, 0.2, 1)'93 }}94 className="relative flex h-32 w-32 sm:h-36 sm:w-36 items-center justify-center rounded-full bg-gradient-to-tr from-slate-900 via-slate-800 to-slate-700 shadow-[inset_0_4px_12px_rgba(0,0,0,0.9),0_6px_16px_rgba(0,0,0,0.6)]"95 >96 <div className="absolute top-2.5 h-3.5 w-1.5 rounded-full bg-gradient-to-b from-cyan-300 to-cyan-500 shadow-[0_0_10px_#22d3ee,0_0_20px_#06b6d4]" />97 <div98 style={{99 boxShadow: 'inset 0 2px 4px rgba(255,255,255,0.4), inset 0 -3px 6px rgba(0,0,0,0.9)'100 }}101 className="flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-b from-slate-700 via-slate-800 to-slate-950 border border-slate-600"102 >103 <div className="text-center">104 <span className="block text-[10px] font-mono tracking-widest text-slate-400 font-bold uppercase">BASS</span>105 <span className="text-xs font-mono font-black text-cyan-400">+{bassGain}dB</span>106 </div>107 </div>108 </div>109 </div>110111 <div className="mt-4 flex items-center gap-3 rounded-full bg-slate-900/90 border border-slate-800 px-4 py-1.5 text-xs text-slate-300 shadow-md">112 {percentage === 0 ? <VolumeX className="h-3.5 w-3.5 text-slate-500" /> : <Volume2 className="h-3.5 w-3.5 text-cyan-400" />}113 <span className="font-mono font-semibold">{percentage}% Level</span>114 <span className="text-[10px] text-slate-500">• Drag to Rotate</span>115 </div>116 </div>117 );118}Installation & Integration Guide
Step 1: Tailwind CSS v4 Setup
Tailwind DocsIf you do not have Tailwind installed yet, run this installation command:
Terminal
npm install tailwindcss @tailwindcss/postcss postcss
Then add the Tailwind directive to your main stylesheet (globals.css):
@import "tailwindcss";
Step 2: Install Icon Dependencies
npm install lucide-react react-icons
Step 3: Add Component File
Create a new file in your project at src/components/effects/SkeuomorphismDial.tsx and paste the copied code from the source viewer above.
Step 4: Usage Example
ExamplePage.tsx
1import SkeuomorphismDial from '@/components/effects/SkeuomorphismDial';23export default function ExamplePage() {4 return (5 <main className="p-8 flex items-center justify-center min-h-screen">6 <SkeuomorphismDial />7 </main>8 );9}