Plenty of tutorials style a button element, call it a download button and move on. It looks right. It also has no file URL behind it, so nothing downloads until someone writes JavaScript.

The version that works is an anchor with an href pointing at the file and the download attribute, dressed up with padding, a background, a border radius and a box shadow. The CSS side really is that plain.

The part most guides skip sits in MDN Web Docs. The attribute works only for same-origin URLs, plus same-origin blob: and data: URLs. Point it at anything else and the browser ignores it and navigates, so a file on a CDN host opens as a page instead of saving.

PDFs, ZIP archives and spec sheets are the usual payloads. WCAG 2.2 also sets a 24 by 24 CSS pixel minimum target size, and the finished button has to clear it.

Which element should a CSS download button use?

See the Pen
Download button
by Thea (@HighFlyer)
on CodePen.

An a element, with an href that points to the file and the download attribute. A button element has no URL to save, so it cannot download anything until JavaScript steps in.

That gap shows up in a lot of copy-paste examples. W3Schools’ download button how-to, for one, styles a button with an icon and a hover color, but the markup carries no file at all.

The anchor route also keeps everything in plain HTML, so the download still works when scripts fail to load.

What the download attribute does

Add it and the browser saves the linked URL instead of navigating to it (MDN Web Docs). The WHATWG HTML Living Standard defines the behavior. Safari on iOS 13 finished cross-browser support in September 2019, and the web-features project lists the attribute as Baseline widely available since March 2022.

Leave the attribute without a value and the browser picks a file name itself. The filename in the server’s Content-Disposition header is one source, and the last segment of the URL path is another. The Content-Type header gets a say too, mostly to settle the extension.

Anything on another domain behaves differently (more on that below).

How to set a custom file name

Give the attribute a value and the browser suggests it as the file name.

<a class="btn-download" href="/files/pricing-guide-2026.pdf" download="acme-pricing-guide.pdf">Download the pricing guide</a>

Slashes and backslashes in the value turn into underscores, and the browser adjusts any character the filesystem forbids.

Where is web design headed next?

Discover the latest web design statistics: industry growth, design trends, technology adoption, and insights defining the future of the web.

Explore the Data →

One catch. If the server sends a Content-Disposition header with a filename parameter, that name overrides the one in your markup.

I’d pick the anchor whenever a real URL exists, even for generated files. MDN’s own example saves a canvas as a PNG with an anchor and a data: URL. Its guidance says a hyperlink is for navigating to a real URL, while a button belongs to script-driven actions, such as a click that calls an API before the file exists.

Core styles that turn an anchor into a button

See the Pen
download button
by Metty (@Metty)
on CodePen.

An anchor looks like a button once it has padding, a solid background, rounded corners and no underline. Add a font weight, a text color and a soft shadow and nobody will guess it started as a link.

  1. Write the anchor with a class, a real file path in href and the download attribute.
  2. Strip the link defaults with text-decoration: none and an explicit color.
  3. Set the shape with display: inline-flex, padding: 0.75rem 1.5rem and border-radius: 8px.
  4. Give it weight and depth with font-weight: 600, a background and a soft box-shadow.
  5. Move colors and radius into custom properties so every state reuses them.
:root {
--btn-bg: #0b5fff;
--btn-bg-hover: #0847c4;
--btn-text: #ffffff;
--btn-radius: 8px;
}

.btn-download {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: var(--btn-radius);
background: var(--btn-bg);
color: var(--btn-text);
font-weight: 600;
text-decoration: none;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}

inline-flex beats inline-block here. It centers an icon and label on one axis and spaces them with gap, so no margin hacks.

Radius depends on context. Somewhere between 4px and 8px reads as a standard interface button. 999px gives you a pill.

If you’d rather start from something visual, a CSS button generator outputs the same declarations. Paste its values into the custom properties above.

A gradient background works too. A CSS gradient generator speeds up the color picking, but checking the lightest stop against the label color is still on you.

How to style hover, focus-visible, active and disabled states

Hover, keyboard focus, press and disabled each get a selector of their own (:hover, :focus-visible, :active and [aria-disabled="true"]). Each one changes a property or two, so the base class stays untouched.

StateSelectorTypical change
Hover:hoverDarker background
Keyboard focus:focus-visibleOutline with offset
Pressed:active1px shift or deeper shade
Disabled[aria-disabled="true"]Reduced opacity, no pointer cursor

