Number Base Converter

Convert between different number bases

Why 2, 8, and 16: Bases as Bit Groupings, Not Arbitrary Choices

Programmers use exactly these bases because each maps cleanly onto bits. One hexadecimal digit is exactly 4 bits (a nibble), so two hex digits are one byte — 0xFF is 11111111 is 255, and a 32-bit value is always 8 hex digits. That is why memory addresses, hash digests, MAC addresses, and color values are written in hex: you can read the byte boundaries directly off the text. Octal groups bits in threes, which matches the Unix permission model perfectly: rwx is three bits, so rwxr-xr-x is 111 101 101 is 755. Decimal, by contrast, has no relationship to bit boundaries at all — 255 tells you nothing visually about which bits are set. Converting decimal to binary requires repeated division by 2; converting hex to binary is a table lookup per digit, which is why experienced engineers do hex-to-binary in their heads but nobody does decimal-to-binary for large numbers. A useful mental anchor is the powers ladder: 2^8 = 256, 2^10 = 1024, 2^16 = 65,536, 2^32 = 4,294,967,296. From these you can derive most limits you meet in practice — the 0–255 range of a byte, the 65,535 maximum TCP port, the 4 GiB limit of a 32-bit address space. And note that Base64 is not a number base in this sense: it is a binary-to-text encoding that maps 6-bit groups to characters. You cannot do arithmetic on Base64, and converting it with a radix converter is a category error.
hex F    F        octal 7   5   5
     |    |              |   |   |
bin 1111 1111       bin 111 101 101
     = 0xFF = 255        = rwxr-xr-x  (chmod 755)

# read a 32-bit value byte by byte
0xDEADBEEF = DE AD BE EF = 222 173 190 239

# the powers ladder
2^8 = 256   2^10 = 1,024   2^16 = 65,536   2^32 = 4,294,967,296

Two's Complement: The Sign Lives in the Width

