Reactで、ファイルの大きさを読みやすい単位で表示する方法
File の size はバイト数なので、そのまま出すと「157286400」のような読みにくい数字になります。読みやすくするには、1000 か 1024 で割りながら単位を上げていきます。気をつけたいのは2点です。1つは、1000 ごとに割るのか 1024 ごとに割るのかを決め、単位の書き方をそれに合わせることです。もう1つは、小数を丸めた結果が「1000.0 kB」にならないようにすることです。
ここでは、バイト数を単位付きの文字列にする formatFileSize という関数と、選んだファイルの大きさを一覧にする FileSizeList というコンポーネントを作りました。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Chromium 153 です。使ったのは react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2、esbuild 0.28.2、Playwright 1.63.0 です。.tsx を esbuild で変換し、tsc で型エラーがないことを確かめたうえで、StrictMode で包んだ開発用のビルドを Playwright で操作しました。選ばせたファイルは、大きさだけを決めた中身のないファイルと、Playwright で中身を渡した小さなテキストファイルです。Firefox と Safari、スマートフォンのブラウザ、ファイル選択の画面を人が操作する場合は試していません。

kB と KiB は、割る数が違う
Linux の man ページの units(7) では、SI の接頭辞の k(キロ)は 1000、M(メガ)は 1000000 です。1024 ごとの単位には、1998年に IEC が定めた2進の接頭辞を使い、Ki(キビ)が 1024、Mi(メビ)が 1048576 です。同じページには、いまでは MB は 1000000 バイト、MiB は 1048576 バイトだと書かれています。
どちらで表示するかで、同じファイルでも数字が変わります。検証環境の ls(GNU coreutils 9.4)でも、-h と --si で表示が分かれました。
| バイト数 | ls -lh |
ls -l --si |
|---|---|---|
| 999,960 | 977K | 1.0M |
| 2,457,600 | 2.4M | 2.5M |
| 157,286,400 | 150M | 158M |
ls --help には、--si は -h と同じ表示を 1024 ではなく 1000 の累乗で行う、と書かれています。どちらも単位は「K」「M」だけで、表示を見ただけではどちらで割ったのか分かりません。2,457,600 バイトは 1024 ごとなら 2.34375 で、ls -lh では切り上げた「2.4M」になっていました。
画面に出すときは、1000 ごとなら kB・MB、1024 ごとなら KiB・MiB と書き分けておくと、読む人が迷いません。
バイト数を単位付きの文字列にする関数
// バイト数を、読みやすい単位の文字列にする
export type Base = 1000 | 1024;
const UNITS: Record<Base, readonly string[]> = {
1000: ['B', 'kB', 'MB', 'GB', 'TB', 'PB'],
1024: ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'],
};
export function formatFileSize(bytes: number, base: Base = 1000, digits = 1, locale?: string): string {
if (!Number.isFinite(bytes) || bytes < 0) return '-';
const units = UNITS[base];
let i = 0;
let value = bytes;
while (value >= base && i < units.length - 1) {
value /= base;
i++;
}
// バイトの桁は小数にしない
const d = i === 0 ? 0 : digits;
// 丸めると base に届くとき(999.96 kB → 1000.0 kB)は、次の単位にする
const p = 10 ** d;
if (Math.round(value * p) / p >= base && i < units.length - 1) {
value /= base;
i++;
}
const number = new Intl.NumberFormat(locale, {
minimumFractionDigits: d,
maximumFractionDigits: d,
}).format(value);
return `${number} ${units[i]}`;
}
// 丸める前の値(バイト数)を、桁区切りで書く
export function formatBytes(bytes: number, locale?: string): string {
return `${new Intl.NumberFormat(locale).format(bytes)} バイト`;
}
値が base 以上のあいだ base で割り、そのたびに単位を1つ上げます。PB より上の単位は用意していないので、それより大きい値は PB のまま数字が大きくなります。バイト数がマイナスや NaN のときは「-」を返します。
丸めると 1000 に届く値は、次の単位に上げる
割る処理だけだと、999,960 バイトは 999.96 kB で止まります。これを小数1桁にすると「1000.0 kB」です。割る処理を先にして、丸めるのを後にしているために起きます。
比べるために、丸めたあとの確認をしない関数(toFixed(1) で丸めるだけのもの)も用意し、検証環境の Chromium で同じ値を渡しました。
| バイト数 | 丸めたあとを確かめない | formatFileSize(1000 ごと) |
|---|---|---|
| 999,949 | 999.9 kB | 999.9 kB |
| 999,950 | 1000.0 kB | 1.0 MB |
| 999,960 | 1000.0 kB | 1.0 MB |
| 999,999,999,999,999 | 1000.0 TB | 1.0 PB |
formatFileSize では、丸めた値が base 以上になったら、もう一度 base で割って単位を上げています。小数を2桁にした場合(digits に2を渡す)は、999,950 バイトは丸めても 1000 に届かないので「999.95 kB」のままでした。
1024 ごとの場合は、1000 以上 1024 未満の値が、単位を上げずに4桁で出ます。1000 バイトは「1,000 B」、1,048,524 バイトは「1,023.9 KiB」になりました。1024 に届くまでは単位を上げないので、これは割り方どおりの結果です。
数字の書き方は Intl.NumberFormat に任せる
数字の部分は Intl.NumberFormat で書いています。minimumFractionDigits と maximumFractionDigits を同じ値にすると、小数の桁数がそろいます。「1.0 MB」の「.0」が消えないので、単位が同じ行どうしなら、右寄せにしたときに小数点の位置がそろいます。
locale を渡すと、小数点や桁区切りがその地域の書き方になります。1,234,567 バイトを渡すと、次のようになりました。
locale |
formatFileSize |
formatBytes |
|---|---|---|
| ja | 1.2 MB | 1,234,567 バイト |
| de | 1,2 MB | 1.234.567 バイト |
| fr | 1,2 MB | 1 234 567 バイト |
MDN の Intl.NumberFormat() コンストラクターの説明によると、locale を省いて undefined を渡すと、実行環境の既定のロケールが使われます。検証では Playwright でページを locale: 'ja-JP' で開いたので、navigator.language は ja-JP でした。locale を省いて呼んでいる、このあと作るコンポーネントでは、桁区切りがカンマになりました。
Intl.NumberFormat の単位の表記は、日本語では「KB」になる
Intl.NumberFormat には、style: 'unit' で単位まで付けて書く機能もあります。MDN の Intl.NumberFormat() コンストラクターの説明によると、unit に使える値は Intl.supportedValuesOf() のページにまとめられています。Intl.supportedValuesOf() の、対応している単位識別子の一覧にあるバイトの単位は、byte、kilobyte、megabyte、gigabyte、terabyte、petabyte で、kibibyte のような 1024 ごとの単位はありません。
検証環境の Chromium で、1.5 をそれぞれの単位で書かせると、次のようになりました。
locale と unitDisplay |
byte | kilobyte | megabyte |
|---|---|---|---|
| ja、short | 1.5 byte | 1.5 KB | 1.5 MB |
| ja、narrow | 1.5B | 1.5KB | 1.5MB |
| ja、long | 1.5 バイト | 1.5 キロバイト | 1.5 メガバイト |
| en、short | 1.5 byte | 1.5 kB | 1.5 MB |
日本語の short では、キロバイトが大文字の「KB」になり、バイトは「byte」になりました。英語の short では「kB」です。unit に kibibyte や mebibyte を渡すと、RangeError になりました。
kibibyte: RangeError: Invalid unit argument for Intl.NumberFormat() 'kibibyte'
Intl.NumberFormat は、渡した数に指定した単位を付けて書くだけで、バイト数から単位を選んで割る処理はしません。units(7) には、2進の接頭辞ができる前は、k を 1000、K を 1024 の意味で使うことが多かったと書かれています。「1.5 KB」と出ても、1000 ごとの値か 1024 ごとの値かは表示から分かりません。そこで formatFileSize では、単位は自前の文字列で付け、Intl.NumberFormat は数字の部分だけに使いました。
選んだファイルの大きさを一覧にするコンポーネント
import { useId, useState } from 'react';
import { formatBytes, formatFileSize, type Base } from './filesize';
export function FileSizeList() {
const id = useId();
const [files, setFiles] = useState<File[]>([]);
const [base, setBase] = useState<Base>(1000);
const total = files.reduce((sum, f) => sum + f.size, 0);
return (
<div className="filesize">
<label htmlFor={`${id}-file`}>ファイルを選ぶ</label>
<input
id={`${id}-file`}
type="file"
multiple
onChange={(e) => setFiles(Array.from(e.target.files ?? []))}
/>
<fieldset>
<legend>単位</legend>
<label>
<input type="radio" name={`${id}-base`} checked={base === 1000} onChange={() => setBase(1000)} />
1000 ごと(kB、MB)
</label>
<label>
<input type="radio" name={`${id}-base`} checked={base === 1024} onChange={() => setBase(1024)} />
1024 ごと(KiB、MiB)
</label>
</fieldset>
{files.length > 0 && (
<table>
<tbody>
{files.map((f, i) => (
<tr key={`${i}-${f.name}`}>
<td>{f.name}</td>
<td className="size">
<span title={formatBytes(f.size)}>{formatFileSize(f.size, base)}</span>
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<th scope="row">合計 {files.length} 件</th>
<td className="size">
<span title={formatBytes(total)}>{formatFileSize(total, base)}</span>
</td>
</tr>
</tfoot>
</table>
)}
</div>
);
}
.filesize {
display: grid;
gap: 8px;
max-width: 420px;
}
.filesize fieldset {
display: grid;
gap: 4px;
border: none;
padding: 0;
}
.filesize table {
border-collapse: collapse;
}
.filesize td,
.filesize th {
border-bottom: 1px solid #cfe9ef;
padding: 6px 4px;
text-align: left;
font-weight: normal;
}
.filesize .size {
text-align: right;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.filesize tfoot {
color: #067a98;
font-weight: bold;
}
MDN の input type="file" の説明にあるとおり、選んだファイルは files プロパティの FileList で受け取れます。配列として扱うために、Array.from() で配列にして state に入れました。それぞれの File の size は、MDN の Blob.size の説明のとおり、バイト単位の大きさです。
state に持つのはファイルと「1000 ごとか 1024 ごとか」だけで、表示する文字列は描画のたびに formatFileSize で作ります。ラジオボタンを切り替えると、ファイルを選び直さなくても、表の単位がすべて変わります。

検証環境の Chromium で4つのファイルを選ぶと、表の値は次のようになりました。memo.txt の 999,960 バイトは、1000 ごとなら「1.0 MB」で、「1000.0 kB」にはなっていません。
| ファイル | バイト数 | 1000 ごと | 1024 ごと |
|---|---|---|---|
| photo.jpg | 2,457,600 | 2.5 MB | 2.3 MiB |
| memo.txt | 999,960 | 1.0 MB | 976.5 KiB |
| video.mp4 | 157,286,400 | 157.3 MB | 150.0 MiB |
| empty.txt | 0 | 0 B | 0 B |
| 合計 4 件 | 160,743,960 | 160.7 MB | 153.3 MiB |
丸めた値だけでは正確な大きさが分からないので、span の title 属性に、桁区切りを付けたバイト数(「2,457,600 バイト」など)を入れています。大きさの列は右寄せにし、font-variant-numeric: tabular-nums で数字の幅をそろえました。
ファイルを選び直すと、onChange で state が新しい一覧に置き換わります。「こんにちは」を UTF-8 で書いたテキストファイル(a.txt)1つを選ぶと、表は a.txt の「15 B」と、合計 1 件の行だけになりました。選択を空にすると、表は消えました。





