# Shape

> A drawn rectangle with the document's own words on top of it.

Source: https://docxcelerate.com/de/docs/nodes/shape/

Diese Seite ist noch nicht übersetzt und wird deshalb auf Englisch angezeigt.

Word draws a real rectangle and the paragraphs sit on its fill rather than beside it. What separates it from a paragraph with a background is the size: a shape is a box you decided the dimensions of and does not grow with its text, which is exactly what a banner needs — one that changed depth with its wording would be a different notice on every letter. When the box *should* grow with what is in it, a `Table` with one cell is the thing to reach for.

> **Was die Renderer heute tun:** Packed as VML — a `w:pict` holding a `v:rect` — rather than as the DrawingML shape Word writes itself. Word reads both identically; docx-preview renders VML and has no reading of DrawingML shapes at all, so the other form would draw in Word and show nothing in the preview. Rounded corners are absent for the same reason and go in when the preview can draw them.

- **Helper:** `Shape`
- **Node-Art:** `shape`
- **Kategorie:** Medien
- **Wird aufgelöst:** Both
- **Kinder:** Paragraphs, usually. Text goes straight in for the one-line case.

| Option | Typ | Was sie bewirkt |
| --- | --- | --- |
| `id` | `string` | Stable address for the node. Engines target it and build artifacts diff on it, so treat a rename as a breaking change. Optional: a node without one takes an id from where it sits, which is what keeps branches and loops from forcing you to invent names. Two nodes claiming one id is an error rather than a race. |
| `width` | `number` | How wide the box is drawn, in points. The full text column unless it is said, which is the width a banner wants and the only one that needs no arithmetic from the document. |
| `height` | `number` | How deep the box is drawn, in points. A shape does not grow to fit its words, so this is a decision the document or its theme makes — the block style's `heightPt` is taken when the node says nothing. |
| `variant` | `string` | A block style the theme looks up — `"band"`, `"badge"`, `"panel"`. Names what the node is, never what it looks like: the appearance lives in the style's `blocks`, so a document restyles without a node changing. A name the theme has not heard of draws as an ordinary block rather than failing. |
| `derivers` | `DeriverInvocation[]` | Values the engine computes before the node resolves, written to `derived.*` and readable from a template token. Built with `derive()`. These survive publishing and run per document — use them for anything computed from request data. `useDeriver` runs one during the build instead. |

## Writing one

A shape needs two things from you: how big it is, and what goes on it.

```tsx
import { Paragraph, Shape, useFormat, useState } from "docxcelerate/template";

export const RenewalBanner: Shape = () => {
  const { date } = useFormat("en-GB");
  const [state] = useState((data: MemberData) => ({
    plan: data.plan,
    renewsOn: data.renewsOn,
  }));

  return (
    <Shape id="renewal-banner" variant="banner" height={44}>
      <Paragraph id="renewal-banner-line">
        {state.plan} renews on {date(state.renewsOn)}
      </Paragraph>
    </Shape>
  );
};
```

`width` and `height` are in points. A shape with no width fills the text
column, which is what a banner wants; saying one makes a block that sits in the
column rather than spanning it. For the one-line case the text can go straight
in — `<Shape id="paid">Paid in full</Shape>` reads as one.

## Why it is not a paragraph with a background

Because a shape is a box you decided the size of, and that is usually the whole
reason you wanted one.

A banner that grew with its wording would be a different depth on every
document it went out on. The one saying "Paid" and the one saying "Overdue —
14 days past the due date" would be two different blocks, and a reader flicking
through a stack reads the shorter one as a different kind of notice. A shape
holds its dimensions whatever is written on it.

The trade runs the other way too, and it is worth knowing which side you are
on. When the box *should* grow with what is in it — a description, a quoted
clause, anything whose length you do not control — a [`Table`](/docs/nodes/table/)
with one cell in it is the thing to reach for. It draws the same fill, rule and
padding, and it grows. Neither is a worse version of the other.

