---
title: "Headless shape annotations | Nutrient Web SDK"
canonical_url: "https://www.nutrient.io/guides/web/headless/shape/"
md_url: "https://www.nutrient.io/guides/web/headless/shape.md"
last_updated: "2026-05-15T19:10:05.084Z"
description: "Compose rectangle, ellipse, line, polyline, and polygon annotations programmatically with Nutrient Web SDK, no shape toolbar required."
---

# Headless shape annotations

Nutrient Web SDK ships five shape annotation classes, `RectangleAnnotation`, `EllipseAnnotation`, `LineAnnotation`, `PolylineAnnotation`, and `PolygonAnnotation`. They all share the same color, stroke, and preset surface, so a single shape palette in your host app can drive every shape type without per-class branching.

Looking for the visual side of this? See the `tools.contextual` [slot](https://www.nutrient.io/guides/web/user-interface/ui-customization/supported-slots.md#tools) for replacing the inline shape toolbar that appears when shape mode is active. This page covers the programmatic surface you’ll call from inside that custom UI.

## When to use this

Reach for the headless shape API when you’re building:

- A custom shape palette in your host-app sidebar, separate from the default toolbar.

- A click-to-place workflow where the user picks a shape and then clicks the canvas to drop it at a coordinate.

- A bulk-creation script that lays out shapes from a coordinate list — for example, importing form field bounds from a server.

- A measurement or markup overlay built on the geometric primitives.

## Shape types and their geometry

Each shape annotation class has its own geometry shape. The differences are subtle, and picking the right one matters because it determines what’s serialized into the PDF.

| Class                 | Geometry                                              | When to use                                                                  |
| --------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------- |
| `RectangleAnnotation` | `boundingBox: Rect`                                   | Axis-aligned rectangles. Optional cloudy border via `cloudyBorderIntensity`. |
| `EllipseAnnotation`   | `boundingBox: Rect`                                   | Axis-aligned ellipses. Same cloudy-border options as rectangles.             |
| `LineAnnotation`      | `startPoint: Point`, `endPoint: Point`, optional caps | A single line segment with optional arrowheads on either end.                |
| `PolylineAnnotation`  | `points: List<Point>`                                 | Open polyline through multiple points. Same caps options as lines.           |
| `PolygonAnnotation`   | `points: List<Point>`                                 | Closed polygon. Point order matters, clockwise winds the fill correctly.     |

The geometry primitives `Point` and `Rect` live under [`NutrientViewer.Geometry`](https://www.nutrient.io/api/web/modules/NutrientViewer.Geometry.html).

## Example: Placing a rectangle from a host-app button

Wire a custom toolbar button to drop a rectangle on the current page. The button uses the current annotation preset for stroke color and width, so it stays in sync with whatever the user picked in your color palette:

```js

const instance = await NutrientViewer.load({
  container: "#viewer",

  document: "contract.pdf"
});

const { RectangleAnnotation } = NutrientViewer.Annotations;
const { Rect } = NutrientViewer.Geometry;

document.getElementById("add-rectangle").onclick = async () => {
  const rectangle = new RectangleAnnotation({
    pageIndex: instance.viewState.currentPageIndex,
    boundingBox: new Rect({ left: 100, top: 100, width: 200, height: 100 }),
    strokeColor: NutrientViewer.Color.fromHex("#1976D2"),

    strokeWidth: 2
  });

  await instance.create(rectangle);
};

```

## Example: Programmatic polygon construction

Build a polygon from a coordinate list, useful for importing precomputed regions from a server or for building shape overlays from analytical data:

```js

const { PolygonAnnotation } = NutrientViewer.Annotations;
const { Point } = NutrientViewer.Geometry;
const { List } = NutrientViewer.Immutable;

async function createRegion(pageIndex, coordinates) {
  const polygon = new PolygonAnnotation({
    pageIndex,
    points: List(coordinates.map(([x, y]) => new Point({ x, y }))),
    strokeColor: NutrientViewer.Color.fromHex("#FF9800"),

    fillColor: NutrientViewer.Color.fromHex("#FFE0B2"),

    strokeWidth: 1.5
  });

  await instance.create(polygon);
}

await createRegion(0, [
  [50, 50],
  [200, 50],
  [200, 200],
  [50, 200]
]);

```

Polygon point order matters. List points clockwise so the fill renders inside the shape rather than outside.

## Example: A line with arrowheads

Lines and polylines share the same cap options. Use the cap enums on `NutrientViewer.LineCap` to add arrowheads to either end. The `lineCaps` object has `start` and `end` fields, and both are optional, so omit either side to leave it uncapped:

```js

const { LineAnnotation } = NutrientViewer.Annotations;
const { Point } = NutrientViewer.Geometry;

const arrow = new LineAnnotation({
  pageIndex: 0,
  startPoint: new Point({ x: 100, y: 100 }),
  endPoint: new Point({ x: 300, y: 200 }),
  strokeColor: NutrientViewer.Color.fromHex("#D32F2F"),

  strokeWidth: 2,
  lineCaps: {
    // Omit `start` to leave that end uncapped.
    end: NutrientViewer.LineCap.openArrow
  }
});

await instance.create(arrow);

```

Available cap values on `NutrientViewer.LineCap` include `square`, `circle`, `diamond`, `openArrow`, `closedArrow`, `butt`, `rOpenArrow`, `rClosedArrow`, and `slash` — all camelCase.

## Setting presets for shape modes

Set the active preset for a shape mode before the user enters drawing mode. That way, the next shape they draw uses your custom defaults. Use the callback form of `setAnnotationPresets` so you merge into the existing preset map instead of replacing every other preset:

```js

instance.setAnnotationPresets((presets) => ({...presets,
  rectangle: {...presets.rectangle,
    strokeColor: NutrientViewer.Color.fromHex("#1976D2"),

    strokeWidth: 2
  },
  ellipse: {...presets.ellipse,
    strokeColor: NutrientViewer.Color.fromHex("#388E3C"),

    fillColor: NutrientViewer.Color.fromHex("#C8E6C9"),

    strokeWidth: 2
  }
}));

```

Passing a plain object (`instance.setAnnotationPresets({ rectangle:..., ellipse:... })`) replaces the entire preset map, dropping every other preset the SDK ships with. Always prefer the callback form when you want to update individual presets.

To switch between shape modes from a custom palette, set the interaction mode and the current preset together:

```js

function selectShapeMode(name) {
  instance.setCurrentAnnotationPreset(name);
  instance.setViewState((v) =>
    v.set("interactionMode", NutrientViewer.InteractionMode.SHAPE_RECTANGLE)
  );
}

```

## Related

- [Color presets](https://www.nutrient.io/guides/web/headless/color-presets.md) — Read the same swatches the default toolbar shows.

- [Geometry primitives](https://www.nutrient.io/api/web/modules/NutrientViewer.Geometry.html) — The `Point` and `Rect` classes used by every shape.

- [Create annotations](https://www.nutrient.io/guides/web/annotations/create-edit-and-remove/create.md) — The cross-cutting create-edit-remove guide.
---

## Related pages

- [Headless callout annotations](/guides/web/headless/callout.md)
- [Headless color presets](/guides/web/headless/color-presets.md)
- [Headless image annotations](/guides/web/headless/image.md)
- [Annotation clipboard](/guides/web/headless/clipboard.md)
- [Headless](/guides/web/headless.md)
- [Headless ink annotations](/guides/web/headless/ink.md)
- [Headless stamp annotations](/guides/web/headless/stamp.md)
- [Headless link annotations](/guides/web/headless/link.md)
- [Headless note annotations](/guides/web/headless/note.md)
- [Redactions from text selection](/guides/web/headless/redactions-from-selection.md)
- [Programmatic notes panel](/guides/web/headless/notes-panel.md)
- [Headless text annotations](/guides/web/headless/text-annotations.md)
- [Headless text markup](/guides/web/headless/text-markup.md)

