Cool download button with animation: makes the download experience more intentional

Tombol download keren dengan animasi: bikin pengalaman unduh jadi lebih niat

Satu tombol download kecil yang bisa bikin website terasa lebih modern dan “serius dikerjakan”

Hello, friends,

Di ryanpratama.com, saya cukup sering ngobrol soal website, hosting, , dan ekosistem digital.

Salah satu detail kecil yang sering diremehkan tapi sebenarnya penting adalah tombol download. Biasanya kita cuma pakai tombol biru polos, teks “Download”, selesai.

Fungsional, tapi ya… biasa banget.

Padahal, dengan sedikit sentuhan animasi, tombol download bisa jadi:

  • Lebih menarik untuk diklik
  • Terasa lebih profesional
  • Ngasih feedback visual yang jelas ke user (“lagi mengunduh”, “berhasil”)

Di tulisan ini, saya mau ajak teman bongkar satu contoh tombol download keren dengan animasi dari CodePen milik Aaron Iker yang menurut saya keren dan wajib saya dokumentasikan kodenya.

Kalau mau bisa kita adaptasikan supaya gampang dipasang di website sendiri.

Berikut previewnya dari codepen

See the Pen Download Button Animation by Aaron Iker (@aaroniker) on CodePen.


Konsep tombol download yang akan kita pakai

Tombol ini punya beberapa karakter:

  • Ada teks bertahap: Download → Downloading → Open File
  • Ada icon di sebelah kanan yang berubah jadi tanda ceklis saat selesai
  • Ada efek “fill” di kotak kanan (indikasi progress)
  • Ada beberapa variasi style:
    • versi default (biru)
    • dark single
    • white single
    • dark penuh

Semua ini diatur pakai kombinasi:

  • HTML sederhana
  • CSS (hasil konversi dari SCSS)
  • JavaScript + GSAP untuk animasi SVG dan transisi

Struktur HTML tombol

Ini struktur dasar HTML yang bisa teman pakai di mana saja (WordPress, HTML biasa, dsb):

<div class="container">

    <!-- Tombol utama dengan teks 3 tahap -->
    <a href="#" class="button">
        <ul>
            <li>Download</li>
            <li>Downloading</li>
            <li>Open File</li>
        </ul>
        <div>
            <svg viewBox="0 0 24 24"></svg>
        </div>
    </a>

    <!-- Tombol icon saja - dark -->
    <a href="#" class="button dark-single">
        <div>
            <svg viewBox="0 0 24 24"></svg>
        </div>
    </a>

    <div></div>

    <!-- Tombol icon saja - white -->
    <a href="#" class="button white-single">
        <div>
            <svg viewBox="0 0 24 24"></svg>
        </div>
    </a>

    <!-- Tombol dark dengan teks 3 tahap -->
    <a href="#" class="button dark">
        <ul>
            <li>Download</li>
            <li>Downloading</li>
            <li>Open File</li>
        </ul>
        <div>
            <svg viewBox="0 0 24 24"></svg>
        </div>
    </a>

</div>

<!-- Kredit kecil (opsional) -->
<a class="dribbble" href="https://dribbble.com/shots/7299868-Download-Buttons" target="_blank">
    <img src="https://cdn.dribbble.com/assets/dribbble-ball-mark-2bd45f09c2fb58dbbfb44766d5d1d07c5a12972d602ef8b32204d28fa3dda554.svg" alt="">
</a>

Beberapa catatan:

  • Teks di dalam <ul> akan dianimasikan naik-turun, sehingga terlihat seakan-akan berubah status.
  • SVG di dalam tombol kanan akan kita isi lewat JavaScript (garis → ceklis).

CSS: gaya tombol + animasi dasar

Di CodePen, style-nya ditulis dalam SCSS. Di bawah ini sudah saya rapikan jadi CSS murni yang siap dipakai di browser tanpa proses build tambahan.

/* Variasi tombol */
.button.dark-single {
    --background: none;
    --rectangle: #242836;
    --success: #4BC793;
}

.button.white-single {
    --background: none;
    --rectangle: #F5F9FF;
    --arrow: #275efe;
    --success: #275efe;
    --shadow: rgba(10, 22, 50, .1);
}

.button.dark {
    --background: #242836;
    --rectangle: #1C212E;
    --arrow: #F5F9FF;
    --text: #F5F9FF;
    --success: #2F3545;
}

/* Style dasar tombol */
.button {
    --background: #275efe;
    --rectangle: #184fee;
    --success: #4672F1; /* hasil mix(white, #184fee, 20%) */
    --text: #fff;
    --arrow: #fff;
    --checkmark: #fff;
    --shadow: rgba(10, 22, 50, .24);

    display: flex;
    overflow: hidden;
    text-decoration: none;
    -webkit-mask-image: -webkit-radial-gradient(white, black);
    background: var(--background);
    border-radius: 8px;
    box-shadow: 0 2px 8px -1px var(--shadow);
    transition: transform .2s ease, box-shadow .2s ease;
}

.button:active {
    transform: scale(.95);
    box-shadow: 0 1px 4px -1px var(--shadow);
}

