PDF や画像へのリンクをクリックすると、ブラウザーがそのままページ内で開いてしまうことがあります。「開くのではなく保存させたい」というときに使うのが、a 要素の download 属性です。属性を1つ足すだけでリンクの動作がダウンロードに変わり、保存されるファイル名まで指定できます。この記事では基本の書き方から、ファイル名を決めるときのルール、他サイトのファイルには効かないという重要な制約、そして JavaScript で作ったデータを保存させる方法までを解説します。
目次
download 属性はリンク先を「開かずに保存」させる
通常のリンクをクリックしたとき、リンク先を表示するか保存するかを決めているのはブラウザーです。HTML や画像、PDF のようにブラウザーが表示できる形式なら画面に開き、ZIP のように表示できない形式ならダウンロードになります。download 属性を付けると、この判断を上書きして常にダウンロード(保存)として扱わせることができます。
<!-- クリックするとブラウザーで開かれる(PDF ビューアーが起動する) -->
<a href="/files/manual.pdf">マニュアルを見る</a>
<!-- クリックすると保存ダイアログになる -->
<a href="/files/manual.pdf" download>マニュアルをダウンロード</a>
値を書かずに download とだけ書けば、ファイル名は元の URL やサーバーからの情報を元にブラウザーが決めます。上の例なら manual.pdf という名前で保存されます。この属性は a 要素のほか、イメージマップで使う area 要素にも指定できます。
保存されるファイル名を指定する
download に値を書くと、その文字列が保存時のファイル名の候補になります。サーバー上のファイル名がハッシュ値や連番になっている場合でも、利用者にとって分かりやすい名前で保存させられます。
<!-- 「2026年度_利用規約.pdf」として保存される -->
<a href="/uploads/f83ac21b9d.pdf" download="2026年度_利用規約.pdf">
利用規約をダウンロード
</a>
ここで指定できるのはあくまで「候補」で、そのとおりに保存されるとは限りません。ブラウザーは安全のために値を加工します。主な挙動は次のとおりです。
| 書いた値 | 実際の挙動 |
|---|---|
download(値なし) | URL やサーバーの情報からブラウザーがファイル名を決める |
download="report.csv" | その名前で保存される |
download="../secret.txt" | パス区切り文字は取り除かれ、別のフォルダーには保存できない |
download="report"(拡張子なし) | ファイルの種類に合った拡張子がブラウザーによって補われることがある |
| 同名のファイルが既にある | report(1).csv のように連番が付けられる |
また、サーバーが Content-Disposition ヘッダーでファイル名を指定している場合は、ヘッダーの指定のほうが優先されます。属性に書いた名前で保存されないときは、サーバー側の設定を確認してみてください。
他サイトのファイルには効かない(同一オリジンの制限)
download 属性でつまずきやすい最大のポイントがこれです。この属性が働くのは、リンク先が自分のサイトと同じオリジン(プロトコル・ドメイン・ポートがすべて同じ)である場合に限られます。外部サイトの画像や PDF へのリンクに download を付けても属性は無視され、普通のリンクとして開かれてしまいます。
<!-- OK:同じサイト内のファイル -->
<a href="/files/logo.png" download="logo.png">ロゴを保存</a>
<!-- NG:別ドメインのファイル。download は無視されて画像が開く -->
<a href="https://example.com/logo.png" download="logo.png">ロゴを保存</a>
これは「気づかないうちに他サイトのファイルを大量に保存させる」といった悪用を防ぐための仕様です。ただし例外があり、blob: と data: で始まる URL は同一オリジンの扱いになるため download が有効に働きます。この性質を使えば、外部のファイルでも JavaScript で一度取得してから保存させられます。
JavaScript で作ったデータをダウンロードさせる
サーバーにファイルを置かなくても、ページ上で組み立てたテキストや CSV をその場でダウンロードさせられます。手順は、データから Blob オブジェクトを作り、URL.createObjectURL() で一時的な URL を発行して href に入れるだけです。作られる URL は blob: で始まるため、上で説明したとおり download がそのまま効きます。
下のデモはその最小構成です。プレビュー内のリンクを押すと、JavaScript タブで作ったテキストが「メモ.txt」として保存されます。JavaScript タブの text の中身を書き換えれば、保存されるファイルの中身も変わります。
<p>下のリンクを押すと、テキストがファイルとして保存されます。</p>
<a id="link" download="メモ.txt">メモをダウンロード</a>
body {
font-family: sans-serif;
margin: 16px;
}
a {
display: inline-block;
padding: 10px 20px;
background: #2563eb;
color: #fff;
border-radius: 6px;
text-decoration: none;
}
// ページ内のテキストから Blob を作り、その URL を href に入れる
const text = "webool のサンプルです。\nこの内容がファイルとして保存されます。";
const blob = new Blob([text], { type: "text/plain" });
const link = document.getElementById("link");
// createObjectURL で作った URL は同一オリジン扱いなので download が効く
link.href = URL.createObjectURL(blob);
実際のアプリケーションでは、表の内容を CSV にして書き出すといった使い方が多いでしょう。ボタンのクリックをきっかけに、リンク要素そのものも JavaScript で作ってしまう書き方が一般的です。
function downloadCsv(rows, fileName) {
// 配列から CSV の文字列を組み立てる
const csv = rows.map((row) => row.join(",")).join("\n");
// Excel での文字化けを防ぐため BOM を先頭に付ける
const blob = new Blob(["" + csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
// a 要素を作って、クリックを発生させる
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
// 発行した URL はメモリを占有するので、使い終わったら解放する
URL.revokeObjectURL(url);
}
downloadCsv(
[
["名前", "点数"],
["田中", "80"],
["鈴木", "95"],
],
"results.csv"
);
URL.createObjectURL() で作った URL は、ページを開いている間ずっとメモリ上にデータを保持し続けます。使い終わったら URL.revokeObjectURL() で解放するのを忘れないでください。なお、a 要素は document.body に追加しなくても click() でダウンロードが始まります。
外部サイトのファイルを保存させたい場合
同一オリジンの制限は、fetch() でファイルを取得して Blob に変換すれば回避できます。ただしこの方法が使えるのは、相手のサーバーが Access-Control-Allow-Origin ヘッダーで外部からの取得を許可している場合だけです。許可されていなければ取得の段階でエラーになります。
async function downloadFromUrl(url, fileName) {
const response = await fetch(url); // CORS の許可が必要
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = objectUrl;
a.download = fileName;
a.click();
URL.revokeObjectURL(objectUrl);
}
ダウンロードにならないときに確認すること
リンク先が別ドメインになっている
まず疑うべきはこれです。同じサイトのつもりでも、画像だけを CDN や別のサブドメインから配信していると別オリジンの扱いになり、属性は無視されます。http と https、www の有無が違うだけでも別オリジンです。開発者ツールのネットワークタブでリンク先のホスト名を確認してみてください。別オリジンだった場合は、前述の fetch() を使う方法か、サーバー側で Content-Disposition: attachment を返す方法に切り替えます。
href がない、または target が併用されている
download 属性は href があって初めて意味を持ちます。href のない a 要素はそもそもリンクとして扱われないため、何も起こりません。また、target="_blank" を一緒に指定すると、ブラウザーによっては新しいタブが一瞬開いてすぐ閉じる、といった不自然な動きになります。ダウンロード用のリンクに target は不要です。
ファイル名が指定どおりにならない
サーバーが返す Content-Disposition ヘッダーにファイル名が含まれていると、そちらが優先されます。WordPress のメディアやクラウドストレージから配信しているファイルでは、こうしたヘッダーが自動で付いていることがあります。属性の値が無視されているように見えたら、開発者ツールのネットワークタブでレスポンスヘッダーを確認してください。拡張子を書き忘れている場合も、ブラウザーが独自に補うことがあります。
ブラウザーの設定で保存先を毎回聞かれる/聞かれない
保存ダイアログを出すか、既定のダウンロードフォルダーへ黙って保存するかは、利用者側のブラウザー設定で決まります。HTML 側からは制御できません。「ダイアログが出ないから動いていない」と判断せず、ダウンロードフォルダーを確認してみてください。スマートフォンでは、ブラウザーやファイル形式によって挙動が大きく異なる点にも注意が必要です。
まとめ
download 属性は、a 要素に付けるだけでリンクの動作を「開く」から「保存する」に変えられる属性です。値を書けば保存時のファイル名も指定できますが、パス区切り文字は取り除かれ、サーバーの Content-Disposition ヘッダーがあればそちらが優先されます。最も注意すべきなのは同一オリジンのファイルにしか効かないという制限で、外部サイトのファイルには使えません。ただし blob: や data: の URL は例外なので、Blob と URL.createObjectURL() を組み合わせれば、その場で生成した CSV やテキストをダウンロードさせられます。使い終わった URL は URL.revokeObjectURL() で解放しておきましょう。