Hover is the easy one. Swap the background to the darker custom property, and if a plain color change feels flat, browse a longer list of button hover effects.

.btn-download:hover { background: var(--btn-bg-hover); }
.btn-download:active { transform: translateY(1px); }

Why :focus-visible beats :focus

MDN’s :focus-visible reference points to WCAG 2.1 SC 1.4.11, which sets the floor for a visual focus indicator at 3:1 contrast. It also warns that removing focus styles makes keyboard navigation inaccessible for sighted users.

:focus-visible matches only when the browser decides the user needs to see where focus sits. A mouse click on the button stays clean, while tabbing to it shows the ring.

.btn-download:focus-visible {
outline: 3px solid #111111;
outline-offset: 3px;
}

Pick an outline color that holds 3:1 against the page background, not just against the button. The offset leaves a gap, so the page color is what the ring sits on.

MDN lists :focus-visible as Baseline widely available, with support across browsers since March 2022. A :focus fallback now only matters for very old browsers.

Disabling a link styled as a button

An anchor has no disabled attribute, so a disabled download link takes ARIA plus some code of your own. Add aria-disabled="true" so assistive technology announces the state. The browser draws no disabled look for it, which means you style [aria-disabled="true"] yourself, and the click gets blocked in JavaScript.

That state comes from ARIA, and it only changes what screen readers announce. The link stays focusable and clickable.

[aria-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
}

pointer-events: none stops mouse clicks but not keyboard activation, so pair it with a click handler that calls preventDefault(). If the disabled button stays in the tab order, keep its text legible, since MDN notes that such styling may still need to pass color contrast requirements.

The blunt alternative is dropping the href. Without it the element is no longer a hyperlink (MDN lists its implicit role as generic), so no download starts.

How to add a download icon, file type and file size

See the Pen
CSS3 Active Download Button
by Arthur Gareginyan (@berserkr)
on CodePen.

Inline SVG is the icon I’d pick. It takes the text color through currentColor, so hover and disabled colors follow along automatically, and nothing extra loads.

Icon fonts are the usual choice in tutorials, Font Awesome especially, but that loads a whole font file for one glyph. A background image can’t change color with the text, so each state needs its own file.

Paste the markup straight into the anchor, as explained in this walkthrough of SVG in HTML.

<a class="btn-download" href="/files/pricing-guide-2026.pdf" download>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 16 6 10h4V3h4v7h4l-6 6zm-8 3h16v2H4z"/>
</svg>
Download the pricing guide (PDF, 2.4 MB)
</a>

The gap from the base style spaces the icon from the label, so the SVG needs no margin. Alignment comes from align-items: center on the anchor.

The icon is decorative here because the visible label already names the action, so aria-hidden="true" keeps screen readers from reading it twice. The same logic for other graphics is covered in this piece on accessible SVG files.

If the icon is the only sign of the file type, give it a text alternative instead. MDN’s anchor reference does this with an alt on a file-type icon.

Showing file type and file size

See the Pen
Button with sliding information
by Miro Karilahti (@miroot)
on CodePen.

GOV.UK’s attachment link component prints file details in brackets after the link text, as in “Temporary snow ploughs: guidance note (PDF, 19.5 KB, 1 page)”. The GOV.UK publishing design guide requires that a link meant for downloading a file, rather than opening a page, specify the file type and size.

The type is just the format name, such as PDF, ZIP or MP3, and the size goes in KB or MB, like the 19.5 KB in the GOV.UK example. Put both inside the link text so screen readers announce them together with the button name.

GOV.UK’s component takes the size and content type as parameters instead of hand-typed text, so the label follows the file. The Scottish Government Design System goes further and uses ARIA attributes so VoiceOver announces type and size together, for example a 44 page PDF at 7.2 megabytes.

Generate the label from the file itself at build time. A typed-in “2.4 MB” goes stale the day someone replaces the PDF.

How to animate a download button with CSS

See the Pen
Download Animation – CSS Only
by Matthew Sechrest (@matthewsechrest)
on CodePen.

A transition on background and transform covers hover and press feedback. Keyframes take over for loading and success states, which a transition cannot script.

Bootstrap 5.3 sets its button transition to 0.15 seconds with ease-in-out across color, background-color, border-color and box-shadow (Bootstrap documentation). Anything much slower reads as lag on a control people click once.

.btn-download {
transition: background-color 0.15s ease-in-out,
transform 0.15s ease-in-out;
}

Add it to the base class. The :hover and :active rules already change the two properties it watches.

