The Jobsite Doesn't Wait for Ads to Load
Picture this: you're on a ladder, EMT bender in one hand, phone in the other, gloves half off so the touchscreen registers. You need one number — the distance between two pencil marks for a 30° offset around a duct — and instead you get:
- A cookie consent modal blocking the input field
- An interstitial ad that hijacks your tap and opens the App Store
- A page that reflows three times while ad units above the fold finish loading
- A "multiplier chart" buried under 1,500 words of AI-generated filler before you can even see it Electricians don't have the patience for this, and neither do I. Conduit bending is unforgiving geometry — a bad shrink calculation means a stub that's a half-inch short, a saddle that doesn't clear the duct, or conduit in the scrap pile. The math deserves a tool that respects it.
So the Conduit Bending Calculator became the second entry in the HypeCalc suite — same rules as the Stair Calculator before it: no ads, no tracking scripts, no layout shift, and math that's isolated, typed, and testable.
This post breaks down the actual trigonometry behind offset bending and saddle layout, and the Next.js architecture — SEO metadata, JSON-LD schema, and a sticky client-side calculator — that ships it as a fast, indexable page instead of a bloated single-page app.
The Trigonometry: Why Conduit Bending Is Just Triangles With Consequences
Every offset bend an electrician makes is a right triangle. The obstacle height is one leg, the bend angle sets the hypotenuse, and the multiplier constants are just precomputed trig ratios so you're not doing csc(θ) by hand on a ladder.
1. The Offset Multiplier (Distance Between Marks)
To clear an obstacle of a given height at a chosen bend angle, the distance between your two pencil marks is the obstacle height scaled by the cosecant of the bend angle:
distanceBetweenMarks = offsetDepth × csc(angle)
In practice, electricians memorize the multiplier rather than compute cosecant on site:
| Bend Angle | Multiplier (csc θ) | Shrink per Inch of Offset |
|---|---|---|
| 10° | 6.00 | 1/16" |
| 22.5° | 2.61 | 3/16" |
| 30° | 2.00 | 1/4" |
| 45° | 1.41 | 3/8" |
| 60° | 1.15 | 1/2" |
2. Shrinkage
Because the conduit now travels diagonally instead of straight, the overall run "shrinks" back toward the obstacle. That shrink is the obstacle height times a fixed constant per angle:
totalShrink = offsetDepth × shrinkConstant(angle)
Forget to add this back into your reference mark, and every downstream measurement on that run is off.
3. The 90° Stub Take-Up
A hand bender's shoe has a radius, which "eats" pipe length during the bend. To land a stub at an exact height, you subtract the bender's fixed take-up value from your target height before marking the pipe:
stubMark = desiredStubHeight - benderTakeUp
Take-up is a lookup by trade size and conduit type (EMT vs. rigid), not something you derive — so it lives as a constant table, not a formula.
4. The 3-Point Saddle
Saddles clear an obstacle with three bends instead of two: a center bend (typically 45°) flanked by two symmetric return bends (typically 22.5°). The center mark accounts for a small correction based on obstacle height, and the two outer marks are offset symmetrically from center:
centerMark = obstacleCenterDistance + (obstacleHeight × 3/16)
outerMarkLow = centerMark - (obstacleHeight × 2.5)
outerMarkHigh = centerMark + (obstacleHeight × 2.5)
The TypeScript Core: Lookup Tables + Pure Functions
Same architectural rule as the stair calculator: no calculation logic touches a React component directly. Bend constants live in typed tables, and the functions that consume them are pure and independently testable.
// lib/calculateConduitBend.ts
export type BendAngle = 10 | 22.5 | 30 | 45 | 60;
export type ConduitType = "EMT" | "Rigid";
export type TradeSize = "1/2" | "3/4" | "1" | "1-1/4";
// Cosecant multiplier and shrink-per-inch, precomputed per standard bend angle
const OFFSET_CONSTANTS: Record<BendAngle, { multiplier: number; shrinkPerInch: number }> = {
10: { multiplier: 6.0, shrinkPerInch: 0.0625 },
22.5: { multiplier: 2.61, shrinkPerInch: 0.1875 },
30: { multiplier: 2.0, shrinkPerInch: 0.25 },
45: { multiplier: 1.41, shrinkPerInch: 0.375 },
60: { multiplier: 1.15, shrinkPerInch: 0.5 },
};
// Bender shoe take-up (inches), by conduit type and trade size
const TAKE_UP_TABLE: Record<ConduitType, Record<TradeSize, number>> = {
EMT: { "1/2": 5, "3/4": 6, "1": 8, "1-1/4": 11 },
Rigid: { "1/2": 6, "3/4": 8, "1": 10, "1-1/4": 13 },
};
export interface OffsetInput {
offsetDepthInches: number;
angle: BendAngle;
}
export interface OffsetResult {
distanceBetweenMarksInches: number;
totalShrinkInches: number;
}
export function calculateOffset({
offsetDepthInches,
angle,
}: OffsetInput): OffsetResult {
if (offsetDepthInches <= 0) {
throw new Error("Offset depth must be a positive number.");
}
const { multiplier, shrinkPerInch } = OFFSET_CONSTANTS[angle];
return {
distanceBetweenMarksInches: Number(
(offsetDepthInches * multiplier).toFixed(3)
),
totalShrinkInches: Number(
(offsetDepthInches * shrinkPerInch).toFixed(3)
),
};
}
export interface StubInput {
desiredStubHeightInches: number;
conduitType: ConduitType;
tradeSize: TradeSize;
}
export interface StubResult {
takeUpInches: number;
stubMarkInches: number;
}
export function calculateStub({
desiredStubHeightInches,
conduitType,
tradeSize,
}: StubInput): StubResult {
const takeUpInches = TAKE_UP_TABLE[conduitType][tradeSize];
const stubMarkInches = desiredStubHeightInches - takeUpInches;
if (stubMarkInches <= 0) {
throw new Error(
"Desired stub height must exceed the bender take-up distance."
);
}
return {
takeUpInches,
stubMarkInches: Number(stubMarkInches.toFixed(3)),
};
}
export interface SaddleInput {
obstacleCenterDistanceInches: number;
obstacleHeightInches: number;
}
export interface SaddleResult {
centerMarkInches: number;
outerMarkLowInches: number;
outerMarkHighInches: number;
}
export function calculateThreePointSaddle({
obstacleCenterDistanceInches,
obstacleHeightInches,
}: SaddleInput): SaddleResult {
const centerMarkInches =
obstacleCenterDistanceInches + obstacleHeightInches * 0.1875; // 3/16"
const spread = obstacleHeightInches * 2.5;
return {
centerMarkInches: Number(centerMarkInches.toFixed(3)),
outerMarkLowInches: Number((centerMarkInches - spread).toFixed(3)),
outerMarkHighInches: Number((centerMarkInches + spread).toFixed(3)),
};
}
Splitting this into three focused functions instead of one mega calculateConduit() was deliberate:
-
Single responsibility, testable in isolation — an offset bend, a stub, and a saddle are three different real-world tasks. Bundling them into one function with a
modeflag would make every test case need to stub out irrelevant parameters. -
Typed lookup tables catch mistakes at compile time.
TradeSizeandConduitTypeare string literal unions, soTAKE_UP_TABLE["Rigid"]["2"]fails to compile instead of silently returningundefinedat 2am on a jobsite. -
No magic numbers in the UI layer. The component never hardcodes
0.1875or2.5— those constants live exactly once, next to the formula that uses them.
The Page Architecture: SEO-First, Interactivity Second
This is where the conduit calculator diverges from a typical "throw a form on a page" build. Because it targets long-tail, high-intent search traffic ("conduit 3 point and 4 point saddle bend calculator," "90 degree stub up and kick bend take up calculator"), the page is structured as a content-first Server Component wrapping a client-only calculator, not a client-rendered SPA with SEO bolted on.
// page.tsx (excerpt)
export const metadata: Metadata = {
title: 'Conduit Bending Calculator - EMT Offsets, Saddles, Shrink',
description: 'Free conduit bending calculator. Calculate EMT offset multiplier marks, shrink per inch, 3-point & 4-point saddles, 90° stub take-up, and concentric bends.',
keywords: [ /* long-tail, field-language queries */ ],
};
export default function ConduitBendingPage() {
return (
<main>
{/* JSON-LD: Breadcrumb, SoftwareApplication, FAQPage */}
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }} />
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10">
{/* LEFT: static, server-rendered reference content */}
<div className="lg:col-span-5">{/* formulas, tables, FAQ */}</div>
{/* RIGHT: sticky client component, the actual tool */}
<div className="lg:col-span-4">
<StickyWrapper>
<CalculatorCard title="Electrical Conduit Bending Calculator">
<CalculatorForm />
</CalculatorCard>
</StickyWrapper>
</div>
</div>
</main>
);
}
A few things worth calling out:
-
page.tsxstays a Server Component. All the metadata, JSON-LD, and reference tables render on the server with zero client JS cost. OnlyCalculatorFormopts into"use client"— the interactive slice is as small as possible, which keeps the shipped bundle lean. -
The FAQ schema is a 1:1 match with visible on-page content. Search engines increasingly discount JSON-LD that doesn't correspond to something a user can actually read on the page — so instead of writing schema as an afterthought, the FAQ section and the
faqSchemaobject are generated from the same source of truth. -
The calculator is sticky, not modal. On desktop,
StickyWrapperkeeps the tool pinned in the viewport while the reference content scrolls — so a user reading "how do I calculate a 3-point saddle" never loses the calculator they came for. -
No
SoftwareApplicationfake review inflation. The schema explicitly omitsaggregateRatingrather than fabricating a 4.9-star rating with zero reviews — a shortcut a lot of "SEO-optimized" calculator sites lean on, and one that search engines have gotten much better at penalizing.
What This Buys You Over the Legacy Field-Calculator Sites
| Metric | Typical Ad-Supported Trade Calculators | HypeCalc |
|---|---|---|
| Interstitial / consent modals blocking input | 1–2, often mid-interaction | 0 |
| Layout shift from late-loading ad units | High (CLS 0.2–0.4+) | 0 |
| JS shipped for a two-field calculator | Hundreds of KB to MBs | Client bundle scoped to CalculatorForm only |
| FAQ content matching structured data | Often mismatched or padded | 1:1, same source of truth |
| Usable one-handed on a ladder | Rarely | Yes |
The recurring theme across both calculators in this suite: every architectural decision either serves the user's actual task, or it doesn't ship. No dark patterns disguised as monetization, no schema written to game rankings instead of describe the page.
Try It on an Actual Bend
The offset, stub, and saddle logic above is live, not a demo:
HypeCalc Conduit Bending Calculator →
Run it through a real offset — 4" obstacle, 30° bend — and check that you land on 8" between marks and 1" of shrink, same as the worked example in the field guide above.
Discussion
A few things I'd genuinely like this community's take on:
- Where's the right line between "SEO-optimized content page" and "bloated content page"? This build keeps ~1,500 words of reference content server-rendered next to the tool — is that good practice for utility sites, or creeping toward the same pattern I'm criticizing?
- Lookup-table constants vs. computing trig live in the browser — for domain constants like bender take-up that are standardized by manufacturer, not derived, is a typed table always the right call over a formula?
- What's a trade or domain you know well where the "official" web calculator gets the math subtly wrong? I'm collecting jobsite-verified formulas for the next tool in this suite — drop them below.
Top comments (0)