← Semua tutor

Hasil akhir · tombol kirim yang memberi kabar

Klik kirim, lalu apa?

Sebelum · tombol kirim biasa

Sesudah · tombol dari asset.inwebsite.online

Coba sendiri ↗

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

Yang perlu ada sebelum mulai.

  • Editor kode apa saja. VS Code, Cursor, atau yang kamu biasa pakai.
  • Node.js dan proyek React. Snippet berjalan di React dan memakai kelas Tailwind CSS.
  • AI agent (opsional, tapi paling cepat). Di panduan ini contohnya Claude Code.
  • Akun InWebAsset. Mencari dan merencanakan gratis tanpa login, tapi kode hanya bisa diambil setelah login.
Perlu loginTanpa login kamu bisa menjelajah, melihat pratinjau, dan memakai MCP untuk mencari. Tombol Copy kode dan API key MCP baru bisa dipakai setelah Sign in.
Batas harian: 5 salinanAkun gratis boleh menyalin kode snippet 5 kali sehari. Setiap salinan dihitung, termasuk snippet yang pernah kamu salin, dan kuotanya dipakai bersama antara situs dan MCP. Akun Pro tanpa batas.

Mencari, melihat pratinjau, dan perencanaan lewat agent tidak memakai kuota.

Langkah 2 · Jelajahi katalog

Pilih desain yang mau diambil.

Buka asset.inwebsite.online. Katalognya berisi 172 snippet. Persempit lewat kategori, tempat pemakaian, atau kata kunci.

  • Pilih kategori Micro Interactions.
  • Di Where it goes, pilih Form.
  • Ketik submit di kolom cari. Hasilnya tinggal satu kartu.
  • Arahkan kursor ke kartu untuk melihat pratinjaunya bergerak, lalu klik Submit Success Button.
REKAMAN · KATALOG
REKAMAN · HALAMAN SNIPPET

Langkah 3 · Buka halaman snippet

Lihat dulu, baru ambil.

  • Tab Preview: pratinjau memutar sendiri urutannya, dari Send invite, putaran loading, sampai Invite sent!. Tombol Replay mengulangnya.
  • Tab Code: sebelum login, kodenya terkunci. Muncul Sign in to get the code dan tombol Sign in to copy.
  • Assets in this snippet: ikon pesawat kertas dan delapan potongan confetti 3D. Klik salah satu untuk menukarnya dengan aset lain dari keluarga yang sama.
  • Install: npm i gsap @gsap/react. Syaratnya React + Tailwind CSS.
  • For agents: ID snippet, tautan registry, dan tombol Copy prompt untuk AI agent.
IngatUntuk menyalin kode kamu harus login, dan akun gratis dibatasi 5 salinan per hari.

Langkah 4 · Ambil kodenya

Dua jalur, pilih satu.

JALUR A · LEBIH CEPATPakai MCP lewat AI agent

Agent mencari, memilih, dan memasang snippet sendiri. Buka halaman For agents, login, buat API key, lalu salin perintahnya.

$claude mcp add --transport http InWebAsset https://asset.inwebsite.online/mcp --header "Authorization: Bearer <API key-mu>"

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.

JALUR B · MANUALSalin source code

Login, buka halaman snippet, tekan Copy di tab Code. Simpan sebagai komponen, lalu pasang sendiri atau kirim ke agent.

$ npm i gsap @gsap/react

Simpan sebagai components/SubmitSuccessButton.jsx. Tiap Copy memakai 1 dari 5 jatah harian akun gratis.

REKAMAN · HALAMAN FOR AGENTS

Langkah 5 · Beri konteks ke agent

Bilang di mana tombolnya dipasang.

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.

Bagian web form di #kontakFile Contact.jsxAmbil tombolnya sajaKirim simulasi 1,4 dtkWarna #e8461e, latar gelapStack React + CSS biasa
Kalau pakai Jalur B: tempel kodenya setelah prompt ini
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>
contoh · Claude Code (Jalur A)
# 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

Snippet itu titik awal.

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.
components/SubmitSuccessButton.jsx · terpasang di web contoh
/*
 * 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

Sebelum vs sesudah.

Sebelum

Sesudah

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.

01 · SIAPKANEditor, React, agent, akun (login)
02 · JELAJAHMicro Interactions → Form
03 · BUKAPratinjau, install, kode (login)
04 · PASANGBeri form, alur kirim, dan warna ke agent