Hasil akhir · tanpa menulis efeknya sendiri
Dua web yang sama, bedanya satu snippet. Gerakkan kursor di kartu kanan: titik-titik di belakang hero ikut bereaksi. Di bawah ini caranya dari nol, langkah demi langkah.
Langkah 1 · Siapkan dulu
Mencari, melihat pratinjau, dan perencanaan lewat agent tidak memakai kuota.
Langkah 2 · Jelajahi katalog
Buka asset.inwebsite.online. Katalognya berisi 172 snippet. Persempit lewat kategori, tempat pemakaian, atau kata kunci.
Langkah 3 · Buka halaman snippet
npm i gsap @gsap/react. Syaratnya React + Tailwind CSS.Langkah 4 · Ambil kodenya
Agent mencari, memilih, dan memasang snippet sendiri. Buka halaman For agents, login, buat API key, lalu salin perintahnya.
Perintah di atas untuk Claude Code. Halaman itu juga punya versi Claude Desktop, Cursor, dan VS Code. Jangan pernah membagikan API key-mu.
Mencari dan perencanaan tanpa kunci. Kodenya (get_snippet) baru keluar dengan API key, dan memakai jatah 5 salinan.
Login, buka halaman snippet, tekan Copy di tab Code. Simpan sebagai komponen, lalu pasang sendiri atau kirim ke agent.
Simpan sebagai components/DotGridSpotlight.tsx. Tiap Copy memakai 1 dari 5 jatah harianmu.
Langkah 5 · Beri konteks ke agent
Sebelum mengirim, kasih tahu agent bagian web mana yang mau diubah. Misalnya hero section. Makin jelas konteksnya, makin sedikit salinan yang terbuang dari jatah 5 harianmu.
Ini kode komponen dari asset.inwebsite.online. Taruh sebagai latar belakang hero section di src/sections/Hero.jsx, di belakang judul dan tombol. Proyek saya React + CSS biasa tanpa Tailwind, jadi sesuaikan. Aksen #e8461e. <tempel kode di sini>
# sekali saja: sambungkan MCP $ claude mcp add --transport http InWebAsset https://asset.inwebsite.online/mcp --header "Authorization: Bearer <API key-mu>" # masuk ke folder proyek, jalankan agent $ claude > Pakai InWebAsset MCP. Ambil snippet backgrounds/dot-grid-spotlight dan pasang sebagai latar belakang hero section di src/sections/Hero.jsx, di belakang judul dan tombol (teks tetap bisa diklik). Proyek saya React + CSS biasa, tanpa Tailwind, jadi sesuaikan kodenya. Warna aksen #e8461e, latar terang #f1ede4. Cek dulu snippet-nya dengan includeCode:false, lalu ambil kodenya sekali saja karena jatah salinanku 5 per hari. Jangan ubah bagian lain.
Yang kamu dapat
Efeknya: titik di dekat kursor membesar, berubah warna, dan condong ke arahnya. Ini versi sederhana supaya hidup di halaman ini. Kode aslinya di kanan.
/*
* Dot Grid Spotlight
* A canvas dot grid where dots near the pointer grow, glow in an accent color, and lean toward it like a magnetic field, under a soft spotlight. Works in light and dark mode.
*
* id: backgrounds/dot-grid-spotlight · v1.0.0
* source: https://asset.inwebsite.online/snippets/backgrounds/dot-grid-spotlight
*
* Install: npm i gsap @gsap/react
* Requires: React + Tailwind CSS.
*/
"use client";
import { useRef, type PointerEvent, type ReactNode } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
const TAU = Math.PI * 2;
type DotGridSpotlightProps = {
/** Distance between dots in px. */
gap?: number;
/** Dot color. Defaults to the panel's text color (neutral, follows light/dark mode). */
dotColor?: string;
/** Highlight color for dots near the pointer and for the spotlight. */
accent?: string;
/** Reach of the pointer's pull in px. */
radius?: number;
/** Base dot radius in px. */
dotSize?: number;
/** A virtual pointer traces a Lissajous loop while the real one is away. */
autoPlay?: boolean;
children?: ReactNode;
className?: string;
};
function SampleContent({ accent }: { accent: string }) {
return (
<div className="flex h-full flex-col items-center justify-center px-6 text-center">
<span className="rounded-full bg-white/80 px-3 py-1 text-xs font-medium text-neutral-600 ring-1 ring-black/5 backdrop-blur dark:bg-neutral-900/80 dark:text-neutral-300 dark:ring-white/10">
Pointer-reactive background
</span>
<h2 className="mt-4 max-w-xl text-4xl leading-[1.05] font-semibold tracking-tight text-neutral-900 sm:text-6xl dark:text-white">
Every dot feels you <span style={{ color: accent }}>coming.</span>
</h2>
<p className="mt-4 max-w-md text-sm text-neutral-600 sm:text-base dark:text-neutral-400">
A magnetic dot field with a soft spotlight. Move your cursor across it.
</p>
</div>
);
}
export default function DotGridSpotlight({
gap = 22,
dotColor,
accent = "#ff5a36",
radius = 150,
dotSize = 1.4,
autoPlay = false,
children,
className = "",
}: DotGridSpotlightProps) {
const root = useRef<HTMLDivElement>(null);
const canvas = useRef<HTMLCanvasElement>(null);
const handlers = useRef<{ move: (x: number, y: number) => void; leave: () => void } | null>(null);
useGSAP(
(_context, contextSafe) => {
const el = root.current;
const cv = canvas.current;
const ctx = cv?.getContext("2d");
const spotEl = el?.querySelector(".dgs-spot");
if (!el || !cv || !ctx || !spotEl || !contextSafe) return;
let w = 0;
let h = 0;
let dpr = 1;
let dots: number[] = [];
const p = { x: 0, y: 0, power: 0 };
const setSpotX = gsap.quickSetter(spotEl, "x", "px");
const setSpotY = gsap.quickSetter(spotEl, "y", "px");
const setSpotO = gsap.quickSetter(spotEl, "opacity");
let color = "";
let colorAge = 0;
const draw = () => {
// Re-read the (theme-dependent) text color every ~half second.
if (colorAge-- <= 0) {
color = dotColor ?? getComputedStyle(cv).color;
colorAge = 30;
}
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
const lit: number[] = [];
ctx.fillStyle = color;
ctx.beginPath();
for (let i = 0; i < dots.length; i += 2) {
let x = dots[i];
let y = dots[i + 1];
let r = dotSize;
const dx = p.x - x;
const dy = p.y - y;
const d = Math.hypot(dx, dy);
if (p.power > 0.01 && d < radius) {
// Smoothstep falloff: dots grow and lean toward the pointer like iron filings.
let f = 1 - d / radius;
f = f * f * (3 - 2 * f) * p.power;
x += dx * f * 0.3;
y += dy * f * 0.3;
r += f * dotSize * 2.6;
lit.push(x, y, r, f);
}
ctx.moveTo(x + r, y);
ctx.arc(x, y, r, 0, TAU);
}
ctx.fill();
ctx.fillStyle = accent;
for (let i = 0; i < lit.length; i += 4) {
ctx.globalAlpha = Math.min(1, lit[i + 3] * 1.3);
ctx.beginPath();
ctx.arc(lit[i], lit[i + 1], lit[i + 2], 0, TAU);
ctx.fill();
}
ctx.globalAlpha = 1;
setSpotX(p.x);
setSpotY(p.y);
setSpotO(p.power);
};
const resize = () => {
w = el.clientWidth;
h = el.clientHeight;
dpr = Math.min(2, window.devicePixelRatio || 1);
cv.width = Math.round(w * dpr);
cv.height = Math.round(h * dpr);
const cols = Math.floor(w / gap);
const rows = Math.floor(h / gap);
const ox = (w - cols * gap) / 2;
const oy = (h - rows * gap) / 2;
dots = [];
for (let r = 0; r <= rows; r++) for (let c = 0; c <= cols; c++) dots.push(ox + c * gap, oy + r * gap);
draw();
};
const ro = new ResizeObserver(resize);
ro.observe(el);
resize();
p.x = w / 2;
p.y = h / 2;
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
const toX = gsap.quickTo(p, "x", { duration: 0.55, ease: "power3.out" });
const toY = gsap.quickTo(p, "y", { duration: 0.55, ease: "power3.out" });
let resume: gsap.core.Tween | null = null;
let idle: gsap.core.Tween | null = null;
if (autoPlay) {
// Virtual pointer on a 3:2 Lissajous curve.
const lj = { t: 0 };
gsap.to(p, { power: 1, duration: 0.8, delay: 0.2 });
idle = gsap.to(lj, {
t: TAU,
duration: 10,
ease: "none",
repeat: -1,
onUpdate: () => {
toX(w / 2 + w * 0.36 * Math.sin(3 * lj.t + Math.PI / 2));
toY(h / 2 + h * 0.32 * Math.sin(2 * lj.t));
},
});
}
handlers.current = {
move: contextSafe((x: number, y: number) => {
resume?.kill();
idle?.pause();
toX(x);
toY(y);
gsap.to(p, { power: 1, duration: 0.3, overwrite: "auto" });
}),
leave: contextSafe(() => {
if (idle) resume = gsap.delayedCall(1.2, () => idle?.resume());
else gsap.to(p, { power: 0, duration: 0.7, ease: "power2.out", overwrite: "auto" });
}),
};
gsap.ticker.add(draw);
return () => {
gsap.ticker.remove(draw);
handlers.current = null;
};
});
return () => ro.disconnect();
},
{ scope: root, dependencies: [gap, dotColor, accent, radius, dotSize, autoPlay] },
);
const onPointerMove = (e: PointerEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
handlers.current?.move(e.clientX - rect.left, e.clientY - rect.top);
};
const spot = radius * 2.6;
return (
<div
ref={root}
onPointerMove={onPointerMove}
onPointerLeave={() => handlers.current?.leave()}
className={`relative isolate h-[420px] w-full max-w-5xl overflow-hidden rounded-3xl bg-[#fafaf9] text-neutral-300 ring-1 ring-black/5 dark:bg-[#0b0b0d] dark:text-neutral-700 dark:ring-white/10 ${className}`}
>
<div
aria-hidden
className="dgs-spot pointer-events-none absolute top-0 left-0 -z-10 rounded-full opacity-0"
style={{
width: spot,
height: spot,
marginLeft: -spot / 2,
marginTop: -spot / 2,
background: `radial-gradient(closest-side, color-mix(in srgb, ${accent} 26%, transparent), transparent)`,
}}
/>
<canvas
ref={canvas}
aria-hidden
className="pointer-events-none absolute inset-0 -z-10 size-full [mask-image:radial-gradient(ellipse_at_center,black_55%,transparent_100%)]"
/>
<div className="relative h-full">{children ?? <SampleContent accent={accent} />}</div>
</div>
);
}
Langkah 6 · Bandingkan
Mau hero seperti ini tanpa memasangnya sendiri? Tim InWebsite bisa bikinin web-mu.
Lihat paket InWebsite →