Board Foot Calculator

Professional board foot calculator for woodworkers, contractors, and estimators. Compute board feet from thickness, width, length, quantity, add waste, and estimate cost. Metric and imperial supported.

Board details

Add as many board lines as needed. Each row calculates its board feet using the specified thickness, width, and length, then multiplies by quantity.

%
$

Used for display in the estimated cost.

How to Use This Calculator

Enter each board line with the actual thickness, width, and length, then multiply by how many identical boards you plan to cut. The calculator totals the board feet for every row and applies the waste factor and unit price to produce cost and volume estimates.

Start with real dimensions rather than nominal sizes to keep estimates accurate. Use the waste field to add trimming, defects, or selection allowances, and the currency field to match your quoting format.

Methodology

We follow the NHLA (National Hardwood Lumber Association) definition of the board foot: a volume equal to 144 cubic inches. The base formula is thickness × width × length, adjusted for units, and divided by the proper conversion constant. Waste is applied proportionally, and the selected price per board foot multiplies the waste-adjusted total.

1 board foot = 144 in³ = 1/12 ft³ ≈ 0.002359737 m³. All results honor this definition and round consistently before display.

  • Imperial rows expect inches for thickness/width and feet for length; metric rows expect millimeters and meters.
  • The waste percentage increases the total board feet before the price is multiplied.
  • Volume conversions help compare the estimate to cubic footage/cubic meter requirements.

Tutti i calcoli si basano rigorosamente sulle formule e sui dati forniti da questa fonte.

Full original guide (expanded)

Glossary of Variables

  • Thickness (T): Board thickness. Inches (imperial) or millimeters (metric).
  • Width (W): Board width. Inches or millimeters.
  • Length (L): Board length. Feet or meters.
  • Quantity: Number of identical boards of that line.
  • Board Feet (BF): Volume measure; totals combine all rows.
  • Waste Factor (%): Additive percentage for defects, trim, or selection.
  • Price per BF: Unit price used for material cost estimates.

Example

Suppose you need four boards, each 2 in thick, 8 in wide, and 10 ft long. Set quantity to 4, waste to 10%, and price to $5.50/BF.

  1. Per-board board feet: (2 × 8 × 10) / 12 = 13.333 BF.
  2. Total before waste: 13.333 × 4 = 53.333 BF.
  3. Apply waste: 53.333 × 1.10 = 58.666 BF.
  4. Estimated cost: 58.666 × 5.50 ≈ $322.66.

Frequently Asked Questions

Is nominal size (e.g., 2×4) the same as actual size?

No. When boards are surfaced or planed, their dimensions shrink. Use actual measured values for accurate board-foot totals.

Can I mix imperial and metric inputs?

Stick to one system per run. Switch to metric to work with millimeters and meters; the calculator converts everything internally.

How precise are the results?

The calculator performs double-precision math and rounds results for display. Export if you need extra precision.

What about volumes in cubic feet or cubic meters?

The panel provides both. ft³ is BF ÷ 12; m³ multiplies BF by 0.002359737.

What waste factor should I use?

5%–15% is typical for hardwood projects. Use higher values when matching colors, trimming defects, or working with short boards.

Does this include tax or cutting fees?

No. The estimate multiplies the waste-adjusted BF by your entered rate. Add tax, machining, and delivery separately.

Is this method accepted in industry?

Yes. It follows the NHLA definition of the board foot and is aligned with North American lumber practice.

Formulas
Imperial inputs \(T_{in}, W_{in}, L_{ft}\): \[\text{BF} = \frac{T_{in} \times W_{in} \times L_{ft}}{12}\] Length in inches: \[\text{BF} = \frac{T_{in} \times W_{in} \times L_{in}}{144}\] Metric inputs \(T_{mm}, W_{mm}, L_{m}\): \[\text{BF} = \frac{T_{mm} \times W_{mm} \times L_{m}}{2359.737216}\] Conversion: \(1\,\text{BF} = 1/12\,\text{ft}^3 \approx 0.002359737\,\text{m}^3\).
Citations
Changelog
Version: 0.1.0-draft
Last code update: 2026-01-19
  • Initial audit spec draft generated from HTML extraction (review required).
  • Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
  • Confirm sources are authoritative and relevant to the calculator methodology.
