نحوه ایجاد افکت Hover Card Spotlight با Tailwind CSS

نسخه نمایشی زنده / دانلود
—
به این آموزش خوش آمدید، جایی که ما شما را به سفری می بریم تا چشم نوازی خلق کنید افکت شناور کارت نورافکن با استفاده از Tailwind CSS. اگر با این افکت پرطرفدار تازه کار هستید، توصیه میکنیم نسخه نمایشی زنده یا قالب صفحه فرود Dark Next.js ما را با نام ستاره ای.
برای شروع، کارت Spotlight را با استفاده از HTML خالص و جاوا اسکریپت وانیلی ایجاد می کنیم. پس از آن، با نشان دادن نحوه ایجاد یک کامپوننت قابل استفاده مجدد برای Next.js و Vue، یک قدم جلوتر خواهیم رفت.
ناوبری سریع
بیا شروع کنیم!
افکت نورافکن را با HTML و جاوا اسکریپت وانیلی ایجاد کنید
همانطور که معمولاً در آموزشهای خود انجام میدهیم، با ایجاد یک سند HTML اولیه که شامل ساختار انیمیشن ما باشد، شروع میکنیم. سپس کد جاوا اسکریپت را در یک فایل JS خارجی می نویسیم.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Spotlight Effect</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
inter: ['Inter', 'sans-serif'],
},
},
},
};
</script>
</head>
<body class="relative font-inter antialiased">
<main class="relative min-h-screen flex flex-col justify-center bg-slate-900 overflow-hidden">
<div class="w-full max-w-6xl mx-auto px-4 md:px-6 py-24">
<!-- Cards container -->
<div class="max-w-sm mx-auto grid gap-6 lg:grid-cols-3 items-start lg:max-w-none group">
<!-- Card 1 -->
<!-- Card 2 -->
<!-- Card 3 -->
</div>
<!-- End: Cards container -->
</div>
</main>
<script src="./spotlight-effect.js"></script>
</body>
</html>
ما یک ساختار بسیار ساده، با ظرفی که کارت های ما را نگه می دارد، ایجاد کرده ایم. حالا بیایید با کدگذاری یک کارت شروع کنیم و سپس آن را برای ایجاد دو کارت دیگر کپی می کنیم.
<div class="relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden">
<!-- 1. Before pseudo element -->
<!-- 2. Card content -->
<div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
<!-- Radial gradient -->
<div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
<div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
</div>
<div class="flex flex-col h-full items-center text-center">
<!-- Image -->
<div class="relative inline-flex">
<div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
<img class="inline-flex" src="./card-01.png" width="200" height="200" alt="Card 01" />
</div>
<!-- Text -->
<div class="grow mb-5">
<h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
<p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
</div>
<a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
<svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
</svg>
<span>Connect</span>
</a>
</div>
</div>
<!-- 3. After pseudo element -->
</div>
ما کلاس های زیادی را به کارت اضافه کرده ایم، اما نگران نباشید، آنها را یکی یکی مرور می کنیم.
یک حاشیه کارت اضافه کنید
در اولین div خود، a اضافه کرده ایم bg-slate-800
برای تعریف رنگ پس زمینه تیره سپس، ما یک را اضافه کرده ایم p-px
کلاس برای اضافه کردن یک بالشتک 1 پیکسلی به کارت، و محتوای کارت همان رنگ پسزمینه صفحه را دارد. با انجام این کار، ما در حال ایجاد یک حاشیه جعلی 1 پیکسلی اطراف کارت
دلیل اینکه ما از مرزهای CSS معمولی استفاده نمی کنیم این است که به ما اجازه نمی دهند اثری را که می خواهیم به دست آوریم ایجاد کنیم.
حالا بیایید به ادامه مطلب برویم before
و after
عناصر شبه ما استفاده خواهیم کرد before
و after
پیشوندهای ارائه شده توسط Tailwind CSS برای ایجاد آنها و تعریف سبک آنها.
این before
عنصر شبه برای روشن کردن مرزهای کارت های ما در شناور ماوس استفاده خواهد شد، در حالی که after
عنصر شبه برای جلوه برجسته بالای کارت استفاده خواهد شد.
از عنصر شبه قبل برای حاشیه کارت استفاده کنید
بیایید ببینیم که عنصر شبه قبل چگونه کار می کند. این لایه ای است که در زیر محتوای کارت قرار گرفته است و کاملاً طوری قرار گرفته است که کل کارت را بپوشاند. در کنارههای کارت به اندازه 1 پیکسل قابل مشاهده است، و حاشیههای کارت را با رنگ خاکستری روشنتر (صفحهای 400) روی ماوس روشن میکند.
کلاس های کلیدی Tailwind CSS این عنصر عبارتند از:
-
before:opacity-0 before:group-hover:opacity-100
:before
عنصر شبه بهطور پیشفرض پنهان است و تنها زمانی که ظرف کارت شناور باشد، قابل مشاهده است. توجه داشته باشید که ما از Tailwind CSS استفاده می کنیمgroup-hover
نوع، بنابراین می خواهید مطمئن شوید که آن را اضافه کنیدgroup
کلاس به عنصر والد کارت. -
before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)]
: این کلاس موقعیت the را تعریف می کندbefore
عنصر شبه ما از ویژگی های دلخواه Tailwind برای تنظیم استفاده می کنیمtranslate-x
وtranslate-y
ویژگی های CSS به موقعیت ماوس. به این ترتیب،before
عنصر شبه نشانگر ماوس را دنبال می کند.
از عنصر after برای نورافکن استفاده کنید
این after
عنصر شبه برای ایجاد افکت نورافکن استفاده می شود. این یک لایه در بالای محتوای کارت قرار گرفته است و کاملاً طوری قرار گرفته است که کل کارت را بپوشاند.
کد بسیار شبیه به before
عنصر شبه، اما چند تفاوت وجود دارد:
- دایره تار کمی بزرگتر به نظر می رسد (
after:w-96 after:h-96
) - رنگ پس زمینه متفاوت است (
after:bg-indigo-500
) و کدورت هدف کمتر است (after:hover:opacity-10
) - ما هنگام نگهداشتن تک کارت، کانون توجه را نشان میدهیم، نه محفظه کارت را همانطور که برای کارت انجام دادیم
before
عنصر شبه
سرد! اکنون، وقتی کارت را نگه میدارید، باید افکت نورافکن و حاشیههای کارت را در گوشه بالا سمت چپ روشن کنید.
همانطور که ممکن است حدس بزنید، ما می خواهیم افکت نورافکن را متحرک کنیم، بنابراین اجازه دهید مقداری جاوا اسکریپت اضافه کنیم.
کاری کنید که کانون توجه نشانگر ماوس را دنبال کند
همانطور که می دانید، ما استفاده کردیم translate-x-[var(--mouse-x)]
و translate-y-[var(--mouse-y)]
برای تعیین موقعیت هر دو before
و after
عنصر شبه به عبارت دیگر استفاده کردیم --mouse-x
و --mouse-x
متغیرهای CSS برای تعیین موقعیت عناصر به جای استفاده از یک مقدار ثابت.
اکنون میخواهیم مقدار آن متغیرهای CSS را در حرکت ماوس بهروزرسانی کنیم، به طوری که کانون توجه نشانگر ماوس را دنبال کند. برای این کار از کمی جاوا اسکریپت استفاده خواهیم کرد.
بیایید یک کلاس جاوا اسکریپت به نام ایجاد کنیم Spotlight
داخل ما spotlight-effect.js
فایل:
// Cards spotlight
class Spotlight {
constructor(containerElement) {
this.container = containerElement;
this.cards = Array.from(this.container.children);
this.mouse = {
x: 0,
y: 0,
};
this.containerSize = {
w: 0,
h: 0,
};
this.initContainer = this.initContainer.bind(this);
this.onMouseMove = this.onMouseMove.bind(this);
this.init();
}
initContainer() {
this.containerSize.w = this.container.offsetWidth;
this.containerSize.h = this.container.offsetHeight;
}
onMouseMove(event) {
const { clientX, clientY } = event;
const rect = this.container.getBoundingClientRect();
const { w, h } = this.containerSize;
const x = clientX - rect.left;
const y = clientY - rect.top;
const inside = x < w && x > 0 && y < h && y > 0;
if (inside) {
this.mouse.x = x;
this.mouse.y = y;
this.cards.forEach((card) => {
const cardX = -(card.getBoundingClientRect().left - rect.left) + this.mouse.x;
const cardY = -(card.getBoundingClientRect().top - rect.top) + this.mouse.y;
card.style.setProperty('--mouse-x', `${cardX}px`);
card.style.setProperty('--mouse-y', `${cardY}px`);
});
}
}
init() {
this.initContainer();
window.addEventListener('resize', this.initContainer);
window.addEventListener('mousemove', this.onMouseMove);
}
}
// Init Spotlight
const spotlights = document.querySelectorAll('[data-spotlight]');
spotlights.forEach((spotlight) => {
new Spotlight(spotlight);
});
ما وارد جزئیات کد نمیشویم، اما در اینجا یک مرور کلی از آنچه انجام میدهد وجود دارد:
- الف را ایجاد می کند
Spotlight
نمونه ای برای هر عنصر باdata-spotlight
ویژگی، که قرار است محفظه نگهدارنده کارت ها باشد - وقتی ظرف کارت را نگه می دارید، تنظیم می شود
--mouse-x
و--mouse-y
متغیرهای CSS در هر کارت - به روز می شود
--mouse-x
و--mouse-y
مقادیر در حرکت ماوس، اما تنها در صورتی که نشانگر ماوس در داخل ظرف کارت باشد
به این ترتیب، مقادیر translate-x و translate-y از before
و after
عناصر شبه با حرکت ماوس به روز می شوند و کانون توجه نشانگر ماوس را دنبال می کند.
در نهایت، اطمینان حاصل کنید که اضافه کنید data-spotlight
به ظرف کارت نسبت دهید و کارت ها را به عنوان فرزندان سطح اول ظرف اضافه کنید:
<div class="max-w-sm mx-auto grid gap-6 lg:grid-cols-3 items-start lg:max-w-none group" data-spotlight>
<!-- Card 1 -->
<div class="relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden">
<div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
<!-- Radial gradient -->
<div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
<div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
</div>
<div class="flex flex-col h-full items-center text-center">
<!-- Image -->
<div class="relative inline-flex">
<div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
<img class="inline-flex" src="./card-01.png" width="200" height="200" alt="Card 01" />
</div>
<!-- Text -->
<div class="grow mb-5">
<h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
<p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
</div>
<a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
<svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
</svg>
<span>Connect</span>
</a>
</div>
</div>
</div>
<!-- Card 2 -->
<div class="relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden">
<div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
<!-- Radial gradient -->
<div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
<div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
</div>
<div class="flex flex-col h-full items-center text-center">
<!-- Image -->
<div class="relative inline-flex">
<div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
<img class="inline-flex" src="./card-02.png" width="200" height="200" alt="Card 02" />
</div>
<!-- Text -->
<div class="grow mb-5">
<h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
<p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
</div>
<a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
<svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
</svg>
<span>Connect</span>
</a>
</div>
</div>
</div>
<!-- Card 3 -->
<div class="relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden">
<div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
<!-- Radial gradient -->
<div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
<div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
</div>
<div class="flex flex-col h-full items-center text-center">
<!-- Image -->
<div class="relative inline-flex">
<div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
<img class="inline-flex" src="./card-03.png" width="200" height="200" alt="Card 03" />
</div>
<!-- Text -->
<div class="grow mb-5">
<h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
<p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
</div>
<a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
<svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
</svg>
<span>Connect</span>
</a>
</div>
</div>
</div>
</div>
یک جزء Spotlight قابل استفاده مجدد برای Next.js ایجاد کنید
اکنون که یک افکت نورافکن کار می کنیم، بیایید یک مؤلفه قابل استفاده مجدد برای Next.js با پشتیبانی از TypeScript ایجاد کنیم. مؤلفه ای که می خواهیم بسازیم در مخزن GitHub ما موجود است که شامل تمام نمونه های Next.js از آموزش های ما است.
بیایید یک فایل جدید به نام ایجاد کنیم spotlight.tsx
درون components
پوشه از آنجایی که ما یک ظرف و تعدادی کارت در داخل آن داریم، میخواهیم دو جزء برای آنها ایجاد کنیم:
-
Spotlight
جزء اصلی است و منطق کامل را مدیریت می کند. -
SpotlightCard
فقط یک لفاف برای محتوای کارت است. الف را می پذیردclassName
پایه ای که روی ظرف کارت اعمال می شود، و الفchildren
پشتیبان که محتوای کارت خواهد بود.
'use client'
import React, { useRef, useState, useEffect } from 'react'
import MousePosition from './utils/mouse-position'
type SpotlightProps = {
children: React.ReactNode
className?: string
}
export default function Spotlight({
children,
className = '',
}: SpotlightProps) {
const containerRef = useRef<HTMLDivElement>(null)
const mousePosition = MousePosition()
const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const containerSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 })
const [boxes, setBoxes] = useState<Array<HTMLElement>>([])
useEffect(() => {
containerRef.current && setBoxes(Array.from(containerRef.current.children).map((el) => el as HTMLElement))
}, [])
useEffect(() => {
initContainer()
window.addEventListener('resize', initContainer)
return () => {
window.removeEventListener('resize', initContainer)
}
}, [setBoxes])
useEffect(() => {
onMouseMove()
}, [mousePosition])
const initContainer = () => {
if(containerRef.current) {
containerSize.current.w = containerRef.current.offsetWidth
containerSize.current.h = containerRef.current.offsetHeight
}
}
const onMouseMove = () => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect()
const { w, h } = containerSize.current
const x = mousePosition.x - rect.left
const y = mousePosition.y - rect.top
const inside = x < w && x > 0 && y < h && y > 0
if (inside) {
mouse.current.x = x
mouse.current.y = y
boxes.forEach((box) => {
const boxX = -(box.getBoundingClientRect().left - rect.left) + mouse.current.x
const boxY = -(box.getBoundingClientRect().top - rect.top) + mouse.current.y
box.style.setProperty('--mouse-x', `${boxX}px`)
box.style.setProperty('--mouse-y', `${boxY}px`)
})
}
}
}
return (
<div className={className} ref={containerRef}>{children}</div>
)
}
type SpotlightCardProps = {
children: React.ReactNode,
className?: string
}
export function SpotlightCard({
children,
className = ''
}: SpotlightCardProps) {
return <div className={`relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden ${className}`}>{children}</div>
}
همانطور که می بینید، کد بسیار شبیه به کدی است که در بخش قبل استفاده کردیم. ما به تازگی برخی از حاشیه نویسی های TypeScript را اضافه کرده ایم و آنها را وارد کرده ایم mouse-position.tsx
جزء – که قبلاً برای آموزش دیگری ایجاد کرده بودیم – برای به دست آوردن موقعیت ماوس.
اکنون میتوانیم آنها را در یک صفحه یا در کامپوننت دیگری وارد کرده و استفاده کنیم، درست مانند این:
<Spotlight className="max-w-sm mx-auto grid gap-6 lg:grid-cols-3 items-start lg:max-w-none group">
{/* Card #1 */}
<SpotlightCard>
<div className="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
{/* Radial gradient */}
<div className="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
<div className="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
</div>
<div className="flex flex-col h-full items-center text-center">
{/* Image */}
<div className="relative inline-flex">
<div className="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
<Image className="inline-flex" src={Card01} width={200} height={200} alt="Card 01" />
</div>
{/* Text */}
<div className="grow mb-5">
<h2 className="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
<p className="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
</div>
<a className="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
<svg className="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
</svg>
<span>Connect</span>
</a>
</div>
</div>
</SpotlightCard>
{/* Card #2 */}
<SpotlightCard>
<div className="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
{/* Radial gradient */}
<div className="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
<div className="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
</div>
<div className="flex flex-col h-full items-center text-center">
{/* Image */}
<div className="relative inline-flex">
<div className="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
<Image className="inline-flex" src={Card02} width={200} height={200} alt="Card 02" />
</div>
{/* Text */}
<div className="grow mb-5">
<h2 className="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
<p className="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
</div>
<a className="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
<svg className="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
</svg>
<span>Connect</span>
</a>
</div>
</div>
</SpotlightCard>
{/* Card #3 */}
<SpotlightCard>
<div className="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
{/* Radial gradient */}
<div className="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
<div className="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
</div>
<div className="flex flex-col h-full items-center text-center">
{/* Image */}
<div className="relative inline-flex">
<div className="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
<Image className="inline-flex" src={Card03} width={200} height={200} alt="Card 03" />
</div>
{/* Text */}
<div className="grow mb-5">
<h2 className="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
<p className="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
</div>
<a className="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
<svg className="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
</svg>
<span>Connect</span>
</a>
</div>
</div>
</SpotlightCard>
</Spotlight>
یک جزء Vue Spotlight ایجاد کنید
به عنوان آخرین مرحله، ما میخواهیم یک مؤلفه قابل استفاده مجدد برای Vue با پشتیبانی TypeScript ایجاد کنیم، که در مخزن GitHub ما موجود است، که شامل تمام نمونههای آموزش Cruip است.
بیایید یک فایل جدید به نام ایجاد کنیم Spotlight.vue
درون components
پوشه
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, reactive, watch } from 'vue'
import useMousePosition from './utils/MousePosition'
const containerRef = ref<HTMLCanvasElement | null>(null)
const mousePosition = useMousePosition()
const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 })
const containerSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 })
const boxes = ref<any[]>([])
onMounted(() => {
if (containerRef.value) {
boxes.value = Array.from(containerRef.value.children)
}
initContainer()
window.addEventListener('resize', initContainer)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', initContainer)
})
watch(
() => mousePosition.value,
() => {
onMouseMove()
}
)
const initContainer = () => {
if (containerRef.value) {
containerSize.w = containerRef.value.offsetWidth
containerSize.h = containerRef.value.offsetHeight
}
}
const onMouseMove = () => {
if (containerRef.value) {
const rect = containerRef.value.getBoundingClientRect()
const { w, h } = containerSize
const x = mousePosition.value.x - rect.left
const y = mousePosition.value.y - rect.top
const inside = x < w && x > 0 && y < h && y > 0
if (inside) {
mouse.x = x
mouse.y = y
boxes.value.forEach((box) => {
const boxX = -(box.getBoundingClientRect().left - rect.left) + mouse.x
const boxY = -(box.getBoundingClientRect().top - rect.top) + mouse.y
box.style.setProperty('--mouse-x', `${boxX}px`)
box.style.setProperty('--mouse-y', `${boxY}px`)
})
}
}
}
</script>
<template>
<div ref="containerRef">
<slot></slot>
</div>
</template>
همانطور که می بینید، ما از آن استفاده می کنیم MousePosition.ts
جزء – که قبلاً برای آموزش انیمیشن ذرات ساخته ایم – که مختصات ماوس را دریافت می کند.
سپس برای هر جعبه به یک کامپوننت جداگانه نیاز داریم که قرار است آن را فراخوانی کنیم SpotlightCard.vue
:
<template>
<div class="relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden">
<slot></slot>
</div>
</template>
در نهایت می توانیم از Spotlight.vue
و SpotlightCard.vue
اجزای موجود در صفحات ما:
<Spotlight class="max-w-sm mx-auto grid gap-6 lg:grid-cols-3 items-start lg:max-w-none group">
<!-- Card #1 -->
<SpotlightCard>
<div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
<!-- Radial gradient -->
<div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
<div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
</div>
<div class="flex flex-col h-full items-center text-center">
<!-- Image -->
<div class="relative inline-flex">
<div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
<img class="inline-flex" :src="Card01" width="200" height="200" alt="Card 01" />
</div>
<!-- Text -->
<div class="grow mb-5">
<h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
<p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
</div>
<a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
<svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
</svg>
<span>Connect</span>
</a>
</div>
</div>
</SpotlightCard>
<!-- Card #2 -->
<SpotlightCard>
<div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
<!-- Radial gradient -->
<div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
<div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
</div>
<div class="flex flex-col h-full items-center text-center">
<!-- Image -->
<div class="relative inline-flex">
<div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
<img class="inline-flex" :src="Card02" width="200" height="200" alt="Card 02" />
</div>
<!-- Text -->
<div class="grow mb-5">
<h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
<p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
</div>
<a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
<svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
</svg>
<span>Connect</span>
</a>
</div>
</div>
</SpotlightCard>
<!-- Card #3 -->
<SpotlightCard>
<div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
<!-- Radial gradient -->
<div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
<div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
</div>
<div class="flex flex-col h-full items-center text-center">
<!-- Image -->
<div class="relative inline-flex">
<div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
<img class="inline-flex" :src="Card03" width="200" height="200" alt="Card 03" />
</div>
<!-- Text -->
<div class="grow mb-5">
<h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
<p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
</div>
<a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
<svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
</svg>
<span>Connect</span>
</a>
</div>
</div>
</SpotlightCard>
</Spotlight>
نظر شما در مورد نتیجه نهایی چیست؟ زیبا نیست؟ ما شخصاً آن را دوست داریم و تعجب نمیکنیم که در سال 2023 جلوه شناور کارت نورافکن بسیار محبوب شده است، زیرا میتواند هر طراحی را تازهتر و مدرنتر نشان دهد.
به راحتی می توانید این افکت را در هر جایی که می خواهید اعمال کنید و همچنین اگر فکر می کنید ممکن است با طرح های شما سازگارتر باشد، آن را روی پس زمینه سفید تست کنید.