An IPv4 Subnet Calculator With Binary Visualization and /31 Handling
IP address math is bitwise arithmetic on 32-bit unsigned integers.
ipToInt('192.168.1.100')gives you0xC0A80164. Anding with a mask gives the network. Oring with the wildcard gives the broadcast. Subnet splitting is bit shifts on the CIDR. It's satisfying to implement because it's mostly one-line functions.
Subnet math is one of those things network engineers do in their heads and developers panic over. What's the broadcast address for 10.0.5.37/22? What's a /31 even used for? An interactive calculator makes the bitwise operations visible.
🔗 Live demo: https://sen.ltd/portfolio/ipv4-subnet/
📦 GitHub: https://github.com/sen-ltd/ipv4-subnet
Features:
- IP + CIDR input parsing
- Network, broadcast, first/last host, total/usable hosts
- Subnet mask, wildcard mask, network class
- RFC 1918 private address detection
- Binary visualization (network vs host bits highlighted)
- Subnet splitting (split /16 into 256 /24s etc.)
- IP-in-range check
- CIDR reference table
- Japanese / English UI
- Zero dependencies, 63 tests
IP as 32-bit unsigned int
The fundamental trick: store IPs as 32-bit integers, not strings:
export function ipToInt(ip) {
const parts = ip.split('.').map(Number);
return (parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
}
export function intToIp(n) {
return [
(n >>> 24) & 0xFF,
(n >>> 16) & 0xFF,
(n >>> 8) & 0xFF,
n & 0xFF,
].join('.');
}
The >>> 0 is critical — JavaScript's bitwise operations produce signed 32-bit integers, so a high bit like 192.x.x.x becomes negative. Unsigned right shift by 0 forces conversion to the unsigned 32-bit representation.
Subnet math
Once IPs are integers, the rest is trivial bitwise:
export function calculateSubnet(ip, cidr) {
const mask = cidrToMask(cidr);
const network = (ip & mask) >>> 0;
const broadcast = (network | (~mask >>> 0)) >>> 0;
// /31 and /32 are special
let firstHost, lastHost, usableHosts;
if (cidr >= 31) {
firstHost = network;
lastHost = broadcast;
usableHosts = cidr === 32 ? 1 : 2; // RFC 3021 for /31
} else {
firstHost = (network + 1) >>> 0;
lastHost = (broadcast - 1) >>> 0;
usableHosts = Math.pow(2, 32 - cidr) - 2;
}
return { network, broadcast, firstHost, lastHost, usableHosts, mask };
}
function cidrToMask(cidr) {
return cidr === 0 ? 0 : (0xFFFFFFFF << (32 - cidr)) >>> 0;
}
Network = IP AND mask (zero out host bits). Broadcast = network OR inverted mask (set all host bits). First usable host = network + 1, last = broadcast - 1. Simple enough that the whole function fits in 20 lines.
/31 and /32: the special cases
Normal subnets lose 2 addresses to network and broadcast. But:
- /32: A single host route. 1 address. No broadcast, no "usable hosts" concept. Used for loopback and host-specific routes.
- /31: Point-to-point link per RFC 3021. Both addresses are usable — no network or broadcast. 2 usable hosts out of 2 total. Saves addresses on router-to-router links.
The function has to special-case these because the formula 2^(32-cidr) - 2 gives -1 and 0 respectively, which is wrong.
RFC 1918 private detection
Three address ranges are "private" (not routable on the public internet):
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
export function isPrivate(ip) {
return (
isInSubnet(ip, ipToInt('10.0.0.0'), 8) ||
isInSubnet(ip, ipToInt('172.16.0.0'), 12) ||
isInSubnet(ip, ipToInt('192.168.0.0'), 16)
);
}
Subnet splitting
Given a /16, how do you split into /24s? You generate 2^(newCidr - oldCidr) subnets, each advancing by the new subnet size:
export function splitSubnet(networkIp, oldCidr, newCidr) {
if (newCidr <= oldCidr) return [];
const count = Math.pow(2, newCidr - oldCidr);
const step = Math.pow(2, 32 - newCidr);
const subnets = [];
for (let i = 0; i < count; i++) {
subnets.push({
network: (networkIp + i * step) >>> 0,
cidr: newCidr,
});
}
return subnets;
}
Splitting a /16 into /24s gives 256 subnets; into /25s gives 512; into /30s gives 16384. The UI caps display at 1024 for sanity.
Binary visualization
The most educational part: show the IP in binary with the mask line highlighted:
192.168. 1.100 (IP)
11000000.10101000.00000001.01100100 (binary)
NNNNNNNN.NNNNNNNN.NNNNNNNN.NNNN____ (/28)
The N's are network bits, the _'s are host bits. Seeing it like this makes the & with the mask obvious — the network bits stay, the host bits become 0.
Series
This is entry #96 in my 100+ public portfolio series.
- 📦 Repo: https://github.com/sen-ltd/ipv4-subnet
- 🌐 Live: https://sen.ltd/portfolio/ipv4-subnet/
- 🏢 Company: https://sen.ltd/

Top comments (0)