Hasil akhir · tombol kirim yang memberi kabar
Dua web yang sama, bedanya satu tombol. Di kanan, setelah form diisi dan Kirim pesan ditekan, tombolnya berputar selama pesan dikirim, lalu muncul centang, confetti, dan tulisan Pesan terkirim. Di kiri, tombol lama membuka WhatsApp di tab baru, jadi halamannya sendiri tidak memberi tanda apa-apa.
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 akun gratis.
Login, buka halaman snippet, tekan Copy di tab Code. Simpan sebagai komponen, lalu pasang sendiri atau kirim ke agent.
Simpan sebagai components/SubmitSuccessButton.jsx. Tiap Copy memakai 1 dari 5 jatah harian akun gratis.
Langkah 5 · Beri konteks ke agent
Snippet aslinya kartu undangan lengkap: judul, kolom email, dan tombol Send invite. Web contoh sudah punya form kontak, jadi yang diambil hanya tombolnya. Agent perlu tahu form mana, apa yang terjadi saat dikirim, dan warnanya. Tanpa itu, kamu dapat kartu undangan baru di tengah halaman.
Ini kode komponen dari asset.inwebsite.online. Pakai tombolnya saja (buang kartu, judul, kolom email, dan ikon pesawat) untuk menggantikan tombol "Kirim lewat WhatsApp" di form section #kontak (src/sections/Contact.jsx). Field wajib di form tetap divalidasi browser dulu. Situs ini belum punya backend: buat fungsi kirim yang untuk sementara hanya menunggu 1,4 detik, dan beri komentar di mana fetch asli nanti disambungkan. Section kontak berlatar gelap, aksen #e8461e, sudut bulat, gerak tenang. Proyek saya React + CSS biasa tanpa Tailwind: pasang Tailwind v4 tanpa preflight supaya gaya lama tidak berubah. Jangan ubah bagian lain. <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 micro-motion/submit-success-button dan pakai tombolnya saja untuk menggantikan tombol "Kirim lewat WhatsApp" di form section #kontak (src/sections/Contact.jsx). Validasi field wajib tetap jalan. Belum ada backend, jadi kirimannya disimulasikan 1,4 detik, dengan komentar di tempat fetch asli nanti disambungkan. Proyek saya React + CSS biasa tanpa Tailwind, jadi pasang Tailwind v4 tanpa preflight. Kirim look situs (gelap, aksen #e8461e, sudut bulat, gerak smooth, tempo sedang) ke get_snippet dan terapkan saran "Fit this site". Cek dulu dengan includeCode:false, lalu ambil kodenya sekali. Jangan ubah bagian lain.
Yang kamu dapat
Kode di kanan adalah komponen yang terpasang di web contoh. Ini yang diubah dari versi asli di katalog:
kartujudul, kolom email, dan ikon pesawat dibuang. Yang dipakai tombolnya saja, di dalam form kontak yang sudah ada.validasitombol membiarkan browser memeriksa field wajib (Nama, Nama usaha) dulu. Kalau masih kosong, tombol tidak berputar.kirimweb contoh tidak punya backend, jadi kirimannya disimulasikan 1,4 detik. Di situs sungguhan, isi fungsi kirim di Contact.jsx dengan fetch ke endpoint form-mu dan lempar error bila gagal (tombol jadi merah, lalu "Coba lagi"). Contohnya ada di komentar atas kode.WhatsApptombol lama membuka WhatsApp di tab baru. Di versi ini alur itu dilepas. Kalau memasangnya di situs asli, ubah juga kalimat "terkirim lewat WhatsApp" di intro.warnabiru #3d5afe jadi aksen #e8461e. Hijau status sukses diganti warna kertas #f1ede4 dengan teks gelap, supaya tetap kontras di latar gelap.confettipotongan biru dan ungu ditukar dengan yang oranye, emas, kuning, dan pink dari keluarga particles-confetti-3d yang sama.tempodurasi dikali 1,25 sesuai saran "Fit this site" dari get_snippet. Ease elastic dan back diganti power3.out supaya geraknya tenang seperti bagian lain situs.autoPlaymode demo berulang dan cabang reduced-motion dibuang. Tombol hanya bergerak setelah diklik.Tailwindweb contoh tidak memakai Tailwind. Dipasang v4 tanpa preflight (hanya theme + utilities) agar gaya lama tetap./*
* Submit Success Button — diadaptasi untuk Tandarupa (form kontak, latar gelap)
* Asal: https://asset.inwebsite.online/snippets/micro-motion/submit-success-button (v1.0.1)
* Perubahan: hanya tombolnya (kartu undangan + input email + ikon pesawat dibuang), dipasang di form yang sudah ada,
* warna violet/indigo/amber → aksen #e8461e, sukses = kertas #f1ede4, confetti hangat (oranye, emas, pink),
* durasi ×1,25 dan ease elastic/back → power3.out sesuai saran "Fit this site", tanpa mode autoPlay.
*
* Pemakaian di src/sections/Contact.jsx (di dalam <form>, menggantikan tombol "Kirim lewat WhatsApp"):
* // Web contoh tidak punya backend, jadi kirimannya disimulasikan 1,4 dtk:
* const kirim = () => new Promise((resolve) => setTimeout(resolve, 1400))
* // Di situs sungguhan, sambungkan kiriman asli di sini; lempar error bila gagal:
* // const kirim = async () => {
* // const r = await fetch('/api/kontak', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...form, paket: pkg }) })
* // if (!r.ok) throw new Error('gagal')
* // }
* <SubmitSuccessButton className="mt-2 justify-self-start" onSubmit={kirim} />
*/
import { useRef, useState } from 'react'
import gsap from 'gsap'
import { useGSAP } from '@gsap/react'
import { DrawSVGPlugin } from 'gsap/DrawSVGPlugin'
import { Physics2DPlugin } from 'gsap/Physics2DPlugin'
gsap.registerPlugin(useGSAP, DrawSVGPlugin, Physics2DPlugin)
const CDN = 'https://inassetweb-cdn.inwebsite.online/sprinkles/v1/particles/'
const CONFETTI = ['confetti-square', 'confetti-star', 'confetti-pill', 'confetti-strip', 'confetti-foil', 'confetti-curl'].map(
(n) => CDN + n + '.png',
)
const PIECES = 24
const IDLE = '#e8461e' // aksen situs
const IDLE_TEXT = '#f1ede4'
const SUCCESS = '#f1ede4' // kertas: kontras di latar gelap
const SUCCESS_TEXT = '#141311'
const ERROR = '#f04438'
function Arrow() {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden className="size-4 transition-transform duration-300 group-hover:translate-x-1">
<path d="M3 10h12m-4.5-5L15 10l-4.5 5" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}
function Tick() {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden className="size-5">
<path d="M4.5 10.5l3.5 3.5 7.5-8" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}
/**
* Tombol kirim untuk <form> yang sudah ada. Taruh di dalam form; validasi bawaan form (required) tetap jalan.
* onSubmit: () => Promise. Resolve = sukses (centang + confetti), reject/throw = gagal (merah + goyang).
*/
export default function SubmitSuccessButton({ onSubmit, labels, className = '' }) {
const text = { idle: 'Kirim pesan', success: 'Pesan terkirim', error: 'Coba lagi', ...labels }
const root = useRef(null)
const busy = useRef(false)
const [status, setStatus] = useState('')
const { contextSafe } = useGSAP(
() => {
gsap.set('.ssb-ring, .ssb-check, .ssb-done, .ssb-err, .ssb-bit', { autoAlpha: 0 })
gsap.set('.ssb-arc', { drawSVG: '0% 0%' })
},
{ scope: root },
)
const run = contextSafe(() => {
if (busy.current) return
busy.current = true
const el = root.current
const q = gsap.utils.selector(el)
const btn = el.querySelector('.ssb-btn')
const full = btn.offsetWidth
setStatus('Mengirim…')
const burst = () => {
gsap.utils.toArray('.ssb-bit', el).forEach((bit) => {
gsap.fromTo(
bit,
{ x: 0, y: 0, scale: 0.2, rotation: 0, rotationX: 0, autoAlpha: 1 },
{
scale: gsap.utils.random(0.7, 1.15),
rotation: gsap.utils.random(-540, 540),
rotationX: gsap.utils.random(-360, 360),
physics2D: { velocity: gsap.utils.random(320, 640), angle: gsap.utils.random(-170, -10), gravity: 520, friction: 0.05 },
duration: 2,
},
)
gsap.to(bit, { autoAlpha: 0, duration: 0.5, delay: gsap.utils.random(1.25, 1.6) })
})
}
let spin = null
const finish = (ok) => {
spin?.kill()
if (!root.current) return // komponen sudah dilepas saat kiriman berjalan
const label = ok ? '.ssb-done' : '.ssb-err'
setStatus(ok ? text.success : text.error)
const tl = gsap.timeline({
onComplete: () => {
gsap.set(btn, { clearProps: 'width,backgroundColor,color,scale' })
busy.current = false
setStatus('')
},
})
tl.to(q('.ssb-arc'), { drawSVG: '0% 100%', duration: 0.38, ease: 'power2.out' })
.to(
btn,
{ backgroundColor: ok ? SUCCESS : ERROR, color: ok ? SUCCESS_TEXT : IDLE_TEXT, scale: 1.1, duration: 0.31, ease: 'power2.out' },
'-=0.08',
)
.to(q('.ssb-ring'), { autoAlpha: 0, scale: 1.4, duration: 0.38 }, '<')
if (ok) {
tl.set(q('.ssb-check'), { autoAlpha: 1, scale: 1 }, '<')
.fromTo(q('.ssb-tick'), { drawSVG: '0%' }, { drawSVG: '100%', duration: 0.44, ease: 'power2.out' }, '<0.05')
.to(btn, { scale: 1, duration: 0.88, ease: 'power3.out' }, '<')
.add(burst, '<')
} else {
tl.to(btn, { scale: 1, duration: 0.25 }, '<').to(btn, {
keyframes: { x: [0, -10, 9, -7, 5, -2, 0] },
duration: 0.62,
ease: 'none',
})
}
tl.to(q('.ssb-check'), { autoAlpha: 0, scale: 0.4, duration: 0.25, ease: 'power2.in' }, '+=0.45')
.to(btn, { width: full, duration: 0.69, ease: 'power3.inOut' }, '<')
.fromTo(q(label), { yPercent: 70, autoAlpha: 0 }, { yPercent: 0, autoAlpha: 1, duration: 0.5, ease: 'power3.out' }, '-=0.25')
.to(q(label), { yPercent: -70, autoAlpha: 0, duration: 0.31, ease: 'power2.in' }, '+=2')
.to(btn, { backgroundColor: IDLE, color: IDLE_TEXT, duration: 0.44 }, '<')
.fromTo(q('.ssb-idle'), { yPercent: 70, autoAlpha: 0 }, { yPercent: 0, autoAlpha: 1, duration: 0.5, ease: 'power3.out' })
.set(q('.ssb-check'), { scale: 1, autoAlpha: 0 })
}
// Menyusut jadi lingkaran berputar selama kiriman berjalan.
gsap
.timeline()
.to(btn, { scale: 0.94, duration: 0.1, ease: 'power2.out' })
.to(btn, { scale: 1, duration: 0.62, ease: 'power3.out' })
.to(q('.ssb-idle'), { yPercent: -70, autoAlpha: 0, duration: 0.28, ease: 'power2.in' }, 0)
.to(btn, { width: btn.offsetHeight, duration: 0.62, ease: 'power3.inOut' }, 0.1)
.fromTo(q('.ssb-ring'), { autoAlpha: 0, scale: 0.5 }, { autoAlpha: 1, scale: 1, duration: 0.38, ease: 'power3.out' }, 0.52)
.fromTo(q('.ssb-arc'), { drawSVG: '0% 0%' }, { drawSVG: '0% 30%', duration: 0.5 }, 0.52)
spin = gsap.to(q('.ssb-arc'), { rotation: 360, svgOrigin: '28 28', duration: 0.94, ease: 'none', repeat: -1 })
const request = Promise.resolve().then(() => (onSubmit ? onSubmit() : new Promise((r) => gsap.delayedCall(1.3, r))))
const minimum = new Promise((r) => gsap.delayedCall(1.2, r)) // putaran tampil minimal 1,2 dtk
Promise.all([request, minimum]).then(
contextSafe(() => finish(true)),
contextSafe(() => finish(false)),
)
})
// Klik (atau Enter di kolom isian) → biarkan browser memvalidasi field wajib dulu; kalau lolos, jalankan kiriman.
const handleClick = (e) => {
if (busy.current) return e.preventDefault()
const form = e.currentTarget.form
if (form && !form.checkValidity()) return
e.preventDefault()
run()
}
return (
<div ref={root} className={`relative ${className}`}>
<button
type="submit"
onClick={handleClick}
aria-disabled={status !== ''}
className="ssb-btn group relative inline-flex h-14 cursor-pointer items-center justify-center overflow-hidden rounded-full border-0 bg-[#e8461e] px-7 text-base font-medium text-[#f1ede4] shadow-[0_14px_28px_-14px_rgba(0,0,0,0.7)] outline-none transition-[filter] duration-300 hover:brightness-110 focus-visible:ring-4 focus-visible:ring-[#e8461e]/40"
>
{/* Penentu lebar: label terpanjang, tidak terlihat */}
<span aria-hidden className="invisible grid whitespace-nowrap">
<span className="col-start-1 row-start-1 flex items-center gap-2.5">
{text.idle}
<Arrow />
</span>
<span className="col-start-1 row-start-1 flex items-center gap-2">
<Tick />
{text.success}
</span>
</span>
<span className="ssb-idle absolute inset-0 flex items-center justify-center gap-2.5 whitespace-nowrap">
{text.idle}
<Arrow />
</span>
<span className="ssb-done invisible absolute inset-0 flex items-center justify-center gap-2 whitespace-nowrap">
<Tick />
{text.success}
</span>
<span className="ssb-err invisible absolute inset-0 flex items-center justify-center whitespace-nowrap">{text.error}</span>
<svg viewBox="0 0 56 56" aria-hidden className="ssb-ring invisible absolute inset-0 m-auto size-14" fill="none">
<circle cx="28" cy="28" r="17" stroke="currentColor" strokeOpacity="0.25" strokeWidth="3.5" />
<circle className="ssb-arc" cx="28" cy="28" r="17" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" />
</svg>
<svg viewBox="0 0 56 56" aria-hidden className="ssb-check invisible absolute inset-0 m-auto size-14" fill="none">
<path className="ssb-tick" d="M18 29l7 7 13-15" stroke="currentColor" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{Array.from({ length: PIECES }, (_, i) => (
<img
key={i}
src={CONFETTI[i % CONFETTI.length]}
alt=""
aria-hidden
className="ssb-bit pointer-events-none invisible absolute top-7 left-7 z-10 -mt-3.5 -ml-3.5 size-7 max-w-none"
/>
))}
<p aria-live="polite" className="sr-only">
{status}
</p>
</div>
)
}
Langkah 6 · Bandingkan
Di layar lebar, kartu kanan hidup: isi Nama dan Nama usaha, lalu tekan Kirim pesan. Di HP, kartu menampilkan rekamannya. Tombol di kartu kiri tetap membuka WhatsApp di tab baru, seperti aslinya.
Mau form kontak yang terasa hidup tanpa memasangnya sendiri? Tim InWebsite bisa bikinin web-mu.
Lihat paket InWebsite →