# Nutrition Facts Label Generator

> A free, open-source web app that turns nutrient values into FDA-compliant Nutrition Facts labels (21 CFR 101.9, and USDA-FSIS meat and poultry labels), exported as print-ready vector PDF and SVG, or a 600 dpi PNG. It applies the rounding, %Daily Value and format rules for you, cites the regulation for every rule, checks that the label fits a package, and runs entirely in the browser: nothing the user enters is uploaded.

This file tells AI agents and assistants how to help someone make a label here: what to ask them, how to operate the page, how to download, how donations work, and how to iterate on size and format. It is published at `/llms.txt` and `/agents.llm`.

The tool applies the regulations as written, but it is **not legal advice**. The user is responsible for their nutrient data and for their label's compliance, and FSIS-regulated labels may need FSIS approval. Say so when you hand over a finished label.

## 1. Before you start: questions to ask the user

Do not guess any of these. Ask, in plain language, and explain why each one matters. Group them; most users know their product but not the regulation.

**The product and who regulates it**
- Product name (used only for file names; in a variety pack, each food's name is printed).
- Is it regulated by FDA (most foods) or USDA-FSIS (products with meat or poultry, typically more than 3% raw or 2% cooked)? For FSIS: FDA's current format (usual today) or the legacy format (only if their approved label sketch uses it)?
- Who is the food for: adults and children 4+ (default), children 1-3, infants through 12 months, or pregnant and lactating women? This changes the Daily Values.
- Must the label also be in Spanish? (If the package uses Spanish anywhere, required information must be in both languages, 101.15(c)(2); one bilingual label is allowed, 101.9(d)(14).)

**The package** (this decides the format and size)
- Shape: box/carton, can/jar/tube, or pouch. Dimensions in inches or millimetres.
- For a box: does it stand with the artwork on an upright side (like a cereal box), or lie flat with the artwork on top (like a tinned-fish carton)?
- Which faces carry printing? (101.9 does not define "total surface area available to bear labeling"; the tool leaves out tops and bottoms by default, as 21 CFR 101.1 does. If counting a face changes the answer, ask the user how their reviewer counts it.)
- How close to the edges can they print? (Default clear margin 1/8 in; many small cartons print within 1/16 in.)
- Should the principal display panel stay free for artwork? (Default yes.)
- Does the package make any nutrition claim (e.g. "high protein", "low sodium")? Claims make some optional lines required and remove the under-12 sq in exemption.

**Columns**
- One food or several different foods in one package (variety pack: aggregate display)?
- Does the package hold 2-3 servings that could be eaten at once? (Then a per-container column is required, 101.9(b)(12)(i).) Is a serving part of a unit, like half a muffin (per-unit column)? Do they want an "as prepared" column? Is it labeled for two age groups (required for foods for both infants and children 1-3)?

**Serving**
- Serving size as a household measure (cup, tbsp, piece, "1 can drained") and in grams or mL. It must come from the food's reference amount (RACC) in 21 CFR 101.12; if they don't know it, tell them to look it up there.
- Servings per container (the actual number; the tool rounds and adds "about").

**Nutrient values**
- Unrounded values from a lab report or database, per serving or per 100 g (then also the serving weight in grams). Do not round them yourself; the tool applies 101.9(c).
- Required: calories, total fat, saturated fat, trans fat, cholesterol, sodium, total carbohydrate, dietary fiber, total sugars, added sugars, protein, vitamin D, calcium, iron, potassium.
- Optional unless claimed or added: polyunsaturated and monounsaturated fat, fluoride, soluble and insoluble fiber, sugar alcohol, other vitamins and minerals (required when added to the food or claimed), folic acid (required as mcg when added).
- A protein %DV needs the protein's PDCAAS (quality score, 0-1); it is required for foods for children under 4 and for protein claims.

**Look**
- Ink and background colour, or transparent (prints on the package); the package colour if transparent. The rule: all black or one colour on white or another neutral contrasting background, 101.9(d)(1)(i). The tool checks contrast.

## 2. Operating the page

The page is a single form in six numbered steps, with a live preview beside it. Everything updates as fields change.

### Option A: fill in the form

Every setting is an element with a `data-path` attribute naming the field it sets (see section 3). Set its value and dispatch an `input` event (bubbling), for example:

```js
const set = (path, value) => {
  const el = document.querySelector(`[data-path="${path}"]`);
  el.value = value;
  el.dispatchEvent(new Event('input', { bubbles: true }));
};
set('servingSizeHousehold', '1 can drained');
set('values.sodium', '270');
```

Checkboxes: set `.checked` and dispatch `input`. Radio questions in step 2 have `name` `q-agency`, `q-contents` (`one`, `variety`), `q-space` (`large`, `small`, `tiny`), `q-shape` (`tall`, `wide`, `line`) and `q-columns` (`one`, `perContainer`, `perUnit`, `secondForm`, `ageGroups`); check one and dispatch `input`. Length fields (label width, package dimensions, margin, space) are in the units chosen in `units` (`in` or `mm`).

### Option B: load a complete label at once (most reliable for scripts)

The page keeps its whole state in `localStorage` under the key `nutrition-label-generator:v1`, and fills in any missing fields with defaults on load. Write a label object and reload:

```js
localStorage.setItem('nutrition-label-generator:v1', JSON.stringify(label));
location.reload();
```

Alternatively, give the user a `.json` file and ask them to use **Open JSON** (bottom of the form). **Save as JSON** downloads the current state, which is the easiest way to keep versions while iterating.

Example label (lengths in inches; nutrient values unrounded, per serving):

```json
{
  "productName": "Smoked Trout",
  "regime": "fda",
  "audience": "general",
  "format": "linear",
  "smallPackage": true,
  "tinyPackage": false,
  "simplified": false,
  "servingSizeHousehold": "1 can drained",
  "servingSizeMetric": "67g",
  "servingsPerContainer": 1,
  "showServings": true,
  "servingsAbout": "auto",
  "basis": "perServing",
  "servingWeightG": 67,
  "calories": 190,
  "values": {
    "totalFat": 13, "satFat": 2, "transFat": 0, "cholesterol": 20, "sodium": 270,
    "totalCarb": 0, "fiber": 0, "totalSugars": 0, "addedSugars": 0, "protein": 17,
    "vitaminD": 2, "calcium": 0, "iron": 0.36, "potassium": 94
  },
  "package": { "shape": "box", "lying": true, "w": 4.33, "d": 2.72, "h": 1.3, "margin": 0.0625 },
  "units": "mm",
  "options": { "widthIn": 3.6, "microsLayout": "stacked", "footnote": "full" }
}
```

### Reading the results

- **Preview:** `#stage` holds the label as SVG; `#meta` gives its size (e.g. "90.2 x 29.3 mm · 256 x 83 pt").
- **Checks:** `#issues li`, each with class `error`, `warning` or `info`, a message and a 21 CFR citation. Resolve every error before download; read warnings to the user.
- **Size (step 5):** `#sizeNow` (current size and area), `#widthRange` (narrowest width these settings allow), `#sizeHint` (whether height follows width, the smallest area), `#fitResult` (whether it fits the space given in `space.w`/`space.h`), `#altTable` (other layouts the regulation allows, each with its smallest size, condition and citation).
- Some results sit inside collapsible sections (`<details>`); read them with `textContent`, which works whether or not the section is open, rather than `innerText`.
- **Package (step 2, "Know the package's dimensions?"):** `#pkgResult` (labelable area, size class, every format rated `fits`, `allowed, doesn't fit` or `not allowed`, with the reason, the panel it goes on, or how close it came). Buttons `[data-pkgopt]` apply a format at the size that fits.
- **On the package:** the `#viewTabs` button `data-view="package"` shows the package drawn flat (`#net`) with the label on it; `#netNote` says whether it fits its panel. `[data-path="package.underlay"]` and `[data-path="package.overlay"]` show or hide the package and the label; `#turnLabel` turns the label 90°; `#resetPlace` puts it back where the tool would place it.

## 3. Fields (data-path)

- Product: `productName`, `regime` (`fda`, `usdaFdaFormat`, `usdaLegacy`), `audience` (`general`, `children1to3`, `infants`, `pregnantLactating`), `spanish`, `servingSizeHouseholdEs`.
- Format: `format` (`standard`, `dual`, `tabular`, `tabularDual`, `linear`, `aggregate`), `smallPackage`, `tinyPackage`, `simplified`.
- Dual columns: `dual.mode` (`perContainer`, `perUnit`, `secondForm`, `ageGroups`), `dual.head1`, `dual.head2`, `dual.unitScale`, `dual.calories2`, `dual.values2.<nutrient>`, and for age groups `dual.audience2`, `dual.servingSize2Household`, `dual.servingSize2Metric`, `dual.servingsPerContainer2`, `dual.folicAcid2`.
- Variety pack: `productCount` (2-4), `products.<0-2>.name` (a line break chooses where it wraps), `products.<i>.servingSizeMetric`, `products.<i>.calories`, `products.<i>.values.<nutrient>`.
- Serving: `servingSizeHousehold`, `servingSizeMetric`, `servingsPerContainer`, `servingsAbout`, `showServings`.
- Nutrients: `basis`, `servingWeightG`, `calories`, `values.<nutrient>`, `folicAcid`. Nutrient keys: `totalFat`, `satFat`, `transFat`, `polyFat`, `monoFat`, `cholesterol`, `sodium`, `fluoride`, `totalCarb`, `fiber`, `solubleFiber`, `insolubleFiber`, `totalSugars`, `addedSugars`, `sugarAlcohol`, `protein`, `vitaminD`, `calcium`, `iron`, `potassium`, `vitaminA`, `vitaminC`, `vitaminE`, `vitaminK`, `thiamin`, `riboflavin`, `niacin`, `vitaminB6`, `folate`, `vitaminB12`, `biotin`, `pantothenicAcid`, `phosphorus`, `iodine`, `magnesium`, `zinc`, `selenium`, `copper`, `manganese`, `chromium`, `molybdenum`, `chloride`, `choline`. USDA legacy vitamins in IU: `legacyValues.vitaminA_IU`, `legacyValues.vitaminD_IU`, `legacyValues.vitaminE_IU`.
- Look and size: `options.ink`, `options.background`, `options.transparent`, `options.packageColor`, `options.widthIn` (layout width), `options.scale` (enlarge proportionally; 1 = 100%, never below 1), `options.microsLayout` (`stacked`, `sideBySide`), `units`, `space.w`, `space.h`.
- Package: `package.shape` (`box`, `cylinder`, `pouch`), `package.lying`, `package.w`, `package.h`, `package.d`, `package.diameter`, `package.margin`, `package.usePdp`, `package.rotate`, per-face counting via the checkboxes `[data-face]`.
- Fine print: `options.lessThan`, `options.pctFromActual`, `options.microUnder2Zero`, `options.omitInsignificant`, `options.showProteinPct`, `options.pdcaas`, `options.vitaminDIU`, `options.kilojoules`, `options.caloriesFromSatFat`, `options.caloriesPerGram`, `options.footnote` (`full`, `short`, `none`), `options.footnoteDV`.

Every option has a "?" tooltip (`.tip`, text in its `data-tip`) with guidance and the regulation it comes from. Read them when the user is unsure.

## 4. Downloading

- Buttons: `#exportPdf` (vector PDF, the usual choice for print), `#exportSvg` (vector SVG for design software such as Illustrator: bold text is real Helvetica Black, so the designer needs that font installed for exact widths), `#exportPng` (600 dpi image; keeps transparency), and `#exportSvgWeb` (SVG for web pages, which imitates Helvetica Black so it looks right without the font; design software imports its bold words as two layers, so don't send this one to a designer).
- **Ask the user before downloading** a file, and tell them the format and file name (`<product>-<format>.pdf`).
- If the label has errors, the page asks for confirmation first. Don't bypass it; fix the errors or explain them.
- Standard fonts are not embedded. If their printer needs outlines, they can open the PDF or SVG in Illustrator or Inkscape and choose "Create outlines".

## 5. Donations

The tool is free. After the first download in a visit, a small card (`#thanks`) thanks the user and offers **Support this project** (`#donateLink`, which opens the project's donation page in a new tab). The footer link `#donateFooterLink` opens the same card. "Maybe later" (`#thanksLater`) closes it; "Don't show this again" (`#thanksNever`) is remembered.

For agents: mention it once, neutrally, if the user found the tool useful, and let them decide. Never click the donation link, enter payment details, or pay on the user's behalf.

## 6. Iterating and experimenting

Work in small steps and re-read `#issues`, `#sizeNow` and `#pkgResult` after each change. Useful loops:

- **"What's the smallest label I can print?"** Read `#widthRange` (the narrowest width for these settings) and `#altTable`. Type sizes are regulatory minimums and are never shrunk, so a label only gets smaller by changing the format or options, each of which the table lists with the condition that allows it: vitamins two per line, tabular (less than about 3 in of vertical space), small-package tabular (40 sq in or less, when no standard or tabular label fits any panel), linear (only when no tabular fits), 6 pt linear (under 12 sq in), leaving out insignificant nutrients, or the short footnote (calorie-free foods only).
- **"Will it fit my package?"** Enter the package in step 2. Each format is rated with the panel it fits on. If nothing fits, `#pkgResult` gives the closest panel and by how much, and whether a smaller margin would do it. Try: a 1/16 in margin, lying flat versus standing, which faces count, allowing the principal display panel, or different package dimensions. Changing what counts can move the package across 12 or 40 sq in; the page warns when it does.
- **"Fit this space."** Set `space.w` and `space.h`; `#fitResult` gives a width that fits and how far it could be enlarged (`options.scale`) to fill the space.
- **On the package:** switch to the package view, drag the label, drag its corner (width; Shift for proportional enlargement), turn it 90°, and hide the package or label to compare. Going past a panel's edge is allowed for experimenting and is flagged in red.
- **Formats side by side:** save a JSON version before each experiment (Save as JSON) so the user can go back.
- **Colour:** try the presets in step 5; `#contrast` shows the contrast ratio (FDA gives no number; the tool warns under 7:1 and errors under 4.5:1, the WCAG levels for small text).

## 7. Things to tell the user

- Values must be unrounded; the tool rounds.
- Check the serving size against 21 CFR 101.12.
- The labelable-area question (which faces count) is a judgement the regulation leaves open; for packages near 12 or 40 sq in, confirm it with their reviewer.
- Where FDA's own sample labels differ from the regulation, the tool follows the regulation.
- Nothing is uploaded; the label is made in their browser.

## Source

- Regulations: 21 CFR 101.9 (FDA), 9 CFR 317.309 and 381.409 (USDA-FSIS), 21 CFR 101.1, 101.2, 101.12, 101.15.
- Code and tests: the project's GitHub repository (MIT licence).
