Roman Numeral Converter — Arabic to Roman & Back
Roman Numeral Converter · Tools · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="wrap">
<h2>Roman Numeral Converter</h2>
<div class="panel">
<label>Arabic number (1-3999)</label>
<input type="text" id="arabic-input" spellcheck="false" value="1994" inputmode="numeric" />
<div class="arrow">converts to</div>
<div class="roman-output" id="roman-output"></div>
</div>
<div class="divider"><span>or convert the other way</span></div>
<div class="panel">
<label>Roman numeral</label>
<input type="text" id="roman-input" spellcheck="false" placeholder="e.g. MCMXCIV" />
<div class="arrow">converts to</div>
<div class="arabic-output" id="arabic-output"></div>
</div>
<div class="breakdown" id="breakdown"></div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: #f8fafc; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 28px 20px; }
.wrap { width: 100%; max-width: 420px; background: #fff; border: 1px solid #e2e8f0; border-radius: 18px; padding: 24px; }
h2 { font-size: 17px; font-weight: 800; color: #1e293b; margin-bottom: 18px; }
.panel { margin-bottom: 6px; }
label { display: block; font-size: 11.5px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 6px; }
input { width: 100%; padding: 12px 14px; border: 1.5px solid #e2e8f0; border-radius: 10px; font-family: "SF Mono", Consolas, monospace; font-size: 16px; color: #1e293b; }
input:focus { outline: none; border-color: #6366f1; }
input.invalid { border-color: #dc2626; }
.arrow { text-align: center; font-size: 10.5px; color: #cbd5e1; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; margin: 8px 0; }
.roman-output, .arabic-output { text-align: center; font-size: 26px; font-weight: 800; color: #4338ca; min-height: 34px; letter-spacing: 0.04em; }
.arabic-output { color: #16a34a; }
.error-text { font-size: 13px; color: #dc2626; font-weight: 600; }
.divider { display: flex; align-items: center; gap: 10px; margin: 20px 0 16px; }
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: #e2e8f0; }
.divider span { font-size: 11px; color: #94a3b8; font-weight: 600; white-space: nowrap; }
.breakdown { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 18px; min-height: 20px; }
.chip { font-size: 11px; font-family: "SF Mono", Consolas, monospace; padding: 4px 8px; border-radius: 6px; background: #f1f5f9; color: #475569; font-weight: 700; }const ROMAN_TABLE = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I'],
];
const arabicInput = document.getElementById('arabic-input');
const romanOutput = document.getElementById('roman-output');
const romanInput = document.getElementById('roman-input');
const arabicOutput = document.getElementById('arabic-output');
const breakdown = document.getElementById('breakdown');
function toRoman(num) {
if (num < 1 || num > 3999) return null;
let result = '';
let remaining = num;
for (const [value, symbol] of ROMAN_TABLE) {
while (remaining >= value) {
result += symbol;
remaining -= value;
}
}
return result;
}
function fromRoman(str) {
const cleaned = str.trim().toUpperCase();
if (!cleaned) return { error: null };
if (!/^[MDCLXVI]+$/.test(cleaned)) return { error: 'Only the letters M, D, C, L, X, V, I are valid' };
const values = { M: 1000, D: 500, C: 100, L: 50, X: 10, V: 5, I: 1 };
let total = 0;
let prev = 0;
const parts = [];
for (let i = cleaned.length - 1; i >= 0; i--) {
const value = values[cleaned[i]];
if (value < prev) {
total -= value;
parts.unshift('-' + value);
} else {
total += value;
parts.unshift('+' + value);
prev = value;
}
}
const roundTrip = toRoman(total);
if (roundTrip !== cleaned) {
return { error: 'Not a valid standard-form Roman numeral (unusual letter ordering)' };
}
return { value: total, parts };
}
function updateFromArabic() {
const raw = arabicInput.value.trim();
arabicInput.classList.remove('invalid');
if (!raw) { romanOutput.innerHTML = ''; return; }
const num = parseInt(raw, 10);
if (!/^\d+$/.test(raw) || Number.isNaN(num)) {
arabicInput.classList.add('invalid');
romanOutput.innerHTML = '<span class="error-text">Enter a whole number</span>';
return;
}
const roman = toRoman(num);
if (!roman) {
arabicInput.classList.add('invalid');
romanOutput.innerHTML = '<span class="error-text">Must be between 1 and 3999</span>';
return;
}
romanOutput.textContent = roman;
}
function updateFromRoman() {
const raw = romanInput.value;
romanInput.classList.remove('invalid');
breakdown.innerHTML = '';
if (!raw.trim()) { arabicOutput.innerHTML = ''; return; }
const result = fromRoman(raw);
if (result.error) {
romanInput.classList.add('invalid');
arabicOutput.innerHTML = '<span class="error-text">' + result.error + '</span>';
return;
}
arabicOutput.textContent = result.value;
breakdown.innerHTML = result.parts.map(p => '<span class="chip">' + p + '</span>').join('');
}
arabicInput.addEventListener('input', updateFromArabic);
romanInput.addEventListener('input', updateFromRoman);
updateFromArabic();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Roman Numeral Converter — Arabic-to-Roman and Roman-to-Arabic with Subtractive Notation

Roman numerals look simple until you have to convert a number like 1994 by hand and remember whether it is MCMXCIV or some other combination of M, D, C, L, X, V, and I — the subtractive notation rules (where a smaller symbol before a larger one means subtraction, like IV for 4) trip up even careful hand conversion. This snippet implements both directions with the actual greedy subtraction algorithm and a strict validity check, rather than a lookup table of pre-computed answers.
Converting Arabic to Roman with a value/symbol table
toRoman() walks a table of value/symbol pairs ordered from largest to smallest, including the six subtractive pairs (900 → CM, 400 → CD, 90 → XC, 40 → XL, 9 → IX, 4 → IV) alongside the seven base symbols. For each pair, it appends the symbol and subtracts its value from the remaining number as many times as that value still fits — a classic greedy algorithm that works because Roman numeral values are specifically structured to make greedy selection always produce the correct, standard-form result. Numbers outside the valid range of 1 to 3999 (the largest number classical Roman numerals can represent without non-standard notation) return null rather than a wrong answer.
Converting Roman to Arabic by scanning right to left
fromRoman() takes the more interesting direction: converting text back into a number. It scans the cleaned, uppercased input from right to left, tracking the value of the previously seen symbol. If the current symbol's value is *less than* the previous one, it is subtracted (this is what makes IV mean 4 rather than I plus V); otherwise it is added, and becomes the new "previous" reference value. This right-to-left scan with a running comparison is the standard technique for decoding subtractive notation, and it correctly handles every valid Roman numeral without needing to special-case each of the six subtractive pairs individually.
Validating standard form with a round-trip check
Not every string of valid Roman letters is a *correctly formed* Roman numeral — IIII decodes arithmetically to 4 by naive addition, but standard form requires IV. Rather than writing a separate grammar-validation regex to catch every malformed variant, fromRoman() takes a simpler and more robust approach: after computing the numeric total, it calls toRoman() on that same total and compares the result against the original cleaned input. If they do not match exactly, the input was not in standard form, and a specific error is shown instead of a silently accepted but non-canonical answer. This round-trip validation technique — converting forward, then backward, and checking they agree — is a useful general pattern any time validating input directly would require re-implementing significant parts of the format's grammar rules.
The additive/subtractive breakdown chips
Below the Roman-to-Arabic result, each individual symbol's contribution is shown as a small chip labeled +1000 or -100, built from the same right-to-left scan that computed the total. This turns an opaque final number into a visible step-by-step accounting of exactly how each letter in the input contributed — genuinely useful for anyone trying to understand *why* MCMXCIV equals 1994 rather than just being told that it does.
Step by step
How to Use
- 1Enter a number to convert to RomanType any whole number from 1 to 3999 in the top field — the Roman numeral updates live below it.
- 2Or enter a Roman numeral to convert to a numberType letters M, D, C, L, X, V, I in the second field — the decoded number appears below it.
- 3Read the breakdown chipsEach symbol's contribution (added or subtracted) is shown as a chip, explaining exactly how the total was computed.
- 4Watch for validation errorsAn out-of-range number, invalid characters, or non-standard Roman form (like IIII instead of IV) is flagged with a specific error message.
- 5Try historically famous numbersTry 1994 (MCMXCIV), 444 (CDXLIV), or 3999 (MMMCMXCIX) to see the subtractive notation rules in action.
- 6Export in your formatClick HTML for a standalone file, JSX for a React component, or Tailwind for a React + Tailwind version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Roman numeral values are specifically structured (with the six subtractive pairs included in the lookup table) so that always taking the largest applicable value first produces the correct standard-form result. This is a genuine mathematical property of the value/symbol table, not a coincidence.
fromRoman() scans the numeral from right to left while comparing each symbol's value to the one before it. When a symbol's value is less than the value already accumulated to its right, it is subtracted rather than added, correctly decoding pairs like IV (4) and IX (9).
IIII decodes arithmetically to 4, but it is not standard Roman numeral form. The tool detects this by converting the computed total back to Roman using toRoman() and comparing it to your original input — since IIII does not round-trip back to itself, it is flagged as non-standard form rather than silently accepted.
3999 (MMMCMXCIX), which is the largest value representable using only the standard seven Roman letters and subtractive notation without resorting to non-standard conventions like an overline for multiplication by 1000.
They show the individual contribution of each letter in your Roman numeral input, in the order they were processed. A positive chip means that value was added to the total; a negative chip means it was subtracted because a smaller-value symbol preceded a larger one, as in IV or CM.
Yes. Input is automatically uppercased before validation and conversion, so both mcmxciv and MCMXCIV are treated identically.