✓ Verified by Ugo Candido Last Updated: 2026-01-19 Version 0.1.0-draft
Version 1.5.0
; return `${safeSymbol}${fmtNumber(value, 2)}`; }; const els = { rows: document.getElementById('rows'), rowTemplate: document.getElementById('rowTemplate'), addRowBtn: document.getElementById('addRowBtn'), addCommonBtn: document.getElementById('addCommonBtn'), calcBtn: document.getElementById('calcBtn'), resetBtn: document.getElementById('resetBtn'), wasteInput: document.getElementById('wastePercent'), priceInput: document.getElementById('pricePerBF'), currencyInput: document.getElementById('currencySymbol'), currencyBadge: document.getElementById('currencyBadge'), totalBF: document.getElementById('totalBF'), adjustedBF: document.getElementById('adjustedBF'), estCost: document.getElementById('estCost'), wasteNote: document.getElementById('wasteNote'), priceNote: document.getElementById('priceNote'), totalPieces: document.getElementById('totalPieces'), volFt3: document.getElementById('volFt3'), volM3: document.getElementById('volM3'), rowSummary: document.getElementById('rowSummary'), errorBox: document.getElementById('errorBox') }; const fieldErrorEls = { waste: document.getElementById('wasteError'), price: document.getElementById('priceError') }; let unitSystem = 'imperial'; let rowCounter = 0; function updateUnits() { const thicknessUnits = els.rows.querySelectorAll('[data-unit-thk]'); const widthUnits = els.rows.querySelectorAll('[data-unit-wid]'); const lengthUnits = els.rows.querySelectorAll('[data-unit-len]'); const placeholderThickness = unitSystem === 'imperial' ? 'e.g., 1.75' : 'e.g., 45'; const placeholderWidth = unitSystem === 'imperial' ? 'e.g., 8' : 'e.g., 200'; const placeholderLength = unitSystem === 'imperial' ? 'e.g., 10' : 'e.g., 3.2'; thicknessUnits.forEach(el => el.textContent = unitSystem === 'imperial' ? 'in' : 'mm'); widthUnits.forEach(el => el.textContent = unitSystem === 'imperial' ? 'in' : 'mm'); lengthUnits.forEach(el => el.textContent = unitSystem === 'imperial' ? 'ft' : 'm'); els.rows.querySelectorAll('[data-input="thickness"]').forEach(input => input.placeholder = placeholderThickness); els.rows.querySelectorAll('[data-input="width"]').forEach(input => input.placeholder = placeholderWidth); els.rows.querySelectorAll('[data-input="length"]').forEach(input => input.placeholder = placeholderLength); } function createRow() { const node = els.rowTemplate.content.firstElementChild.cloneNode(true); const id = `row-${++rowCounter}`; node.dataset.rowId = id; const removeBtn = node.querySelector('[data-remove]'); removeBtn.addEventListener('click', () => { if (els.rows.children.length === 1) return; node.remove(); const debouncedUpdate = debounce(update, 100); document.querySelectorAll('#inputsCard input, #inputsCard select, #inputsCard textarea') .forEach((el) => { el.addEventListener('input', debouncedUpdate); el.addEventListener('change', debouncedUpdate); }); update(); }); const tipBtn = node.querySelector('[data-tip-btn]'); const tip = node.querySelector('[data-tip]'); if (tipBtn && tip) { tip.id = `${id}-tip`; tipBtn.setAttribute('aria-expanded', 'false'); tipBtn.setAttribute('aria-controls', tip.id); tip.hidden = true; tipBtn.addEventListener('click', () => { const expanded = tipBtn.getAttribute('aria-expanded') === 'true'; tipBtn.setAttribute('aria-expanded', expanded ? 'false' : 'true'); tip.hidden = expanded; }); } node.querySelectorAll('input').forEach(input => { input.addEventListener('input', debouncedUpdate); input.addEventListener('change', debouncedUpdate); }); els.rows.appendChild(node); updateUnits(); return node; } function setRowValues(row, values = {}) { ['thickness', 'width', 'length', 'quantity'].forEach(key => { const input = row.querySelector(`[data-input="${key}"]`); if (!input) return; if (values[key] !== undefined) { input.value = values[key]; } }); } function parseInputs() { const rows = []; els.rows.querySelectorAll('[data-row]').forEach(rowEl => { const data = { element: rowEl, thickness: parseFloat(rowEl.querySelector('[data-input="thickness"]').value), width: parseFloat(rowEl.querySelector('[data-input="width"]').value), length: parseFloat(rowEl.querySelector('[data-input="length"]').value), quantity: parseFloat(rowEl.querySelector('[data-input="quantity"]').value) }; rows.push(data); }); return { rows, wastePercent: parseFloat(els.wasteInput.value), pricePerBF: parseFloat(els.priceInput.value), currencySymbol: els.currencyInput.value.trim() || ', unitSystem }; } function setRowError(rowEl, key, message) { const errEl = rowEl.querySelector(`[data-error="${key}"]`); if (!errEl) return; errEl.textContent = message || ''; } function clearRowErrors(rowEl) { rowEl.querySelectorAll('[data-error]').forEach(err => err.textContent = ''); } function clearFieldErrors() { Object.values(fieldErrorEls).forEach(el => { if (el) el.textContent = ''; }); } function setFieldError(field, message) { const target = fieldErrorEls[field]; if (!target) return; target.textContent = message; } function validate(inputs) { const errors = []; clearFieldErrors(); let hasValidRow = false; inputs.rows.forEach((row, index) => { clearRowErrors(row.element); let rowValid = true; if (!Number.isFinite(row.thickness) || row.thickness <= 0) { setRowError(row.element, 'thickness', 'Enter a thickness greater than 0.'); rowValid = false; } if (!Number.isFinite(row.width) || row.width <= 0) { setRowError(row.element, 'width', 'Enter a width greater than 0.'); rowValid = false; } if (!Number.isFinite(row.length) || row.length <= 0) { setRowError(row.element, 'length', 'Enter a length greater than 0.'); rowValid = false; } if (!Number.isFinite(row.quantity) || row.quantity < 1 || !Number.isInteger(row.quantity)) { setRowError(row.element, 'quantity', 'Enter a whole number of 1 or more.'); rowValid = false; } if (!rowValid) { errors.push(`Row ${index + 1} has invalid dimensions.`); } else { hasValidRow = true; } }); if (!hasValidRow) { errors.push('Provide at least one valid board line.'); } if (!Number.isFinite(inputs.wastePercent) || inputs.wastePercent < 0 || inputs.wastePercent > 100) { errors.push('Waste must be between 0% and 100%.'); setFieldError('waste', 'Enter a percentage between 0 and 100.'); } if (!Number.isFinite(inputs.pricePerBF) || inputs.pricePerBF < 0) { errors.push('Price per board foot must be 0 or greater.'); setFieldError('price', 'Enter a value that is 0 or higher.'); } return { ok: errors.length === 0, errors }; } function bfPerPiece(t, w, l, unit) { if (unit === 'imperial') { return (t * w * l) / 12; } const DEN = 2359.737216; return (t * w * l) / DEN; } function compute(inputs) { let totalBF = 0; let pieces = 0; const rowDetails = []; inputs.rows.forEach((row, index) => { const thickness = row.thickness; const width = row.width; const length = row.length; const quantity = row.quantity; const detail = { index, total: 0, thickness, width, length, quantity, valid: false }; if ([thickness, width, length, quantity].every(val => Number.isFinite(val) && val > 0)) { const perPiece = bfPerPiece(thickness, width, length, inputs.unitSystem); const rowTotal = perPiece * quantity; detail.total = rowTotal; detail.valid = true; totalBF += rowTotal; pieces += quantity; } rowDetails.push(detail); }); const waste = Math.max(0, Math.min(100, Number.isFinite(inputs.wastePercent) ? inputs.wastePercent : 0)); const adjustedBF = totalBF * (1 + waste / 100); const price = Math.max(0, Number.isFinite(inputs.pricePerBF) ? inputs.pricePerBF : 0); const cost = adjustedBF * price; const volFt3 = totalBF / 12; const volM3 = totalBF * 0.002359737216; return { totalBF, adjustedBF, wastePercent: waste, pricePerBF: price, cost, pieces, rowDetails, volFt3, volM3, currencySymbol: inputs.currencySymbol }; } function format(outputs, inputs) { const rowTotals = outputs.rowDetails.map(detail => roundTo(detail.total, 2)); const summaryLines = outputs.rowDetails .filter(detail => detail.valid) .map((detail, idx) => { const unitsShort = inputs.unitSystem === 'imperial' ? 'in' : 'mm'; const lenUnit = inputs.unitSystem === 'imperial' ? 'ft' : 'm'; return `#${detail.index + 1}: ${detail.quantity} × ${fmtNumber(detail.thickness)}${unitsShort} × ${fmtNumber(detail.width)}${unitsShort} × ${fmtNumber(detail.length)}${lenUnit} = ${fmtNumber(detail.total)} BF`; }); return { totalBF: `${fmtNumber(outputs.totalBF)} BF`, adjustedBF: `${fmtNumber(outputs.adjustedBF)} BF`, estCost: formatCurrency(outputs.currencySymbol, outputs.cost), wasteNote: `Includes ${fmtNumber(outputs.wastePercent, 0)}% waste`, priceNote: `${fmtNumber(outputs.adjustedBF)} × ${outputs.currencySymbol}${fmtNumber(outputs.pricePerBF)}`, totalPieces: `${Math.round(outputs.pieces)} ${Math.round(outputs.pieces) === 1 ? 'piece' : 'pieces'}`, volFt3: `${fmtNumber(outputs.volFt3, 3)} ft³`, volM3: `${fmtNumber(outputs.volM3, 6)} m³`, rowSummary: summaryLines.length ? summaryLines.join(' | ') : '—', rowTotals }; } function render(formatted = {}, errors = []) { if (errors.length) { els.errorBox.style.display = 'block'; els.errorBox.textContent = errors.join(' '); } else { els.errorBox.style.display = 'none'; els.errorBox.textContent = ''; } els.totalBF.textContent = formatted.totalBF || '0.00 BF'; els.adjustedBF.textContent = formatted.adjustedBF || '0.00 BF'; els.estCost.textContent = formatted.estCost || '$0.00'; els.wasteNote.textContent = formatted.wasteNote || 'Includes 0% waste'; els.priceNote.textContent = formatted.priceNote || '0.00 × $0.00'; els.totalPieces.textContent = formatted.totalPieces || '0 pieces'; els.volFt3.textContent = formatted.volFt3 || '0.000 ft³'; els.volM3.textContent = formatted.volM3 || '0.000000 m³'; els.rowSummary.textContent = formatted.rowSummary || '—'; els.rows.querySelectorAll('[data-row]').forEach((rowEl, idx) => { const rowValue = formatted.rowTotals && formatted.rowTotals[idx] !== undefined ? formatted.rowTotals[idx] : 0; const rowLabel = rowEl.querySelector('[data-row-bf]'); if (rowLabel) { rowLabel.textContent = `${fmtNumber(rowValue)} BF`; } }); } function update() { const inputs = parseInputs(); const validation = validate(inputs); if (!validation.ok) { render({}, validation.errors); return; } const outputs = compute(inputs); const formatted = format(outputs, inputs); render(formatted, []); } function addExampleRow() { const row = createRow(); if (unitSystem === 'imperial') { setRowValues(row, { thickness: 2, width: 4, length: 8, quantity: 1 }); } else { setRowValues(row, { thickness: 50.8, width: 101.6, length: 2.4384, quantity: 1 }); } update(); } function resetForm() { els.rows.innerHTML = ''; rowCounter = 0; unitSystem = 'imperial'; document.getElementById('units-imperial').checked = true; document.getElementById('units-metric').checked = false; els.wasteInput.value = '10'; els.priceInput.value = '0'; els.currencyInput.value = '; els.currencyBadge.textContent = '; const row = createRow(); setRowValues(row, { quantity: 1 }); updateUnits(); update(); } els.addRowBtn.addEventListener('click', () => { createRow(); update(); }); els.addCommonBtn.addEventListener('click', addExampleRow); els.calcBtn.addEventListener('click', update); els.resetBtn.addEventListener('click', resetForm); els.wasteInput.addEventListener('input', debouncedUpdate); els.priceInput.addEventListener('input', debouncedUpdate); els.currencyInput.addEventListener('input', () => { els.currencyBadge.textContent = els.currencyInput.value || '; debouncedUpdate(); }); document.querySelectorAll('input[name="units"]').forEach(radio => { radio.addEventListener('change', (event) => { if (event.target.checked) { unitSystem = event.target.value; updateUnits(); update(); } }); }); // Initialize with a single row and run first update. const first = createRow(); setRowValues(first, { quantity: 1 }); updateUnits(); update(); })();