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
/24into/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;
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(.);
}
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;
}
Total addresses in a /64:
const totalHosts = 1n << BigInt(128 - 64); // 2n^64 = 18446744073709551616n
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., ::1 → 0000: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(":");
}
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;
}
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
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(".");
}
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;
}
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
| 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");
});
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
- Tool: devnestio.pages.dev/cidr-calculator/
- Hub (26 tools): devnestio.pages.dev
Next time someone asks "what is /25 again?" you will know: 128 addresses, 126 usable, split your /24 exactly in half.
Top comments (0)