Packing water for a camping trip is a small estimation problem that most people solve by guessing. This post walks through a tiny planner that turns the guess into arithmetic you can see and change: no framework, no build step, about 60 lines of JavaScript.

The model

Water use on a car-camping trip splits into four line items, and each one scales differently:

  • Drinking: people × days × weather
  • Cooking: people × days × hot meals
  • Dishes: days × hot meals (the whole group shares the bins)
  • Hand washing: people × days

That difference is the whole point of the tool. Drinking water grows with head count, but dish water mostly grows with the number of meals you cook. For a short trip with a small group, dishes can be the largest line, which is not what people expect.

Fuel is simpler: burner minutes per hot meal, times meals, divided by how long one canister lasts on your stove.

Inputs are assumptions, not constants

Every coefficient is an input field with a default:

<label>Drinking water, L per person per day <input id="drink" type="number" step="0.1" value="3"> </label> 

The defaults are rules of thumb. Hard-coding them would make the tool look more authoritative than it is, so they live in a collapsible <details> block that says "edit me". Native <details> gives you the disclosure widget, keyboard support and accessibility for free.

One function, no state

There is no state object and no framework. The DOM is the state: every recalculation reads the inputs again.

const $ = id => document.getElementById(id); const num = id => Math.max(0, parseFloat($(id).value) || 0); function calc() { const people = num('people'), days = num('days'), meals = num('meals'); const weather = parseFloat($('weather').value); const margin = 1 + num('margin') / 100; const parts = [ ['Drinking', people * days * num('drink') * weather], ['Cooking', people * days * meals * num('cook')], ['Dishes', days * meals * num('dish')], ['Hand washing', people * days * num('hands')], ].map(([k, v]) => [k, v * margin]); const total = parts.reduce((s, p) => s + p[1], 0); // ...render } document.querySelectorAll('input,select') .forEach(el => el.addEventListener('input', calc)); calc(); 

Three small decisions are worth pointing out:

  • num() never returns NaN. An empty field while the user is typing becomes 0 instead of poisoning every result with NaN.
  • input, not change. Results update on every keystroke and on every click of the number spinner.
  • The line items are data. parts is an array of [label, liters] pairs, and the table and the bar chart are both rendered from it. Adding "coffee" or "dog" is one more row.

A bar chart without a chart library

The breakdown bar is a flex container with one child per line item, each sized by its share:

$('bar').innerHTML = parts.map(([k, v], i) => `<i title="${k}" style="width:${v / total * 100}%;background:${COLORS[i]}"></i>` ).join(''); 

For four segments that is all a stacked bar needs. The table next to it carries the exact numbers, so the chart does not have to be read precisely.

Units and the tank field

Output is in liters and US gallons, because water jugs in the US are sold by the gallon. The last tile divides the total by a tank size. It is there for camp kitchens with a built-in water tank: for example, the VOZ Camp Kitchen box has a 2-gallon tank feeding its faucet, and "how many refills is that" is a more useful answer than a raw volume.

Styling notes

  • font-variant-numeric: tabular-nums keeps the big numbers from jittering as digits change.
  • grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)) makes the form responsive without a media query.
  • Colors are custom properties with a prefers-color-scheme: dark override, so the pen follows the system theme.

What to add next

  • Save the assumptions in localStorage, so your own stove's numbers survive a reload.
  • A share link that encodes the inputs in the URL hash.
  • A print stylesheet that turns the result into a packing slip.

The pen is public: fork it and change the line items to match how you actually camp.