Keyframes for the arrow and the success state

See the Pen
Download Button Animation GSAP
by Kasper De Bruyne (@kdbkapsere)
on CodePen.

A looping arrow needs keyframes, because a transition only moves between two states. This guide to CSS keyframes covers the syntax, and the rule itself is short.

@keyframes nudge {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(3px); }
}

.btn-download:hover svg {
animation: nudge 0.6s ease-in-out 2;
}

The pure CSS arrow-to-checkmark button in freefrontend’s download button roundup works the same way, using keyframes to turn the arrow into a checkmark.

Why a CSS progress bar can only be decoration

See the Pen
Download Progress Bar (CSS)
by Amli (@uzcho_)
on CodePen.

CSS can run a bar on a fixed timer after the click, and it can swap the label to “Preparing” and back. It cannot read how many bytes the browser has saved, and it has no idea whether the download finished or failed.

Vaadin’s download documentation states that the browser shows no progress percentage for a native link download. Real progress needs a streamed fetch response and a Content-Length header, the route the download extension in htmx 4 takes with its progress events.

If the server needs time to generate the file, a CSS progress bar can show that preparation is under way. Label it that way, so the bar never promises a transfer percentage it cannot measure.

Pure CSS state tricks carry a cost too. A button driven by the checkbox hack is a label, and one driven by :target is a hash link, so neither points at a file.

Respecting reduced motion

MDN lists the prefers-reduced-motion media feature as Baseline widely available, with support across browsers since January 2020. It exists for people who set their device to minimize non-essential motion, and a nudging arrow counts as non-essential.

@media (prefers-reduced-motion: reduce) {
.btn-download,
.btn-download svg,
.btn-download:hover svg {
transition: none;
animation: none;
}
}

Place the block after the animation rules. Where two rules have equal specificity, the later one in source order wins (MDN). The hover rule above is more specific than .btn-download svg, which is why the block lists .btn-download:hover svg as well.

When the download attribute does not work

A file on another origin, a conflicting Content-Disposition header, a browser without support and a cross-origin fetch without CORS headers will each break it. The table shows what the browser does in every case and how to fix it.

ConditionWhat happensFix
File on another origin (CDN host, subdomain, other port)Attribute ignored, normal navigationServe from your origin or send Content-Disposition: attachment
Same-origin file with an inline headerSources disagree on which side winsSend attachment and remove the doubt
Opera MiniAttribute unsupported in every version (Can I use)Accept the plain-link fallback
Fetch and blob from another hostBlocked without CORS headersAdd Access-Control-Allow-Origin on the file host

Cross-origin files, engine by engine

Chrome 65, in beta from February 2018, made Blink ignore the attribute on cross-origin anchors. Chrome’s developer blog gives the reason: it closes a route for leaking cross-origin information through user actions.

WebKit’s changeset 206478 ignores the attribute the same way. A Mozilla engineer’s note on a Bugzilla report (bug 1990259) says Firefox can trigger a plain navigation for such links instead of a download.

Before shipping, run the button through the routine in this guide to cross-browser compatibility. Chrome, Firefox and Safari are the minimum set.

Fixing it on the server

Fix the headers before reaching for scripts. MDN notes that a target page can still instruct a download through Content-Disposition, even when the attribute is ignored.

Content-Disposition: attachment; filename="pricing-guide-2026.pdf"

One case has conflicting documentation. Older notes say Firefox lets an inline header win over the attribute while Chrome honors the attribute, but MDN’s current text says an honored attribute overrides inline.

Testing both browsers settles it for your files. An attachment header removes the question.

The fetch and blob workaround

When the server headers are out of reach, JavaScript can fetch the file and hand the browser a blob: URL that sits on the page’s own origin. It only works if the file host sends CORS headers. The whole file also loads into memory before saving, so keep it to small files, and if scripts fail the link falls back to a plain navigation.

const link = document.querySelector(".btn-download");

link.addEventListener("click", async (event) => {
event.preventDefault();
const response = await fetch(link.href);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const temp = document.createElement("a");
temp.href = url;
temp.download = "pricing-guide-2026.pdf";
temp.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
});

What does an accessible download button need?

The checks come from the WCAG 2.2 rules behind web accessibility, and most of them take one line of CSS. The button needs a target of at least 24 by 24 CSS pixels, 4.5:1 contrast for the label and a visible focus indicator. The link text has to name the file as well.