## What it looks like is the theme's

`variant` names what the banner **is** — `banner`, `stamp`, `bannerOverdue` —
and never what colour it should be. The fill, the rule, the room inside and how
the words sit on it all come from the style's `blocks`, so restyling a document
restyles its shapes without a node changing. A variant the theme has not heard
of draws in the palette's accent, which is a banner without an opinion rather
than a broken one.

The block properties a shape reads are the ones a table cell reads: `fill`,
`border`, `borderWidthPt`, `paddingPt`, `align`, and `heightPt` — which a shape
takes as its depth when the node has not given one, because a block declaring a
height is already saying exactly that.

## How it is packed, and why it matters

A shape is written as VML — a `w:pict` holding a `v:rect` — rather than as the
DrawingML shape Word writes itself.

Word reads both identically: it opens either as a real `Rectangle` with the
width, height, fill and text the file gives it. The difference is on the other
side. `docx-preview` renders VML and has no reading of DrawingML shapes at all,
so the modern form would draw correctly in Word and show **nothing** in your
preview. Packing the form both engines read is what keeps the preview showing
the document rather than an opinion about it.

The same reasoning is why rounded corners are not here yet. `v:roundrect` is
not something the preview can draw, and a radius that draws in Word and vanishes
on screen is worse than no radius at all. It goes in when both engines can.

### A banner across the column

A shape with no width, filling the text column at a stated depth.

Source: `src/nodes/shape/basic.node.tsx`

```tsx
import { Paragraph, type Shape as ShapeComponent, Shape, useFormat, useState } from "docxcelerate/template";
import type { SampleData } from "../sample-data.ts";

/**
 * A drawn rectangle with the document's own words on it.
 *
 * The height is the point. A banner that grew with its wording would be a
 * different depth on every letter it went out on, and a reader flicking
 * through a stack would read the shorter one as a different kind of notice.
 */
export const RenewalBanner: ShapeComponent = () => {
  const { date } = useFormat("en-GB");
  const [state] = useState((data: SampleData) => ({
    plan: data.plan,
    renewsOn: data.renewsOn,
  }));

  return (
    <Shape id="renewal-banner" variant="bannerAttention" height={44}>
      <Paragraph id="renewal-banner-line">
        {state.plan} renews on {date(state.renewsOn)}
      </Paragraph>
    </Shape>
  );
};
```

**Wozu es aufgelöst wird**

```json
{
  "id": "renewal-banner",
  "kind": "shape",
  "height": 44,
  "children": [
    {
      "id": "renewal-banner-line",
      "kind": "paragraph",
      "mode": "static",
      "text": "Peak Anytime renews on 1 October 2026"
    }
  ],
  "variant": "bannerAttention"
}
```

### A block of its own size

The same element given a width, so it sits in the column rather than spanning it.

Source: `src/nodes/shape/sized.node.tsx`

```tsx
import { Paragraph, type Shape as ShapeComponent, Shape } from "docxcelerate/template";

/**
 * The same element given a width as well as a height.
 *
 * A shape with no width fills the text column, which is what a banner wants.
 * Saying one makes a block that sits in the column rather than spanning it —
 * a stamp, a callout, a badge beside prose. The tone is the theme's, named
 * for the state it reports rather than the colour it happens to be.
 */
export const PaidStamp: ShapeComponent = () => (
  <Shape id="paid-stamp" variant="bannerPositive" width={160} height={56}>
    <Paragraph id="paid-stamp-line">Paid in full</Paragraph>
  </Shape>
);
```

**Wozu es aufgelöst wird**

```json
{
  "id": "paid-stamp",
  "kind": "shape",
  "width": 160,
  "height": 56,
  "children": [
    {
      "id": "paid-stamp-line",
      "kind": "paragraph",
      "mode": "static",
      "text": "Paid in full"
    }
  ],
  "variant": "bannerPositive"
}
```
