Skip to main content

Overview

Elements are the fundamental building blocks of any Excalidraw drawing. Every shape, line, text, image, and frame in Excalidraw is represented as an element object with a consistent base structure and type-specific properties.

Element Types

Excalidraw supports multiple element types, each representing a different drawing primitive:
Basic geometric shapes that form the foundation of drawings:
  • rectangle - Rectangular shapes with configurable roundness
  • diamond - Diamond/rhombus shapes
  • ellipse - Circular and elliptical shapes
type ExcalidrawRectangleElement = _ExcalidrawElementBase & {
  type: "rectangle";
};

type ExcalidrawDiamondElement = _ExcalidrawElementBase & {
  type: "diamond";
};

type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
  type: "ellipse";
};

Base Element Structure

All element types extend a common base structure defined in _ExcalidrawElementBase:
packages/element/src/types.ts
type _ExcalidrawElementBase = Readonly<{
  // Identity
  id: string;
  type: ExcalidrawElementType;
  
  // Position and dimensions
  x: number;
  y: number;
  width: number;
  height: number;
  angle: Radians;
  
  // Styling
  strokeColor: string;
  backgroundColor: string;
  fillStyle: FillStyle;
  strokeWidth: number;
  strokeStyle: StrokeStyle;
  roundness: null | { type: RoundnessType; value?: number };
  roughness: number;
  opacity: number;
  
  // Versioning and collaboration
  version: number;
  versionNonce: number;
  updated: number;  // epoch timestamp
  seed: number;
  index: FractionalIndex | null;
  
  // State
  isDeleted: boolean;
  locked: boolean;
  
  // Relationships
  groupIds: readonly GroupId[];
  frameId: string | null;
  boundElements: readonly BoundElement[] | null;
  
  // Metadata
  link: string | null;
  customData?: Record<string, any>;
}>;
  • id: Unique identifier for the element, generated using randomId()
  • seed: Random integer used to seed shape generation in rough.js, ensuring consistent rendering across sessions
  • version: Integer that increments on each change, used for collaboration reconciliation
  • versionNonce: Random integer regenerated on each change, used for deterministic reconciliation when versions are identical
  • index: Fractional index string (using fractional indexing) for ordering in multiplayer scenarios
  • updated: Epoch timestamp (ms) of last element update
  • boundElements: Array of elements bound to this element (e.g., arrows, text labels)
  • roundness: Configures corner rounding - null for no rounding, or an object with type (“adaptive” or “proportional”) and optional value

Creating Elements

Excalidraw provides factory functions for creating new elements with proper initialization:
import { newElement, newTextElement, newLinearElement } from "@excalidraw/element";

// Create a rectangle
const rectangle = newElement({
  type: "rectangle",
  x: 100,
  y: 100,
  width: 200,
  height: 150,
  strokeColor: "#000000",
  backgroundColor: "#ffffff",
  fillStyle: "hachure",
  strokeWidth: 2,
  roughness: 1,
  opacity: 100,
});

// Create a text element
const text = newTextElement({
  x: 150,
  y: 150,
  text: "Hello Excalidraw",
  fontSize: 20,
  fontFamily: 1,
  textAlign: "center",
});

// Create an arrow
const arrow = newLinearElement({
  type: "arrow",
  x: 50,
  y: 50,
  startArrowhead: null,
  endArrowhead: "arrow",
});
Element factory functions automatically initialize required properties like id, version, versionNonce, seed, and updated.

Mutating Elements

Excalidraw provides two approaches for updating elements:

1. Immutable Updates with newElementWith

Creates a new element object with updates applied:
packages/element/src/mutateElement.ts
import { newElementWith } from "@excalidraw/element";

const updatedElement = newElementWith(element, {
  x: 200,
  y: 300,
  strokeColor: "#ff0000",
});
// Returns a new element with version and versionNonce incremented

2. Mutable Updates with mutateElement

Mutates an element in place for performance:
packages/element/src/mutateElement.ts
import { mutateElement } from "@excalidraw/element";

const elementsMap = scene.getElementsMapIncludingDeleted();

mutateElement(element, elementsMap, {
  width: 300,
  height: 200,
});
// Mutates element in place, updates version, versionNonce, and updated timestamp
mutateElement does not trigger component re-renders. Use scene.mutateElement() or excalidrawAPI.mutateElement() when you need to trigger updates.

Version Bumping

Manually bump an element’s version without other changes:
import { bumpVersion } from "@excalidraw/element";

bumpVersion(element);
// Increments version, regenerates versionNonce, updates timestamp

Element Properties

Styling Properties