The bit pattern 11111111 has no inherent sign. Interpreted as an unsigned 8-bit integer it is 255; interpreted as a signed (two's complement) 8-bit integer it is -1. The same bytes, two contracts. Two's complement won because arithmetic is uniform: negation is flip-all-bits-then-add-one, and the same adder circuit handles positive and negative values. An n-bit signed integer covers -(2^(n-1)) through 2^(n-1)-1 — for 8 bits, -128 to 127. The asymmetry is real and occasionally bites: negating -128 in an 8-bit register overflows back to -128, which is why abs() of the most negative integer is undefined behavior in C. This matters in a converter because a converter without a declared width cannot show you negative numbers — there is no such thing as 'the binary of -5', only 'the 8-bit (11111011) or 32-bit (…11111011) two's complement of -5'. JavaScript adds its own twist: bitwise operators coerce numbers to signed 32-bit integers. So 0xFFFFFFFF is 4294967295 as a plain literal, but 0xFFFFFFFF | 0 is -1 because bit 31 becomes the sign bit. The unsigned right shift by zero (value >>> 0) is the standard idiom to reinterpret a value as unsigned 32-bit, and typed arrays (Int8Array vs Uint8Array) make the two readings of the same bytes explicit. When a hex value from a register dump or a network capture looks 'huge and weird', check whether it is a negative number that was printed unsigned.
(0xFF | 0)                 // 255 — fits in int32, stays positive
(0xFFFFFFFF | 0)           // -1  — bit 31 became the sign bit
(-1 >>> 0).toString(16)    // 'ffffffff'
(-1 >>> 0).toString(2)     // '11111...' (32 ones)

// same byte, two contracts
new Int8Array([0xFF])[0]   // -1
new Uint8Array([0xFF])[0]  // 255

// negate = flip bits, add one
//  5 = 00000101
// ~5 = 11111010
// +1 = 11111011  = -5 in 8-bit two's complement

IEEE 754 and the 2^53 Cliff: When JavaScript Silently Rounds Your Integers

JavaScript has one Number type: the IEEE 754 double-precision float. A double has a 52-bit mantissa plus one implicit bit, so it represents every integer exactly up to 2^53 (9,007,199,254,740,991 — Number.MAX_SAFE_INTEGER). One past that, the representable values start skipping: 2^53 + 1 rounds to 2^53, and the comparison 9007199254740992 === 9007199254740993 is true. Nothing throws. The rounding is silent, which is what makes it dangerous. This ambushes real systems constantly, because 64-bit integer IDs are everywhere: database bigint columns, Twitter/X snowflake IDs, Discord IDs, order numbers. JSON has no integer-size limit, but the moment JSON.parse materializes a 19-digit ID as a Number, the low digits are gone — and the corrupted ID will happily round-trip through your code looking plausible. The fixes: transmit large IDs as strings, or parse with a BigInt-aware reviver. BigInt handles arbitrary precision and accepts radix prefixes in its string form. The same IEEE 754 story explains the famous 0.1 + 0.2 !== 0.3, and it is fundamentally a base-conversion problem: 0.1 has a finite representation in decimal but is an infinite repeating fraction in binary (0.0001100110011...), so it must be rounded to fit 52 bits. Any decimal fraction whose denominator is not a pure power of 2 has this property. That is why money should be integer cents, and why a base converter for fractions would need to show repeating expansions.
Number.MAX_SAFE_INTEGER          // 9007199254740991 (2^53 - 1)
9007199254740992 === 9007199254740993   // true (!)

// 64-bit ID from an API, silently corrupted
JSON.parse('{"id": 900719925474099267}').id
// -> 900719925474099300

// exact alternatives
BigInt('900719925474099267')     // 900719925474099267n
BigInt('0xff')                   // 255n — radix prefix works
(255n).toString(16)              // 'ff'

// the fraction side of the same coin
0.1 + 0.2                        // 0.30000000000000004
// 0.1 in binary = 0.000110011001100... (repeats forever)

Radix Literals and Parsing Traps Across Languages

Modern languages agree on prefixes: 0x for hex, 0b for binary, 0o for octal (JavaScript, Python, Rust, and Swift all accept these), plus underscore digit separators like 1_000_000 for readability. The historical wart is C-style octal: a bare leading zero, so 010 means 8. That notation caused enough real bugs — zero-padded columns pasted into code, chmod values passed as decimal — that strict-mode JavaScript made legacy octal literals a syntax error and introduced 0o instead. If an API takes a file mode as a plain number, 644 and 0o644 are very different values (644 decimal is 0o1204). JavaScript's parsing functions each have personality. parseInt(str, radix) should always be called with an explicit radix: older engines interpreted a leading zero as octal, and even today parseInt auto-detects 0x but not 0b or 0o. Number('0b101') understands all three modern prefixes, but Number('') is 0 and parseInt stops at the first invalid character instead of failing — parseInt('12px', 10) is 12, which is either a feature or a landmine depending on the day. parseInt also first converts its argument to a string, producing famous nonsense on very small floats (parseInt(0.0000005) is 5, via the string '5e-7'). For output, toString(radix) handles bases 2 through 36, and padStart gives fixed-width dumps: (5).toString(2).padStart(8, '0') prints 00000101. For bit-level work at 64 bits, do it in BigInt — Number-based bitwise operators truncate to 32 bits and will corrupt the high half.
parseInt('08')          // 8 today; 0 in pre-ES5 engines — always pass radix
parseInt('0x1F')        // 31 — hex auto-detected
parseInt('0b101')       // 0  — binary prefix NOT understood
Number('0b101')         // 5  — all modern prefixes work
parseInt('12px', 10)    // 12 — stops at first invalid char
parseInt(0.0000005)     // 5  — via the string '5e-7' (!)

0o755                   // 493 — modern octal literal
1_000_000               // digit separators, ignored by the engine
(255).toString(2).padStart(8, '0')   // '11111111'
(0xDEADBEEFn << 8n).toString(16)     // 'deadbeef00' — 64-bit-safe in BigInt

Practical Bit Work: Permissions, Flags, and Colors

The three places working developers actually convert bases by hand are Unix permissions, feature flags, and colors — and each is a small idiom worth internalizing. Permissions: each octal digit is read plus write plus execute, weighted 4-2-1. So 755 decomposes as 7 = 4+2+1 (owner rwx), 5 = 4+1 (group r-x), 5 (others r-x). 644 is rw-, r--, r--. The setuid/setgid/sticky bits form an optional fourth leading digit — 4755 is setuid root territory, which is why you should notice a leading 4 or 2 in an audit. Going symbolic-to-octal in your head is just adding the weights. Flags: define each flag as 1 shifted left by its index, combine with bitwise OR, test with bitwise AND, clear with AND NOT, toggle with XOR. The binary view in a converter is the debugging view: a flags value of 13 is 1101, so bits 0, 2, and 3 are set. This layout is the same one used by file permission masks, Linux capabilities, CSS font-feature bitfields, and the flags integers in countless network protocols. The one JavaScript caveat: past 31 flags you must switch to BigInt or an array of words, because of the 32-bit truncation. Colors: a CSS hex color is three (or four, with alpha) bytes concatenated. Extracting channels is a shift plus a mask: red is value shifted right 16 AND 0xFF. This is why 0xFF0000 is pure red, and why the converter's hex and decimal views of 16711680 describe the same pixel.
// permissions: weights 4-2-1 per octal digit
// 755 = rwx r-x r-x     644 = rw- r-- r--     4755 = setuid + 755

// flags: define, set, test, clear, toggle
const READ  = 1 << 0;  // 1
const WRITE = 1 << 1;  // 2
const EXEC  = 1 << 2;  // 4
let flags = READ | EXEC;          // 5 = 101
(flags & WRITE) !== 0             // false
flags &= ~READ;                   // clear -> 100
flags ^= EXEC;                    // toggle -> 000

// colors: bytes packed into one integer
const c = 0xff8800;               // 16746496
const r = (c >> 16) & 0xff;       // 255
const g = (c >> 8)  & 0xff;       // 136
const b =  c        & 0xff;       // 0
Last updated:

About this tool

A number base converter switches an integer between decimal (base 10), binary (base 2), octal (base 8), and hexadecimal (base 16). Programmers reach for hex when reading memory addresses or HTTP byte counts, binary when manipulating bit flags, and octal mostly when setting Unix file permissions like 755 or 644.

How to use

  1. Type a number into any of the four fields — decimal, binary, octal, or hex.
  2. The other three fields update automatically with the equivalent representation.
  3. Click Copy on any row to grab that representation for your code or terminal.
  4. Use the leading 0x prefix as appropriate when pasting into source code.
  5. Watch for invalid input warnings if you mix incompatible characters (e.g., 9 in binary).

Common use cases

  • Reading a memory address from a stack trace and finding the decimal offset.
  • Translating Unix file permissions between symbolic (rwxr-xr-x) and octal (755) form.
  • Decoding a network packet field given as hex into the underlying integer value.
  • Working with bitmask flags by viewing the binary representation.
  • Converting an RGB channel value (0–255) into the hex pair used in a CSS color.
  • Checking that a hex constant in your code matches the decimal value in a spec.

Frequently asked questions

Q. Why does 10 in hex equal 16 in decimal?

A. Hex (base 16) digits go 0-9 then A-F. Position values are 16⁰, 16¹, … so "10" hex means 1×16 + 0 = 16.

Q. Are negative numbers supported?

A. For most calculator use the tool sticks to non-negative integers. Negative values in binary need an explicit width and two-complement encoding, which differs by platform.

Q. How big a number can I convert?

A. JavaScript handles integers up to 2^53 - 1 safely. Beyond that you need BigInt; this tool focuses on the typical 32/64-bit range.

Q. Why does the hex representation use uppercase letters?

A. Convention only. Lowercase is equally valid (and what JavaScript .toString(16) produces by default). Many specs and tools display uppercase for readability.