3D Pin
A gradient pin that animates on hover, perfect for product links
Aceternity UI
Customizable Tailwind CSS and Framer Motion Components.
<script lang="ts">
import { PinContainer } from '.';
</script>
<div class="flex h-[40rem] w-full items-center justify-center">
<PinContainer title="/aceternity.sveltekit.io" href="https://aceternity.sveltekit.io">
<div
class="flex h-[20rem] w-[20rem] basis-full flex-col p-4 tracking-tight text-slate-100/50 sm:basis-1/2"
>
<h3 class="!m-0 max-w-xs !pb-2 text-base font-bold text-slate-100">Aceternity UI</h3>
<div class="!m-0 !p-0 text-base font-normal">
<span class="text-slate-500">
Customizable Tailwind CSS and Framer Motion Components.
</span>
</div>
<div
class="mt-4 flex w-full flex-1 rounded-lg bg-gradient-to-br from-violet-500 via-purple-500 to-blue-500"
/>
</div>
</PinContainer>
</div>
Installation
Install Dependencies
npm i svelte-motion clsx tailwind-merge
Add util file
src/lib/utils/cn.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy the source code
src/lib/components/ui/ThreeDPin/PinContainer.svelte
<script lang="ts">
import { cn } from '@/utils';
import PinPerspective from './PinPerspective.svelte';
export let title: string | undefined;
export let href: string | undefined;
export let className: string | undefined = undefined;
export let containerClassName: string | undefined = undefined;
let transform = 'translate(-50%,-50%) rotateX(0deg)';
const onMouseEnter = () => {
transform = 'translate(-50%,-50%) rotateX(40deg) scale(0.8)';
};
const onMouseLeave = () => {
transform = 'translate(-50%,-50%) rotateX(0deg) scale(1)';
};
</script>
<div
class={cn('group/pin relative z-50 cursor-pointer', containerClassName)}
on:mouseenter={onMouseEnter}
on:mouseleave={onMouseLeave}
>
<div
style="perspective: 1000px; translateZ(0);"
class="absolute left-1/2 top-1/2 ml-[0.09375rem] mt-4 -translate-x-1/2 -translate-y-1/2"
>
<div
style={`transform: ${transform};`}
class="absolute left-1/2 top-1/2 flex items-start justify-start overflow-hidden rounded-2xl border border-white/[0.1] bg-black p-4 shadow-[0_8px_16px_rgb(0_0_0/0.4)] transition duration-700 group-hover/pin:border-white/[0.2]"
>
<div class={cn(' relative z-50 ', className)}><slot /></div>
</div>
</div>
<PinPerspective {title} {href} />
</div>
src/lib/components/ui/ThreeDPin/PinPerspective.svelte
<script lang="ts">
import { Motion } from 'svelte-motion';
export let title: string | undefined;
export let href: string | undefined;
</script>
<Motion let:motion>
<div
use:motion
class="pointer-events-none z-[60] flex h-80 w-96 items-center justify-center opacity-0 transition duration-500 group-hover/pin:opacity-100"
>
<div class=" inset-0 -mt-7 h-full w-full flex-none">
<div class="absolute inset-x-0 top-0 flex justify-center">
<a
{href}
target={'_blank'}
class="relative z-10 flex items-center space-x-2 rounded-full bg-zinc-950 px-4 py-0.5 ring-1 ring-white/10"
>
<span class="relative z-20 inline-block py-0.5 text-xs font-bold text-white">
{title}
</span>
<span
class="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-emerald-400/90 to-emerald-400/0 transition-opacity duration-500 group-hover/btn:opacity-40"
></span>
</a>
</div>
<div
style="perspective: 1000px; transform: rotateX(70deg) translateZ(0);"
class="absolute left-1/2 top-1/2 ml-[0.09375rem] mt-4 -translate-x-1/2 -translate-y-1/2"
>
<Motion
let:motion
initial={{
opacity: 0,
scale: 0,
x: '-50%',
y: '-50%'
}}
animate={{
opacity: [0, 1, 0.5, 0],
scale: 1,
z: 0
}}
transition={{
duration: 6,
repeat: Infinity,
delay: 0
}}
>
<div
use:motion
class="absolute left-1/2 top-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
></div>
</Motion>
<Motion
let:motion
initial={{
opacity: 0,
scale: 0,
x: '-50%',
y: '-50%'
}}
animate={{
opacity: [0, 1, 0.5, 0],
scale: 1,
z: 0
}}
transition={{
duration: 6,
repeat: Infinity,
delay: 2
}}
>
<div
use:motion
class="absolute left-1/2 top-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
></div>
</Motion>
<Motion
let:motion
initial={{
opacity: 0,
scale: 0,
x: '-50%',
y: '-50%'
}}
animate={{
opacity: [0, 1, 0.5, 0],
scale: 1,
z: 0
}}
transition={{
duration: 6,
repeat: Infinity,
delay: 4
}}
>
<div
use:motion
class="absolute left-1/2 top-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
></div>
</Motion>
</div>
<Motion let:motion>
<div
use:motion
class="absolute bottom-1/2 right-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-cyan-500 blur-[2px] group-hover/pin:h-40"
/>
</Motion>
<Motion let:motion>
<div
use:motion
class="absolute bottom-1/2 right-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-cyan-500 group-hover/pin:h-40"
/>
</Motion>
<Motion let:motion>
<div
use:motion
class="absolute bottom-1/2 right-1/2 z-40 h-[4px] w-[4px] translate-x-[1.5px] translate-y-[14px] rounded-full bg-cyan-600 blur-[3px]"
/>
</Motion>
<Motion let:motion>
<div
use:motion
class="absolute bottom-1/2 right-1/2 z-40 h-[2px] w-[2px] translate-x-[0.5px] translate-y-[14px] rounded-full bg-cyan-300"
/>
</Motion>
</div>
</div>
</Motion>
src/lib/components/ui/ThreeDPin/index.ts
import PinContainer from './PinContainer.svelte';
import PinPerspective from './PinPerspective.svelte';
export { PinContainer, PinPerspective };
Props
PinContainer
Prop | Type | Description |
---|---|---|
title | string | undefined | Title that shows up on hover |
href | string | undefined | Link of the component that you want to redirect to |
className | string | undefined | The class name of the child component. |
containerClassName | string | undefined | The class name of the Container Component. |