On the numbers, W3C sets the minimum target size at 24 by 24 CSS pixels for Level AA (SC 2.5.8), while MDN, citing SC 2.5.5, recommends 44 by 44 CSS pixels at Level AAA. Normal text needs 4.5:1 contrast at Level AA (W3C, SC 1.4.3).

Target size and spacing

Padding sets the target, not the icon. The base style from earlier lands at roughly 42 to 48 pixels tall depending on the font and its line height, so it clears the 24 pixel minimum and sits near the 44 pixel recommendation.

.btn-download {
min-height: 44px;
}

W3C lists min-height and min-width on the target container as a sufficient technique (C42). Stacked download buttons need a gap between them as well, because W3C’s own toolbar example shows small targets passing with space around them and failing when packed together.

Contrast in every state

W3C’s guidance applies the ratio to text shown on hover or keyboard focus too, so the darker hover background must still hold 4.5:1 against the label. Checking takes seconds once you know how color contrast is calculated.

The sources split on disabled buttons. W3C exempts text on inactive controls, but MDN still advises legible styling when an aria-disabled control stays in the tab order. I’d treat the exemption as a floor and keep the disabled label readable.

Accessible name

Say what the file is. “Download the 2026 pricing guide” works, “Click here” doesn’t.

Each button on a page needs its own name, since screen reader link lists show names out of context. Strong link text helps sighted users too, because they scan links the same way (MDN).

Plain CSS, Bootstrap or Tailwind: which fits a download button?

Plain CSS fits a single button. Bootstrap or Tailwind win only when the site already runs on one of them.

ApproachSetupControlMain drawback
Plain CSSOne class, no dependencyFullEvery state is yours to write
Bootstrap 5.3btn btn-primary on the anchorCSS variables on .btnPulls in a framework for one control
TailwindUtility classes on the anchorPer-utility variantsLong class strings in the markup

Bootstrap

The Bootstrap button classes work on a elements, so the anchor from earlier takes them unchanged. Bootstrap asks for role="button" only when an anchor triggers in-page behavior, and a link to a file is navigation, so the attribute stays off.

Disabling gets awkward. Bootstrap’s docs say a disabled anchor button should drop the href. If the href must stay, add tabindex="-1" and custom JavaScript, because pointer-events: none leaves keyboard activation untouched.

<a class="btn btn-primary" href="/files/pricing-guide-2026.pdf" download>Download the pricing guide</a>

Tailwind

<a class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 font-semibold text-white hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" href="/files/pricing-guide-2026.pdf" download>Download the pricing guide</a>

Tailwind’s documentation applies every utility conditionally through a variant prefix, and it includes hover, focus, active and focus-visible variants. The Tailwind button patterns in that style stack on top of the same anchor.

The cost is the class string, which grows with every state you add.

Reuse the framework’s button class if the site already loads one. Loading Bootstrap for a single download anchor is the wrong trade.

CSS Download Button Examples To Check Out

Download Buttom By Jesus Tapial

See the Pen
Download buttom
by jesus tapial (@machuenca)
on CodePen.

Apple/Android Download Button: Courtesy of Matthias Benz

See the Pen
Apple/Android download button
by Matthias Benz (@Phadzo)
on CodePen.

Gosnippets’ Dual Download and Upload Button

Better Discord’s CSS Button Game

See the Pen
How to install a Discord theme? (With BetterDiscord)
by liliantdn (@Beard-Design)
on CodePen.

Chic Download Button by Andrei

See the Pen
Download Button
by Andrei (@lightmyfire)
on CodePen.

Cassidy Williams’ Subtle Touch

See the Pen
CSS Download Button
by Cassidy (@cassidoo)
on CodePen.

Álvaro’s App-tastic Button

See the Pen
App download button concept
by Álvaro Hernández Perales (@Varo)
on CodePen.

Giang Gii’s Smooth Operator

See the Pen
Download Button only CSS
by Giang Gii (@bingii901)
on CodePen.

Rosh Jutherford’s Creation

See the Pen
Download Button
by Rosh Jutherford (@the_ruther4d)
on CodePen.

Foolproof’s Button with Progress Bar

See the Pen
Download button with progress indicator
by Foolproof (@foolproof)
on CodePen.

The Mesmerizing Button Animation by Denis Pasko

See the Pen
download button animation
by Denis Pasko (@faustdp)
on CodePen.

Namahake’s Blue Brilliance

See the Pen
Download Button
by namahake (@namahake)
on CodePen.

