CSV to HTML Table Converter — Free, Browser-Based | SmallStudyTools
100% Free · No Signup · Browser-Based
CSV to HTML Table Converter Upload a CSV file and get a real, semantic HTML table, right in your browser — nothing is uploaded to a server. Pro Mode adds header row control, basic styling, zebra striping, a caption, right-aligned numeric columns, and a paste-ready snippet mode.
Add to Favorites
Share This Tool
● Pro Version is Free for now
Simple
Pro — Full Control
✓ Bulk convert multiple CSVs at once · ✓ Header row, styling, zebra striping & more · ✓ Caption, numeric alignment & embeddable snippets
Drag & drop your CSV here
or click to browse — only .csv files are accepted
Browse Files
+ Add Another CSV
</> Convert to HTML 🏷️ First Row as Header PRO
Turns the first row into real <th> header cells inside a <thead>.
Use first row as header
On by default · off = every row is a plain data row
Embeds a clean <style> block — borders, padding, header background.
Include basic styling
On by default · off = bare, unstyled table markup
Shades alternating rows via CSS — needs Basic Styling turned on.
Alternate row shading
Off by default · requires styling
🔢 Right-Align Numeric Columns PRO
Auto-detects columns that are entirely numeric and right-aligns them.
Right-align numeric columns
Off by default · detected automatically per column
📋 Embed as Snippet Only PRO
Outputs just the <table> (and style, if on) — no <!DOCTYPE>/<head>/<body>, ready to paste into an existing page.
Snippet only, no full document
Off by default · off = complete standalone .html file
`;
}
function buildSnippetHtml(tableMarkup, styled, zebra) {
return styled ? `${buildStyleBlock(zebra)}\n${tableMarkup}` : tableMarkup;
}// ── CONVERSION ────────────────────────────────────────────────
async function startConversion() {
if (!queuedFiles.length || converting) return;
converting = true;
clearErr();
updateConvertBtn();
document.getElementById('progressWrap').classList.add('show');
fileResults = [];const hasHeader = isPro ? headerRowOn : true;
const styled = isPro ? stylingOn : true;
const zebra = isPro ? (zebraOn && stylingOn) : false;
const caption = isPro ? tableCaption.trim() : '';
const rightAlign = isPro ? rightAlignNumeric : false;
const snippet = isPro ? snippetOnly : false;
const totalFiles = queuedFiles.length;try {
for (let fi = 0; fi < queuedFiles.length; fi++) {
const qf = queuedFiles[fi];
setProgress(`Converting "${qf.file.name}"...`, (fi + 0.5) / totalFiles);
await new Promise(r => setTimeout(r, 0));
const text = await readAsUtf8Text(qf.file);
const delimiter = detectDelimiter(text);
const rows = parseCSV(text, delimiter);
const title = qf.file.name.replace(/\.csv$/i, '');
const tableMarkup = buildTableMarkup(rows, hasHeader, caption, rightAlign);
const fullHtml = snippet ? buildSnippetHtml(tableMarkup, styled, zebra) : buildFullHtml(tableMarkup, title, styled, zebra);
const rowCount = hasHeader ? Math.max(0, rows.length - 1) : rows.length;
fileResults.push({ id: qf.id, name: qf.file.name, text: fullHtml, rowCount, colCount: rows.length ? rows[0].length : 0 });
}
renderResults();
document.getElementById('resultsCard').classList.add('show');
document.getElementById('resultsCard').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
toast(`Converted ${fileResults.length} CSV${fileResults.length === 1 ? '' : 's'} to HTML!`);
} catch (e) {
showErr('Something went wrong converting that CSV. Please make sure the file isn\'t corrupted, and try again.');
} finally {
converting = false;
document.getElementById('progressWrap').classList.remove('show');
updateConvertBtn();
}
}function setProgress(label, frac) {
document.getElementById('progressLabel').textContent = label;
document.getElementById('progressFill').style.width = Math.min(100, Math.max(4, frac * 100)) + '%';
}// ── RESULTS / DOWNLOADS ───────────────────────────────────────
function renderResults() {
const wrap = document.getElementById('fileResultsList');
wrap.innerHTML = fileResults.map(fr => `
${escHtml(fr.name.replace(/\.csv$/i, ''))}.html
${fr.rowCount} row${fr.rowCount === 1 ? '' : 's'} · ${fr.colCount} column${fr.colCount === 1 ? '' : 's'} · ${formatBytes(fr.text.length)}
${escHtml(fr.text.slice(0, 700))}${fr.text.length > 700 ? '\n…' : ''}
`).join('');const topActions = document.getElementById('resultsTopActions');
topActions.innerHTML = fileResults.length > 1
? `
Download Everything (.zip)
`
: '';
}function copyResult(fileId) {
const fr = fileResults.find(f => f.id === fileId);
if (!fr) return;
navigator.clipboard.writeText(fr.text)
.then(() => toast('Copied to clipboard!'))
.catch(() => toast('Could not copy — try the Download button instead'));
}function downloadResult(fileId) {
const fr = fileResults.find(f => f.id === fileId);
if (!fr) return;
const base = fr.name.replace(/\.csv$/i, '');
const blob = new Blob([fr.text], { type: 'text/html' });
triggerBlobDownload(blob, `${base}.html`);
toast('HTML file downloaded!');
}async function downloadEverythingZip() {
if (!fileResults.length) return;
const zip = new JSZip();
fileResults.forEach(fr => {
const base = fr.name.replace(/\.csv$/i, '');
zip.file(`${base}.html`, fr.text);
});
const blob = await zip.generateAsync({ type: 'blob' });
triggerBlobDownload(blob, 'csv-to-html-converted.zip');
toast('ZIP downloaded!');
}function triggerBlobDownload(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename; a.click();
setTimeout(() => URL.revokeObjectURL(url), 4000);
}function resetTool() {
queuedFiles = [];
fileResults = [];
renderFileChips();
updateConvertBtn();
document.getElementById('resultsCard').classList.remove('show');
document.getElementById('mainCol').scrollIntoView({ behavior: 'smooth', block: 'start' });
}// ── HEADER ROW / STYLING / ZEBRA (pro) ─────────────────────────
function toggleHeaderRow() {
if (!isPro) { toast('Header row control is part of Pro Mode'); return; }
headerRowOn = !headerRowOn;
document.getElementById('headerSwitch').classList.toggle('on', headerRowOn);
document.getElementById('headerSwitch').setAttribute('aria-checked', String(headerRowOn));
}
function toggleStyling() {
if (!isPro) { toast('Styling control is part of Pro Mode'); return; }
stylingOn = !stylingOn;
document.getElementById('stylingSwitch').classList.toggle('on', stylingOn);
document.getElementById('stylingSwitch').setAttribute('aria-checked', String(stylingOn));
updateZebraAvailability();
}
function toggleZebra() {
if (!isPro) { toast('Zebra striping is part of Pro Mode'); return; }
if (!stylingOn) { toast('Turn on Basic Styling first — zebra striping is a CSS rule'); return; }
zebraOn = !zebraOn;
document.getElementById('zebraSwitch').classList.toggle('on', zebraOn);
document.getElementById('zebraSwitch').setAttribute('aria-checked', String(zebraOn));
}
function updateZebraAvailability() {
const sw = document.getElementById('zebraSwitch');
const sub = document.getElementById('zebraSub');
if (!sw) return;
sw.disabled = !stylingOn;
if (sub) sub.textContent = stylingOn ? 'Off by default' : 'Off by default · requires styling to be on';
}
function setTableCaption(v) {
if (!isPro) { toast('Table caption is part of Pro Mode'); return; }
tableCaption = v;
}
function toggleRightAlign() {
if (!isPro) { toast('Right-align numeric columns is part of Pro Mode'); return; }
rightAlignNumeric = !rightAlignNumeric;
document.getElementById('rightAlignSwitch').classList.toggle('on', rightAlignNumeric);
document.getElementById('rightAlignSwitch').setAttribute('aria-checked', String(rightAlignNumeric));
}
function toggleSnippetOnly() {
if (!isPro) { toast('Embed snippet mode is part of Pro Mode'); return; }
snippetOnly = !snippetOnly;
document.getElementById('snippetSwitch').classList.toggle('on', snippetOnly);
document.getElementById('snippetSwitch').setAttribute('aria-checked', String(snippetOnly));
}// ── MODE ──────────────────────────────────────────────────────
function setMode(mode) {
isPro = mode === 'pro';
document.getElementById('btnSimple').classList.toggle('active', !isPro);
document.getElementById('btnPro').classList.toggle('active', isPro);
document.body.classList.toggle('pro-mode', isPro);
const pb = document.getElementById('proBar'); if (pb) pb.style.display = isPro ? 'flex' : 'none';
document.getElementById('fileInput').multiple = isPro;
document.getElementById('uploadCardTitle').textContent = isPro ? 'Upload Your CSVs' : 'Upload Your CSV';
document.getElementById('dropzoneHint').textContent = isPro
? 'or click to browse — only .csv files are accepted, up to ' + MAX_FILES_PRO + ' at once'
: 'or click to browse — only .csv files are accepted';
if (!isPro && queuedFiles.length > 1) {
queuedFiles = queuedFiles.slice(0, 1);
renderFileChips();
toast('Switched to Simple Mode — only the first file was kept');
}
if (!isPro) {
headerRowOn = true;
document.getElementById('headerSwitch').classList.add('on');
document.getElementById('headerSwitch').setAttribute('aria-checked', 'true');
stylingOn = true;
document.getElementById('stylingSwitch').classList.add('on');
document.getElementById('stylingSwitch').setAttribute('aria-checked', 'true');
zebraOn = false;
document.getElementById('zebraSwitch').classList.remove('on');
document.getElementById('zebraSwitch').setAttribute('aria-checked', 'false');
updateZebraAvailability();
tableCaption = '';
const captionInput = document.getElementById('captionInput');
if (captionInput) captionInput.value = '';
rightAlignNumeric = false;
document.getElementById('rightAlignSwitch').classList.remove('on');
document.getElementById('rightAlignSwitch').setAttribute('aria-checked', 'false');
snippetOnly = false;
document.getElementById('snippetSwitch').classList.remove('on');
document.getElementById('snippetSwitch').setAttribute('aria-checked', 'false');
}
updateConvertBtn();
}// ── UTILS ─────────────────────────────────────────────────────
function escHtml(s) { return String(s || '').replace(/&/g, '&').replace(//g, '>'); }
function toast(msg) { const t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 2800); }
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}