DEV Community

Dev Nestio
Dev Nestio

Posted on

I Built a Browser-Only IP / CIDR Calculator — IPv4 & IPv6, Subnet Split, Binary Breakdown (136 Tests)

CIDR notation is one of those things every developer encounters but rarely fully understands. What does /24 mean exactly? What is the broadcast address? How many usable hosts are in a /26? These questions come up during infra work, firewall rules, VPC setup, and Kubernetes networking — and I always had to reach for a tool.

So I built my own: a single HTML file that handles both IPv4 and IPv6 CIDR calculations, subnet splitting, supernetting, and binary visualization — with zero uploads, zero dependencies, and 136 passing tests.

Live tool → devnestio.pages.dev/cidr-calculator/


What it calculates

For IPv4:

  • Network address (host bits masked to 0)
  • Broadcast address (host bits all 1)
  • Subnet mask and wildcard mask
  • First and last usable host
  • Total addresses and usable hosts
  • IP class (A/B/C/D) and type (Private RFC 1918, Loopback, Link-local, Multicast, Public)
  • Binary breakdown with color-coded network vs host bits
  • Subnet splitter: divide a /24 into /26s, etc.
  • Supernet: one prefix up

For IPv6:

  • Compressed and expanded representations
  • Network and last address
  • Prefix mask and wildcard
  • Total address count (as BigInt — these numbers are huge)
  • Address type: Global Unicast, Link-local (fe80::/10), Unique Local (fc00::/7), Multicast (ff00::/8), Loopback (::1), Documentation (2001:db8::/32)

There is also a prefix reference table listing /8 through /32 with mask, wildcard, and host count.


The math: IPv4 bit operations

IPv4 addresses are 32-bit unsigned integers. CIDR prefix math is pure bitwise operations:

// prefix → subnet mask
function prefixToMask(prefix) {
  if (prefix === 0) return 0;
  return (0xFFFFFFFF << (32 - prefix)) >>> 0;
}

// from mask + IP → network, broadcast, hosts
const mask      = prefixToMask(prefix);
const wildcard  = (~mask) >>> 0;          // invert all bits
const network   = (ip & mask) >>> 0;      // AND with mask
const broadcast = (network | wildcard) >>> 0; // OR with wildcard
const firstHost = (network + 1) >>> 0;
const lastHost  = (broadcast - 1) >>> 0;
const usable    = Math.pow(2, 32 - prefix) - 2;
Enter fullscreen mode Exit fullscreen mode

The >>> 0 casts keep everything as unsigned 32-bit. JavaScript bitwise operators work on signed 32-bit integers internally, so without >>> 0, a /0 mask would give -1 instead of 4294967295.

Rendering IP addresses:

function uint32ToIPv4(n) {
  return [
    (n >>> 24) & 0xFF,
    (n >>> 16) & 0xFF,
    (n >>> 8)  & 0xFF,
    n          & 0xFF,
  ].join(.);
}
Enter fullscreen mode Exit fullscreen mode

Edge cases worth knowing

/31 (point-to-point links, RFC 3021): No broadcast or network address — both addresses are usable. My code special-cases prefix === 31: usableHosts = 2, and firstHost = network, lastHost = broadcast.

/32 (single host route): One address, used as a host route. totalHosts = 1, usableHosts = 1.

/0 (default route): network = 0.0.0.0, broadcast = 255.255.255.255. The left-shift 0xFFFFFFFF << 32 in JavaScript wraps around to 0xFFFFFFFF due to how shift amounts are masked — I guard with if (prefix === 0) return 0.


The math: IPv6 with BigInt

IPv6 addresses are 128-bit. JavaScript numbers are 64-bit floats, so they can not represent 128-bit integers precisely. BigInt handles this:

function ipv6ToBigInt(expanded) {
  // expanded = "2001:0db8:0000:..."
  return BigInt("0x" + expanded.split(":").join(""));
}

function calcIPv6Network(ipInt, prefix) {
  const maxVal = (1n << 128n) - 1n;
  const maskInt = prefix === 0 ? 0n
    : (maxVal << BigInt(128 - prefix)) & maxVal;
  return ipInt & maskInt;
}
Enter fullscreen mode Exit fullscreen mode

Total addresses in a /64:

const totalHosts = 1n << BigInt(128 - 64); // 2n^64 = 18446744073709551616n
Enter fullscreen mode Exit fullscreen mode

That is about 18.4 quintillion addresses per /64. You cannot fit that in a regular JavaScript number.

IPv6 compression/expansion

IPv6 has a compact notation that omits leading zeros and replaces the longest run of zero groups with ::. I implement both directions:

Expand (e.g., ::10000:0000:...:0001):