Ana Tudor’s Click-Worthy Creation

See the Pen
download button (click me!)
by Ana Tudor (@thebabydino)
on CodePen.

3D Wizardry by Terence Devine

See the Pen
3D Download Button w/ Meter Progress
by Terence Devine (@tdevine33)
on CodePen.

James Fleeting’s Iconic Design

See the Pen
#codevember 06-2016 <Download Button>
by James Fleeting (@fleeting)
on CodePen.

Twistblogg’s Stylish Statements

Gael’s Masterpiece with a Twist

See the Pen
Download Button
by Gael (@Miraeld)
on CodePen.

Hưng Nguyễn’s Animated Attraction

See the Pen
Animated download button
by Hưng Nguyễn (@ZeroX-DG)
on CodePen.

Vineeth’s Sleek Design

See the Pen
Download Button
by Vineeth.TR (@vineethtrv)
on CodePen.

Kerem Suer’s Triple Threat

See the Pen
Download Button – Design by Kerem Suer
by ActiveCodex (@ActiveCodex)
on CodePen.

Zulhilmi Zainudin’s Transformative Touch

See the Pen
Download Button CSS Transition
by Zulhilmi Zainudin (@zulhfreelancer)
on CodePen.

Rune Sejer Hoffmann’s Minimalistic Touch

See the Pen
Pure css download button
by Rune Sejer Hoffmann (@RSH87)
on CodePen.

Get Hypnotized by Dany Santos

See the Pen
Downloading Animation
by Dany Santos (@THEORLAN2)
on CodePen.

Aaron Iker’s Smooth Operator

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

Adam Kuhn’s Simply Effective

See the Pen
Download Button
by Adam Kuhn (@cobra_winfrey)
on CodePen.

Fizzy Delight by Jamie Coulter

See the Pen
CSS Fizzy Button
by Jamie Coulter (@jcoulterdesign)
on CodePen.

Floating on Cloud Nine with Jon Kantner

See the Pen
Cloud Download Animation
by Jon Kantner (@jkantner)
on CodePen.

Michael Hobizal’s E-commerce Essential

See the Pen
Another Download Link
by Michael Hobizal (@mikehobizal)
on CodePen.

App Galore by Krystian Rysnik

See the Pen
Download buttons
by Krystian Rysnik (@chrisR_)
on CodePen.

CSS Download Button FAQ

How do you make a download button full width on mobile?

See the Pen
Responsive Download Button
by Ryan Locklear (@themondayafter)
on CodePen.

Set display: flex inside a max-width media query, so the anchor fills its container on phones and keeps its inline-flex size elsewhere. Leave the padding from the base style alone, because it holds the target size.

Which color should a download button use?

See the Pen
Flat UI Download Button Awesome
by Virlyz ID (@samsurysites)
on CodePen.

Use the site’s primary action color, provided the label passes the contrast check covered earlier. Bootstrap’s documentation adds a warning that color alone reaches sighted users only, so the label text must name the action.

How do you add a download button in WordPress or Divi without writing CSS?

In WordPress, the File block has a Show download button toggle in its settings, and no CSS is needed.

Divi’s Button module links to a file, but adding the download attribute takes a jQuery snippet, per Elegant Themes. A third-party module such as Divi Supreme offers a Download button type instead, in its Pro version.

Should a download button open in a new tab?

No. The download attribute makes the browser treat the URL as a download, so a new tab adds nothing. If a link does open a new tab, the link text should say so, as MDN advises.

Can CSS or HTML force a Save As dialog?

No. MDN states that the browser, user settings and other factors decide whether the user is prompted, the file saves automatically, or it opens in an application or the browser. A Content-Disposition: attachment header set on the server prompts in most browsers.

What to Check Before Shipping a CSS Download Button

Check the button on the production host first, then by keyboard, then on a phone.

Localhost hides cross-origin failures, which is why production goes first. The keyboard pass should cover every state, focus included. On the phone, look at target size and tap spacing.

The order follows where failures hide. A local server delivers the file from the page’s own origin, so the button downloads in development and opens a page once the file moves to a CDN, with every style intact.

Moving the file onto the page’s origin removes that failure at the cost of serving the traffic from the main host. The fetch and blob route avoids that cost but holds the whole file in memory.

This advice was verified on October 14, 2026, against MDN’s anchor element page, last modified October 12, 2026. A browser that lifts the same-origin restriction would remove the first check.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)