DEV Community

Cover image for I Rebuilt the Electrician's Conduit Bending Calculator Because Trig Shouldn't Require Dodging Popups
G S
G S

Posted on

I Rebuilt the Electrician's Conduit Bending Calculator Because Trig Shouldn't Require Dodging Popups

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)),
  };
}
Enter fullscreen mode Exit fullscreen mode

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 mode flag would make every test case need to stub out irrelevant parameters.
  • Typed lookup tables catch mistakes at compile time. TradeSize and ConduitType are string literal unions, so TAKE_UP_TABLE["Rigid"]["2"] fails to compile instead of silently returning undefined at 2am on a jobsite.
  • No magic numbers in the UI layer. The component never hardcodes 0.1875 or 2.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>
  );
}
Enter fullscreen mode Exit fullscreen mode

A few things worth calling out:

  • page.tsx stays a Server Component. All the metadata, JSON-LD, and reference tables render on the server with zero client JS cost. Only CalculatorForm opts 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 faqSchema object are generated from the same source of truth.
  • The calculator is sticky, not modal. On desktop, StickyWrapper keeps 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 SoftwareApplication fake review inflation. The schema explicitly omits aggregateRating rather 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:

  1. 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?
  2. 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?
  3. 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)