element.strokeColor = "#000000";      // Hex color for borders
element.backgroundColor = "#ffffff";  // Hex color for fills
element.opacity = 100;                // 0-100 percentage

Geometric Properties

// Position (top-left corner)
element.x = 100;
element.y = 200;

// Dimensions
element.width = 300;
element.height = 150;

// Rotation in radians (0 to 2π)
element.angle = 0.785398; // 45 degrees

Relationship Properties

// Grouping
element.groupIds = ["groupId1", "groupId2"]; // Ordered from deepest to shallowest

// Frame membership
element.frameId = "frameElementId" | null;

// Binding (arrows and text)
element.boundElements = [
  { id: "arrowId", type: "arrow" },
  { id: "textId", type: "text" }
];

Element State Management

Deletion

Elements are soft-deleted by setting the isDeleted flag:
element.isDeleted = true;
Use helper functions to filter deleted elements:
import { isNonDeletedElement, getNonDeletedElements } from "@excalidraw/element";

// Filter single element
if (isNonDeletedElement(element)) {
  // element is guaranteed to have isDeleted: false
}

// Filter array
const activeElements = getNonDeletedElements(allElements);

Locking

Locked elements cannot be modified by user interactions:
element.locked = true;

Ordering with Fractional Indices

Elements use fractional indices for consistent ordering in collaborative scenarios:
packages/element/src/types.ts
type FractionalIndex = string & { _brand: "fractionalIndex" };

element.index = "a0"; // Fractional index for ordering
Fractional indices are automatically synced with array order by syncMovedIndices() and syncInvalidIndices() functions. You typically don’t need to manipulate them directly.

Element Bindings

Excalidraw supports two types of element bindings:

Arrow Bindings

Arrows can bind to bindable elements (rectangles, diamonds, ellipses, text, images, frames):
packages/element/src/types.ts
type FixedPointBinding = {
  elementId: ExcalidrawBindableElement["id"];
  // Normalized position (0.0-1.0) on the bound element
  fixedPoint: [number, number];
  // Binding mode: "inside" | "orbit" | "skip"
  mode: BindMode;
};

arrow.startBinding = {
  elementId: "rect123",
  fixedPoint: [0.5, 0],  // Top center
  mode: "orbit"
};

arrow.endBinding = {
  elementId: "rect456",
  fixedPoint: [0.5, 1],  // Bottom center
  mode: "orbit"
};

Text Container Bindings

Text elements can bind to containers:
textElement.containerId = "rectId";
rectElement.boundElements = [{ id: "textId", type: "text" }];

Type Guards

Excalidraw provides comprehensive type guards for element types:
import {
  isTextElement,
  isLinearElement,
  isArrowElement,
  isFreeDrawElement,
  isImageElement,
  isFrameLikeElement,
} from "@excalidraw/element";

if (isTextElement(element)) {
  // TypeScript knows element is ExcalidrawTextElement
  console.log(element.text, element.fontSize);
}

if (isArrowElement(element)) {
  // Access arrow-specific properties
  console.log(element.startBinding, element.endBinding);
}

Element Maps

For performance, Excalidraw uses Map structures to store elements:
packages/element/src/types.ts
// Map of all scene elements (including deleted)
type SceneElementsMap = Map<
  ExcalidrawElement["id"],
  Ordered<ExcalidrawElement>
> & MakeBrand<"SceneElementsMap">;

// Map of non-deleted elements only
type NonDeletedSceneElementsMap = Map<
  ExcalidrawElement["id"],
  Ordered<NonDeletedExcalidrawElement>
> & MakeBrand<"NonDeletedSceneElementsMap">;
Element maps use branded types to ensure type safety and prevent mixing different map types.

Custom Data

Elements support custom data storage for extensions:
element.customData = {
  pluginName: "my-plugin",
  userData: { foo: "bar" },
  metadata: [1, 2, 3]
};
Keep custom data lightweight as it’s serialized and shared in collaboration sessions.

Best Practices

  • Use mutateElement for batch updates instead of creating new elements
  • Access elements through Scene maps rather than iterating arrays
  • Filter non-deleted elements once and reuse the filtered collection
  • Use type guards early to avoid runtime checks
  • Never mutate version or versionNonce manually
  • Always use mutateElement or newElementWith to ensure proper versioning
  • Respect element locked state in custom interactions
  • Use fractional indices for ordering, don’t rely on array position
  • Use specific element types rather than ExcalidrawElement when possible
  • Leverage type guards to narrow element types
  • Use branded types (FileId, FractionalIndex) for type safety
  • Prefer NonDeleted<T> types when working with active elements
  • Scene - Managing collections of elements
  • App State - Global application state management
  • Collaboration - Multi-user element synchronization