function expandIPv6(str) {
  if (str.includes("::")) {
    const [left, right] = str.split("::");
    const leftParts  = left  ? left.split(":")  : [];
    const rightParts = right ? right.split(":") : [];
    const missing = 8 - leftParts.length - rightParts.length;
    const mid = Array(missing).fill("0000");
    return [...leftParts, ...mid, ...rightParts]
      .map(p => p.padStart(4, "0")).join(":");
  }
  return str.split(":").map(p => p.padStart(4, "0")).join(":");
}
Enter fullscreen mode Exit fullscreen mode

Compress (find longest run of 0 groups, replace with ::):

function compressIPv6(expanded) {
  const groups = expanded.split(":").map(g => parseInt(g, 16).toString(16));
  let bestStart = -1, bestLen = 0, curStart = -1, curLen = 0;
  for (let i = 0; i <= groups.length; i++) {
    if (i < groups.length && groups[i] === "0") {
      if (curStart === -1) { curStart = i; curLen = 1; }
      else curLen++;
    } else {
      if (curLen > bestLen) { bestLen = curLen; bestStart = curStart; }
      curStart = -1; curLen = 0;
    }
  }
  if (bestLen < 2) return groups.join(":");
  const before = groups.slice(0, bestStart).join(":");
  const after  = groups.slice(bestStart + bestLen).join(":");
  return (before ? before + "::" : "::") + after;
}
Enter fullscreen mode Exit fullscreen mode

Binary visualization

One of the most useful features for learning CIDR is seeing the binary breakdown. For 192.168.1.0/24:

IP Address   11000000.10101000.00000001.00000000
Subnet Mask  11111111.11111111.11111111.00000000
Wildcard     00000000.00000000.00000000.11111111
Network      11000000.10101000.00000001.00000000
Enter fullscreen mode Exit fullscreen mode

The first 24 bits (network part) are highlighted in teal; the remaining 8 bits (host part) in amber. This makes it immediately clear why 192.168.1.0/24 gives 256 total addresses.

function uint32ToBinary(n) {
  return [24, 16, 8, 0].map(shift =>
    ((n >>> shift) & 0xFF).toString(2).padStart(8, "0")
  ).join(".");
}
Enter fullscreen mode Exit fullscreen mode

Subnet splitting

Splitting a network into equal-sized subnets:

function splitSubnet(info, newPrefix) {
  if (newPrefix <= info.prefix || newPrefix > 32) return null;
  const count = Math.pow(2, newPrefix - info.prefix);
  const subnetSize = Math.pow(2, 32 - newPrefix);
  const subnets = [];
  for (let i = 0; i < count; i++) {
    const net = (info.network + i * subnetSize) >>> 0;
    // ...compute broadcast, first/last host
    subnets.push({ cidr: `${uint32ToIPv4(net)}/${newPrefix}`, ... });
  }
  return subnets;
}
Enter fullscreen mode Exit fullscreen mode

A /24 split into /26 gives 4 subnets of 64 addresses each (62 usable). Each subnet in the output is clickable — it loads that CIDR into the calculator instantly.


Testing: 136 tests, plain node

The pure-logic functions are tested with node test/test.js — no framework, just assert:

node test/test.js
Enter fullscreen mode Exit fullscreen mode
Suite Tests
parseIPv4 11
uint32ToIPv4 round-trips 5
prefixToMask 7
maskToPrefix (incl. inverse loop) 7
uint32ToBinary 5
calcIPv4 /24 full check 12
calcIPv4 edge cases 24
parseCIDRv4 5
splitSubnet 13
supernet 4
expandIPv6 / compressIPv6 8
calcIPv6 10
IPv6 BigInt conversion 5
isIPv6 4
calculate() dispatcher 5
Additional calcIPv4 12
Total 136

Key test vectors:

test("prefixToMask and maskToPrefix are inverse for /0–/32", () => {
  for (let i = 0; i <= 32; i++) {
    assert.strictEqual(maskToPrefix(prefixToMask(i)), i);
  }
});

test("/64 totalHosts = 2^64", () => {
  const r = calcIPv6("2001:db8::/64");
  assert.strictEqual(r.totalHosts, 2n ** 64n);
});

test("host IP gets masked to network: 192.168.1.100/24", () => {
  const r = calcIPv4("192.168.1.100/24");
  assert.strictEqual(r.networkStr, "192.168.1.0");
});
Enter fullscreen mode Exit fullscreen mode

Tech used

  • Bitwise operations — all IPv4 math (>>>, &, |, ~)
  • BigInt — IPv6 128-bit arithmetic
  • No dependencies — pure Vanilla JS, single HTML file
  • node + assert — testing without frameworks

Try it

Next time someone asks "what is /25 again?" you will know: 128 addresses, 126 usable, split your /24 exactly in half.

Top comments (0)