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.
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.
- Write the anchor with a class, a real file path in
hrefand thedownloadattribute. - Strip the link defaults with
text-decoration: noneand an explicitcolor. - Set the shape with
display: inline-flex,padding: 0.75rem 1.5remandborder-radius: 8px. - Give it weight and depth with
font-weight: 600, abackgroundand a softbox-shadow. - 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.
| State | Selector | Typical change |
|---|---|---|
| Hover | :hover | Darker background |
| Keyboard focus | :focus-visible | Outline with offset |
| Pressed | :active | 1px 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.
| Condition | What happens | Fix |
|---|---|---|
| File on another origin (CDN host, subdomain, other port) | Attribute ignored, normal navigation | Serve from your origin or send Content-Disposition: attachment |
Same-origin file with an inline header | Sources disagree on which side wins | Send attachment and remove the doubt |
| Opera Mini | Attribute unsupported in every version (Can I use) | Accept the plain-link fallback |
| Fetch and blob from another host | Blocked without CORS headers | Add 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.
| Approach | Setup | Control | Main drawback |
|---|---|---|---|
| Plain CSS | One class, no dependency | Full | Every state is yours to write |
| Bootstrap 5.3 | btn btn-primary on the anchor | CSS variables on .btn | Pulls in a framework for one control |
| Tailwind | Utility classes on the anchor | Per-utility variants | Long 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.
- What is Backend in Web Development? - September 13, 2026
- What is Frontend Development? - September 11, 2026
- How to Make a Button in Figma: Design Best Practices - September 10, 2026


