# No Synthetic Weights or Italics

**MUST** · **ID:** `content-no-synthetic-weights` · **Category:** content
**Source:** [jakubkrehel](https://github.com/jakubkrehel/skills)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/content-no-synthetic-weights

> MUST: Set `font-synthesis: none` on the root so a weight or style you never loaded fails visibly instead of being faked. The browser draws a missing bold as the 400 outlines doubled a hair apart and a missing italic as a `skewX` on the roman — neither looks broken enough to file a ticket, so the missing files ship. The fix is always the same: load the file you asked for.

Set font-synthesis: none so a missing bold or italic file fails visibly instead of being faked

Synthesis is the browser covering for you, and it covers so competently that the bug never surfaces. Ask for a bold the `@font-face` block never loaded and you do not get an error — you get the 400 outlines drawn twice, a hair apart, so the stems thicken and the counters clog. Ask for an italic and you get a `skewX` on the roman: a mechanical slant, not an italic, with no redrawn single-storey `a` and no descending `f`. Neither looks broken enough to file a ticket, so the missing files ship and every heading in the product is quietly mush. `font-synthesis: none` on the root removes the safety net: the `<strong>` renders at plain 400, which is unmistakably wrong, which is the point — it fails in review instead of in production. The fix is then always the same, load the file you actually asked for.

## Rule snippet

```tsx
html { font-synthesis: none; }
```

## Bad — do not do this

`content-no-synthetic-weights-bad`

```tsx
/**
 * Bad: only weight 400 of the family is loaded, and `font-synthesis` is left at
 * its default. The `<strong>` and the `<em>` still "work" — the browser fakes
 * them, smearing the 400 glyphs sideways for bold and shearing them for italic.
 *
 * The two panels below are rendered with CSS that reproduces what the browser
 * does when it synthesizes: a double-strike for the fake bold, a mechanical
 * `skewX` for the fake oblique. Compare the letterforms, not the slant: a real
 * italic redraws `a` and `f`, a synthesized one just leans the roman.
 */

const FACE_CSS = `@font-face {
  font-family: "AcmeSans";
  src: url("/fonts/acme-sans-400.woff2") format("woff2");
  font-weight: 400;          /* 700 and italic were never loaded */
}
/* font-synthesis left at its default: weight style small-caps */`;

export function NoSyntheticWeightsBad() {
  return (
    <div className="w-full space-y-4">
      <pre className="overflow-x-auto rounded-lg border border-border bg-muted p-3 text-xs text-muted-foreground">
        <code>{FACE_CSS}</code>
      </pre>

      <p className="text-sm text-foreground">
        The markup asks for a <code>&lt;strong&gt;</code> and an <code>&lt;em&gt;</code>. Neither
        file exists, so the browser manufactures them instead of failing:
      </p>

      <div className="grid gap-3 sm:grid-cols-2">
        <div className="rounded-lg border border-destructive/40 bg-card p-3">
          <p className="mb-2 text-xs font-medium text-destructive">Synthesized bold</p>
          <p
            className="text-4xl text-foreground"
            style={{ fontWeight: 400, textShadow: '0.03em 0 currentColor, -0.03em 0 currentColor' }}
          >
            Bold
          </p>
          <p className="mt-2 text-xs text-muted-foreground">
            A blurry double-strike: the same 400 outline drawn twice, a hair apart. Stems thicken,
            counters clog, and the shapes never sharpen at any size.
          </p>
        </div>

        <div className="rounded-lg border border-destructive/40 bg-card p-3">
          <p className="mb-2 text-xs font-medium text-destructive">Synthesized italic</p>
          <p
            className="text-4xl text-foreground"
            style={{ fontStyle: 'normal', transform: 'skewX(-14deg)', transformOrigin: 'left' }}
          >
            after
          </p>
          <p className="mt-2 text-xs text-muted-foreground">
            A mechanical slant, not an italic. Look at the <code>a</code> and the <code>f</code>:
            a true italic redraws them (single-storey <code>a</code>, descending <code>f</code>).
            The oblique just leans the roman letter over.
          </p>
        </div>
      </div>

      <p className="text-xs text-destructive">
        Nothing looks broken enough to file a bug, so the missing files ship. Every heading and
        every emphasis in the product is mush, and no one can say when it started.
      </p>
    </div>
  );
}
```

## Good — do this

`content-no-synthetic-weights-good`

```tsx
/**
 * Good: `font-synthesis: none` on the root. The browser stops covering for the
 * missing files, so a `<strong>` with no 700 face loaded renders as plain 400 —
 * visibly wrong, caught in dev, fixed before it ships.
 *
 * The left panel shows the honest failure; the right shows the fix (load the
 * face). Either way you now know which one you are looking at.
 */

const ROOT_CSS = `:root {
  font-synthesis: none;      /* no faked bold, no faked oblique */
}

@font-face { font-family: "AcmeSans"; src: url("/fonts/acme-sans-400.woff2") format("woff2"); font-weight: 400; }
@font-face { font-family: "AcmeSans"; src: url("/fonts/acme-sans-700.woff2") format("woff2"); font-weight: 700; }
@font-face { font-family: "AcmeSans"; src: url("/fonts/acme-sans-400-italic.woff2") format("woff2"); font-weight: 400; font-style: italic; }`;

export function NoSyntheticWeightsGood() {
  return (
    <div className="w-full space-y-4">
      <pre className="overflow-x-auto rounded-lg border border-border bg-muted p-3 text-xs text-muted-foreground">
        <code>{ROOT_CSS}</code>
      </pre>

      <div className="grid gap-3 sm:grid-cols-2">
        <div className="rounded-lg border border-border bg-card p-3">
          <p className="mb-2 text-xs font-medium text-muted-foreground">
            Face missing, synthesis off
          </p>
          <p className="text-4xl font-normal text-foreground">Bold</p>
          <p className="mt-2 text-xs text-muted-foreground">
            The <code>&lt;strong&gt;</code> renders at 400. It is unmistakably not bold, so the
            missing <code>700</code> file is a bug you can see in review instead of a texture you
            slowly stop noticing.
          </p>
        </div>

        <div className="rounded-lg border border-border bg-card p-3">
          <p className="mb-2 text-xs font-medium text-success">Face loaded</p>
          <p className="text-4xl font-bold text-foreground">Bold</p>
          <p className="mt-2 text-xs text-muted-foreground">
            Real 700 outlines: even stems, open counters, crisp at every size. This is the shape the
            fake was pretending to be.
          </p>
        </div>
      </div>

      <p className="text-xs text-success">
        <code>font-synthesis: none</code> does not make type better on its own — it makes a missing
        file fail loudly. A build that renders plain 400 where it promised bold gets fixed; a build
        that renders a smear gets shipped.
      </p>
    </div>
  );
}
```

## References

- [jakubkrehel — better-typography SKILL.md](https://raw.githubusercontent.com/jakubkrehel/skills/main/skills/better-typography/SKILL.md)
- [jakubkrehel — variable-fonts-and-opentype.md](https://raw.githubusercontent.com/jakubkrehel/skills/main/skills/better-typography/variable-fonts-and-opentype.md)
- [MDN — font-synthesis](https://developer.mozilla.org/en-US/docs/Web/CSS/font-synthesis)
