Download Button

Documentation
Webflow
Code
Setup: External Scripts
External Scripts in Webflow
Make sure to always put the External Scripts before the Javascript step of the resource.
In this video you learn where to put these in your Webflow project? Or how to include a paid GSAP Club plugin in your project?
HTML
Step 1: Copy structure to Webflow
Copy structure to Webflow
In the video below we described how you can copy + paste the structure of this resource to your Webflow project.
Copy to Webflow
Webflow structure is not required for this resource.
Step 1: Add HTML
HTML
<button data-download-src="https://vz-6ed806ff-5e5.b-cdn.net/9e75ac7b-ca03-4b9e-a472-0898d863593d/playlist.m3u8" data-download-name="osmo-logo.jpg" class="download-btn">
<span data-download-icon-wrap="" class="download-btn__icon-hold">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 16 16" fill="none" data-download-icon="" class="download-btn__icon">
<g clip-path="url(#clip0_1363_5581)">
<path d="M8.74902 10.7734L12.4688 7.05371L13.5293 8.11426L7.99902 13.6445L2.46875 8.11426L3.5293 7.05371L7.24902 10.7734V0.0830078H8.74902V10.7734Z" fill="currentColor" data-download-arrow=""></path>
<path d="M15.5 14.75V16.25H0.5V14.75H15.5Z" fill="currentColor" data-download-base=""></path>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 20 20" fill="none" data-download-success="" class="download-btn__icon is--success">
<path d="M2 9.5L8 15.5L19 4.5" stroke="currentColor" stroke-width="1.75" stroke-miterlimit="10" stroke-dasharray="25" stroke-dashoffset="25"></path>
</svg>
</span>
<span data-download-label="" class="download-btn__label">Download</span>
</button>HTML structure is not required for this resource.
Step 2: Add CSS
CSS
.download-btn {
grid-column-gap: .625em;
grid-row-gap: .625em;
color: #f2f2f2;
background-color: #0065e1;
border-radius: .5em;
justify-content: center;
align-items: center;
padding: .75em 1.5em .75em 1em;
display: flex;
}
.download-btn__label {
font-size: 1.5em;
font-weight: 500;
line-height: 1.2;
}
.download-btn__icon-hold {
background-color: #fff3;
border-radius: 100em;
flex: none;
justify-content: center;
align-items: center;
width: 2.5em;
height: 2.5em;
padding: 0;
display: flex;
}
.download-btn__icon {
justify-content: center;
align-items: center;
width: 1em;
height: 1em;
display: flex;
overflow: visible !important;
}
.download-btn__icon.is--success {
position: absolute;
}
/* Transition settings */
[data-download-src]{
transition: 0.25s background-color ease;
}
[data-download-arrow], [data-download-base]{
transition: 0.5s transform cubic-bezier(0.625, 0.05, 0, 1);
}
[data-download-icon-wrap]{
clip-path: inset(0em round 100em);
transition: 0.5s clip-path cubic-bezier(0.625, 0.05, 0, 1);
}
[data-download-success] path{
transition: 0.4s stroke-dashoffset cubic-bezier(0.625, 0.05, 0, 1);
}
[data-download-base]{
transform-origin: center center;
}
/* When status is 'downloading' */
[data-download-src][data-download-state="downloading"]{
pointer-events: none;
}
body:has([data-download-src][data-download-state="downloading"]){
cursor: waiting;
}
/* When status is 'ready' or 'fallback' */
[data-download-src][data-download-state="ready"] [data-download-icon-wrap]{
transition-delay: 0.15s;
clip-path: inset(0.35em round 100em);
}
[data-download-src][data-download-state="ready"] [data-download-arrow]{
transform: translate(0px, 200%);
}
[data-download-src][data-download-state="ready"] [data-download-base]{
transition-delay: 0.1s;
transform: scale(0, 1);
}
[data-download-src][data-download-state="ready"] [data-download-success] path{
transition-delay: 0.25s;
stroke-dashoffset: 0;
}
/* Hover state */
@media (hover: hover) and (pointer: fine){
[data-download-src]:hover{
background-color: #0a75f8;
}
[data-download-src][data-download-state="idle"]:hover [data-download-arrow]{
transform: translate(0px, -30%);
}
[data-download-src][data-download-state="idle"]:hover [data-download-base]{
transform: scale(1.2, 1);
}
}
/* Focus state */
[data-download-src]:focus{
background-color: #0a75f8;
}
[data-download-src][data-download-state="idle"]:focus [data-download-arrow]{
transform: translate(0px, -30%);
}
[data-download-src][data-download-state="idle"]:focus [data-download-base]{
transform: scale(1.2, 1);
}Step 2: Add custom Javascript
Custom Javascript in Webflow
In this video, Ilja gives you some guidance about using JavaScript in Webflow:
Step 2: Add Javascript
Step 3: Add Javascript
Javascript
function initDownloadButtons() {
const selector = '[data-download-src]';
const attrSrc = 'data-download-src';
const attrName = 'data-download-name';
const setState = (el, state) => {
el.dataset.downloadState = state; // "idle" | "downloading" | "ready" | "fallback"
};
const triggerDownload = (url, filename) => {
const a = document.createElement('a');
a.href = url;
if (filename) a.download = filename;
a.rel = 'noopener';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
document.querySelectorAll(selector).forEach(el => {
setState(el, 'idle');
// Optional label inside the button
const labelEl = el.querySelector('[data-download-label]');
if (labelEl && !labelEl.dataset.downloadOriginalLabel) {
labelEl.dataset.downloadOriginalLabel = labelEl.textContent;
}
const showSuccessAndReset = () => {
if (labelEl) {
const successText = labelEl.getAttribute('data-download-success');
if (successText) {
labelEl.textContent = successText;
}
}
// clear any previous timer on this button
if (el._downloadResetTimeout) {
clearTimeout(el._downloadResetTimeout);
}
el._downloadResetTimeout = setTimeout(() => {
setState(el, 'idle');
if (labelEl) {
const original = labelEl.dataset.downloadOriginalLabel;
if (original != null) {
labelEl.textContent = original;
}
}
}, 3000);
};
el.addEventListener('click', async (e) => {
e.preventDefault();
// prevent double-click spam while we’re fetching
if (el.dataset.downloadState === 'downloading') return;
const src = el.getAttribute(attrSrc);
if (!src) return;
const customName = el.getAttribute(attrName);
// derive filename from URL if no explicit name
const urlObj = new URL(src, window.location.href);
const urlFilePart = urlObj.pathname.split('/').pop() || 'download';
const fileName = customName || urlFilePart;
// remove focus from the button we clicked
el.blur();
try {
setState(el, 'downloading');
const res = await fetch(src, { mode: 'cors', credentials: 'omit' });
if (!res.ok) throw new Error('bad status');
const blob = await res.blob();
const objectUrl = URL.createObjectURL(blob);
setState(el, 'ready');
triggerDownload(objectUrl, fileName);
showSuccessAndReset();
// cleanup a bit later
setTimeout(() => URL.revokeObjectURL(objectUrl), 10_000);
} catch (err) {
// CORS / network / whatever → fallback to plain link
setState(el, 'fallback');
triggerDownload(src, fileName);
showSuccessAndReset();
}
});
});
}
document.addEventListener("DOMContentLoaded", function () {
initDownloadButtons();
})Step 3: Add custom CSS
Step 2: Add custom CSS
Custom CSS in Webflow
Curious about where to put custom CSS in Webflow? Ilja explains it in the below video:
CSS
/* Transition settings */
[data-download-src]{
transition: 0.25s background-color ease;
}
[data-download-arrow], [data-download-base]{
transition: 0.5s transform cubic-bezier(0.625, 0.05, 0, 1);
}
[data-download-icon-wrap]{
clip-path: inset(0em round 100em);
transition: 0.5s clip-path cubic-bezier(0.625, 0.05, 0, 1);
}
[data-download-success] path{
transition: 0.4s stroke-dashoffset cubic-bezier(0.625, 0.05, 0, 1);
}
[data-download-base]{
transform-origin: center center;
}
/* When status is 'downloading' */
[data-download-src][data-download-state="downloading"]{
pointer-events: none;
}
body:has([data-download-src][data-download-state="downloading"]){
cursor: waiting;
}
/* When status is 'ready' or 'fallback' */
[data-download-src][data-download-state="ready"] [data-download-icon-wrap]{
transition-delay: 0.15s;
clip-path: inset(0.35em round 100em);
}
[data-download-src][data-download-state="ready"] [data-download-arrow]{
transform: translate(0px, 200%);
}
[data-download-src][data-download-state="ready"] [data-download-base]{
transition-delay: 0.1s;
transform: scale(0, 1);
}
[data-download-src][data-download-state="ready"] [data-download-success] path{
transition-delay: 0.25s;
stroke-dashoffset: 0;
}
/* Hover state */
@media (hover: hover) and (pointer: fine){
[data-download-src]:hover{
background-color: #0a75f8;
}
[data-download-src][data-download-state="idle"]:hover [data-download-arrow]{
transform: translate(0px, -30%);
}
[data-download-src][data-download-state="idle"]:hover [data-download-base]{
transform: scale(1.2, 1);
}
}
/* Focus state */
[data-download-src]:focus{
background-color: #0a75f8;
}
[data-download-src][data-download-state="idle"]:focus [data-download-arrow]{
transform: translate(0px, -30%);
}
[data-download-src][data-download-state="idle"]:focus [data-download-base]{
transform: scale(1.2, 1);
}Implementation
Download Source
Use [data-download-src] to define the URL that will be fetched and downloaded, triggering the full download logic whenever the user clicks the button. The element with this attribute is also the one we listen for clicks on.
Download Name
Use [data-download-name] to optionally define a custom filename for the downloaded file, replacing the auto-detected filename extracted from the URL.
<button
data-download-src="https://example.com/file.pdf"
data-download-name="custom-name.pdf"
class="download-btn">
</button>Download State
Use [data-download-state] to reflect and animate the button’s current stage, switching automatically between "idle", "downloading", "ready", and "fallback" according to script progress.
Download Label
Use [data-download-label] to define a text element inside the button that dynamically changes between the default label and an success state.
Success Text
Use [data-download-success] on the label to optionally define a short confirmation message that temporarily replaces the default label after a successful download.
Hosting Files
We recommend hosting your downloadable assets on Bunny CDN, as files served from their network deliver fast response times with minimal cost. For Webflow users, you can also host your files directly in the Webflow Asset Manager, then copy the file URL and use it inside [data-download-src] for a fully native setup.
CORS Notes
When fetching files from external hosts, some servers block cross-origin requests, which causes the script’s fetch to fail. in this case the button automatically switches to "fallback" and downloads the file directly via the original [data-download-src] URL. If you run into this issue, make sure the file extension you are serving is included in your host’s CORS-allowed extension list.
Resource details
Last updated
November 19, 2025
Category
Utilities & Scripts
Need help?
Join Slack




























































































