/* Teks 3 tahap di dalam tombol */
.button ul {
    margin: 0;
    padding: 16px 40px;
    list-style: none;
    text-align: center;
    position: relative;
    backface-visibility: hidden;
    font-size: 16px;
    font-weight: 500;
    line-height: 28px;
    color: var(--text);
}

.button ul li:not(:first-child) {
    top: 16px;
    left: 0;
    right: 0;
    position: absolute;
}

.button ul li:nth-child(2) {
    top: 76px;
}

.button ul li:nth-child(3) {
    top: 136px;
}

/* Kotak kanan (icon + background animasi) */
.button > div {
    position: relative;
    width: 60px;
    height: 60px;
    background: var(--rectangle);
}

.button > div::before,
.button > div::after {
    content: '';
    display: block;
    position: absolute;
}

.button > div::before {
    border-radius: 1px;
    width: 2px;
    top: 50%;
    left: 50%;
    height: 17px;
    margin: -9px 0 0 -1px;
    background: var(--arrow);
}

/* Layer animasi “progress” */
.button > div::after {
    width: 60px;
    height: 60px;
    transform-origin: 50% 0;
    border-radius: 0 0 80% 80%;
    background: var(--success);
    top: 0;
    left: 0;
    transform: scaleY(0);
}

/* Icon SVG di dalam kotak */
.button > div svg {
    display: block;
    position: absolute;
    width: 20px;
    height: 20px;
    left: 50%;
    top: 50%;
    margin: -9px 0 0 -10px;
    fill: none;
    z-index: 1;
    stroke-width: 2px;
    stroke: var(--arrow);
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* State loading: trigger animasi */
.button.loading ul {
    animation: text calc(var(--duration) * 1ms) linear forwards calc(var(--duration) * .065ms);
}

.button.loading > div::before {
    animation: line calc(var(--duration) * 1ms) linear forwards calc(var(--duration) * .065ms);
}

.button.loading > div::after {
    animation: background calc(var(--duration) * 1ms) linear forwards calc(var(--duration) * .065ms);
}

.button.loading > div svg {
    animation: svg calc(var(--duration) * 1ms) linear forwards calc(var(--duration) * .065ms);
}

/* Animasi teks (slide naik) */
@keyframes text {
    10%,
    85% {
        transform: translateY(-100%);
    }
    95%,
    100% {
        transform: translateY(-200%);
    }
}

/* Animasi garis vertikal */
@keyframes line {
    5%,
    10% {
        transform: translateY(-30px);
    }
    40% {
        transform: translateY(-20px);
    }
    65% {
        transform: translateY(0);
    }
    75%,
    100% {
        transform: translateY(30px);
    }
}

/* Animasi icon SVG (garis → ceklis) */
@keyframes svg {
    0%,
    20% {
        stroke-dasharray: 0;
        stroke-dashoffset: 0;
    }
    21%,
    89% {
        stroke-dasharray: 26px;
        stroke-dashoffset: 26px;
        stroke-width: 3px;
        margin: -10px 0 0 -10px;
        stroke: var(--checkmark);
    }
    100% {
        stroke-dasharray: 26px;
        stroke-dashoffset: 0;
        margin: -10px 0 0 -10px;
        stroke: var(--checkmark);
    }
    12% {
        opacity: 1;
    }
    20%,
    89% {
        opacity: 0;
    }
    90%,
    100% {
        opacity: 1;
    }
}

/* Animasi background progress */
@keyframes background {
    10% {
        transform: scaleY(0);
    }
    40% {
        transform: scaleY(.15);
    }
    65% {
        transform: scaleY(.5);
        border-radius: 0 0 50% 50%;
    }
    75% {
        border-radius: 0 0 50% 50%;
    }
    90%,
    100% {
        border-radius: 0;
    }
    75%,
    100% {
        transform: scaleY(1);
    }
}

/* Reset & layout dasar halaman */
html {
    box-sizing: border-box;
    -webkit-font-smoothing: antialiased;
}

*,
*::before,
*::after {
    box-sizing: inherit;
}

body {
    min-height: 100vh;
    display: flex;
    font-family: 'Roboto', Arial, sans-serif;
    justify-content: center;
    align-items: center;
    background: #E4ECFA;
}

/* Wrapper tombol */
body .container {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
}

body .container > div {
    flex-basis: 100%;
    width: 0;
}

body .container .button {
    margin: 16px;
}

@media (max-width: 400px) {
    body .container .button {
        margin: 12px;
    }
}

/* Kredit Dribbble (opsional) */
body .dribbble {
    position: fixed;
    display: block;
    right: 20px;
    bottom: 20px;
}

body .dribbble img {
    display: block;
    height: 28px;
}

JavaScript: menghidupkan animasi dengan GSAP

Bagian paling “ajaib” ada di JavaScript. Di sini kita:

  • Menentukan durasi animasi
  • Menggambar path SVG (garis → ceklis) secara dinamis
  • Menambahkan class .loading ke tombol saat diklik
  • Menggunakan GSAP untuk mengatur smoothing dan pergerakan garis

Pastikan teman menambahkan GSAP terlebih dahulu:

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>

Lalu tambahkan script berikut (boleh di bawah HTML, sebelum </body>):

<script>
document.querySelectorAll('.button').forEach(button => {

    let duration = 3000,
        svg = button.querySelector('svg'),
        svgPath = new Proxy({
            y: null,
            smoothing: null
        }, {
            set(target, key, value) {
                target[key] = value;
                if (target.y !== null && target.smoothing !== null) {
                    svg.innerHTML = getPath(target.y, target.smoothing, null);
                }
                return true;
            },
            get(target, key) {
                return target[key];
            }
        });

    button.style.setProperty('--duration', duration);

    svgPath.y = 20;
    svgPath.smoothing = 0;

    button.addEventListener('click', e => {
        e.preventDefault();

        if (!button.classList.contains('loading')) {

            button.classList.add('loading');

            gsap.to(svgPath, {
                smoothing: .3,
                duration: (duration * .065) / 1000
            });

            gsap.to(svgPath, {
                y: 12,
                duration: (duration * .265) / 1000,
                delay: (duration * .065) / 1000,
                ease: "elastic.out(1.12, 0.4)"
            });

            setTimeout(() => {
                svg.innerHTML = getPath(0, 0, [
                    [3, 14],
                    [8, 19],
                    [21, 6]
                ]);
            }, duration / 2);
        }
    });
});

function getPoint(point, i, a, smoothing) {
    let cp = (current, previous, next, reverse) => {
            let p = previous || current,
                n = next || current,
                o = {
                    length: Math.sqrt(Math.pow(n[0] - p[0], 2) + Math.pow(n[1] - p[1], 2)),
                    angle: Math.atan2(n[1] - p[1], n[0] - p[0])
                },
                angle = o.angle + (reverse ? Math.PI : 0),
                length = o.length * smoothing;
            return [
                current[0] + Math.cos(angle) * length,
                current[1] + Math.sin(angle) * length
            ];
        },
        cps = cp(a[i - 1], a[i - 2], point, false),
        cpe = cp(point, a[i - 1], a[i + 1], true);
    return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`;
}

function getPath(update, smoothing, pointsNew) {
    let points = pointsNew ? pointsNew : [
            [4, 12],
            [12, update],
            [20, 12]
        ],
        d = points.reduce((acc, point, i, a) => 
            i === 0 
                ? `M ${point[0]},${point[1]}` 
                : `${acc} ${getPoint(point, i, a, smoothing)}`, 
            ''
        );
    return `<path d="${d}" />`;
}
</script>

Cara pakai di WordPress atau proyek teman

Kurang lebih langkahnya seperti ini:

  1. Copy HTML ke:
    • Halaman custom HTML
    • Atau ke template theme (misalnya single.php, page.php)
  2. Copy CSS ke:
    • Customizer → Additional CSS, atau
    • File CSS tema/child theme, atau
    • File CSS di dalam builder (Elementor, dsb)
  3. Tambah GSAP + Script:
    • Tempel <script src="...gsap.min.js"></script> dan script JavaScript di footer theme, atau
    • Gunakan plugin “Insert Headers and Footers” lalu taruh di bagian footer scripts.
  4. Ubah teks sesuai kebutuhan:
    • “Download” → “Download CV”
    • “Open File” → “Buka File” atau “Lihat Hasil”
  5. Kalau mau, teman bisa:
    • Menonaktifkan beberapa tombol dan hanya pakai satu variasi
    • Ubah warna variabel CSS (--background, --rectangle, dsb) supaya sesuai brand warna website.

Kenapa detail kecil seperti ini layak dikerjakan?

Buat saya, tombol seperti ini bukan sekadar “gaya-gayaan”.

Dia memberi:

  • Feedback yang jelas: user tahu kapan proses dimulai dan kapan selesai.
  • Rasa “niat”: memberi kesan bahwa pemilik website peduli dengan pengalaman pengunjung.
  • Branding halus: lewat warna dan gerakan, website terasa lebih hidup dan modern.

Tentu saja, teman tidak wajib pakai tombol animasi seperti ini di semua halaman. Tapi di beberapa titik penting:

  • Halaman download resource gratis
  • Tombol download e-book atau file penting
  • Halaman portfolio (download CV, proposal, dsb.)

…tombol seperti ini bisa jadi sentuhan kecil yang membuat website terasa beda.

Pada akhirnya, saya percaya desain web yang baik sering lahir dari perpaduan:

  • fungsi yang jelas,
  • ditambah detail kecil yang memperkaya pengalaman.

Kalau teman tertarik ngoprek lebih jauh, sumber asli tombol ini ada di

CodePen:
https://codepen.io/aaroniker/pen/MWgNKGr

Silakan dimodifikasi, disesuaikan, dan dijadikan bagian dari ekosistem digital yang sedang teman bangun.

Previous Article

From Predictive AI to AI Agents: When Machines No Longer Just Answer Questions

Next Article

One Punch Man Season 3: When High Expectations Meet “Lackluster” Animation”

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *