Back to Component Explorer

Skeuomorphism Dial

effects

Realistic 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 dropdown
SkeuomorphismDial.tsx
1'use client';
2
3import React, { useState, useRef, useEffect } from 'react';
4import { Volume2, VolumeX } from 'lucide-react';
5
6export default function SkeuomorphismDial() {
7 const [angle, setAngle] = useState(45); // 0 to 270 deg
8 const [isDragging, setIsDragging] = useState(false);
9 const dialRef = useRef<HTMLDivElement>(null);
10
11 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 };
25
26 const handleMouseDown = (e: React.MouseEvent) => {
27 setIsDragging(true);
28 calculateAngle(e.clientX, e.clientY);
29 };
30
31 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 };
37
38 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);
48
49 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]);
62
63 const percentage = Math.round((angle / 270) * 100);
64 const bassGain = ((percentage / 100) * 12).toFixed(1);
65
66 return (
67 <div className="flex flex-col items-center justify-center p-4 select-none">
68 <div
69 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 <div
79 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 ))}
88
89 <div
90 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 <div
98 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>
110
111 <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 Docs

If 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';
2
3export default function ExamplePage() {
4 return (
5 <main className="p-8 flex items-center justify-center min-h-screen">
6 <SkeuomorphismDial />
7 </main>
8 );
9}