Skip to main content
Elements are the individual components that make up a block’s content. While blocks represent top-level content units (paragraphs, headings, etc.), elements define the internal structure and hierarchy within those blocks.

Understanding Elements

In Yoopta Editor:
  • Blocks are top-level containers identified by unique IDs
  • Elements are Slate.js nodes that define the block’s internal structure
  • Each block contains an array of elements in its value property
packages/core/editor/src/editor/types.ts
type SlateElement<K extends string = string, T = any> = {
  id: string;
  type: K;              // Element type (kebab-case)
  children: Descendant[];
  props?: PluginElementProps<T>;
};
Element types use kebab-case (“paragraph”, “heading-one”, “accordion-list-item”) while block types use PascalCase (“Paragraph”, “HeadingOne”, “Accordion”).

Element Types

Block Elements

The primary element that represents the block’s content:
{
  id: 'elem-123',
  type: 'paragraph',
  children: [{ text: 'Hello world' }],
  props: { nodeType: 'block' }
}

Inline Elements

Elements that flow within text (links, mentions):
{
  id: 'elem-456',
  type: 'link',
  children: [{ text: 'Click here' }],
  props: { 
    nodeType: 'inline',
    url: 'https://example.com',
    target: '_blank'
  }
}

Void Elements

Non-editable elements (images, dividers):
{
  id: 'elem-789',
  type: 'image',
  children: [{ text: '' }],  // Void elements require empty text child
  props: { 
    nodeType: 'void',
    src: 'https://example.com/image.jpg',
    alt: 'Description'
  }
}

Inline Void Elements

Void elements that flow within text (emoji, inline mentions):
{
  id: 'elem-999',
  type: 'emoji',
  children: [{ text: '' }],
  props: { 
    nodeType: 'inlineVoid',
    emoji: '😊'
  }
}

Element Builder API

The editor.y builder provides a fluent API for creating elements:

Creating Block Elements

packages/core/editor/src/editor/elements/create-element-structure.ts
// Simple element
const para = editor.y('paragraph');

// Element with text
const heading = editor.y('heading-one', {
  children: [editor.y.text('My Heading')]
});

// Element with custom props
const callout = editor.y('callout', {
  props: { theme: 'warning' },
  children: [editor.y.text('Important!')]
});

// Element with custom ID
const custom = editor.y('paragraph', {
  id: 'my-custom-id',
  children: [editor.y.text('Content')]
});

Creating Text Nodes

packages/core/editor/src/editor/elements/create-element-structure.ts
// Plain text
editor.y.text('Hello world')

// Text with single mark
editor.y.text('Bold text', { bold: true })
editor.y.text('Italic text', { italic: true })

// Text with multiple marks
editor.y.text('Bold and italic', { 
  bold: true, 
  italic: true 
})

// Text with all marks
editor.y.text('Formatted', {
  bold: true,
  italic: true,
  underline: true,
  code: true,
  strike: true,
  highlight: {
    color: '#000000',
    backgroundColor: '#ffff00'
  }
})
Text marks (bold, italic, etc.) are stored on text nodes, not on elements. This follows the Slate.js model for text formatting.

Creating Inline Elements

packages/core/editor/src/editor/elements/create-element-structure.ts
// Simple link
editor.y.inline('link', {
  props: { url: 'https://example.com' },
  children: [editor.y.text('Click here')]
})

// Link with formatted text
editor.y.inline('link', {
  props: { url: 'https://example.com', target: '_blank' },
  children: [
    editor.y.text('Bold ', { bold: true }),
    editor.y.text('link')
  ]
})

// Using inline elements in paragraphs
editor.y('paragraph', {
  children: [
    editor.y.text('Visit '),
    editor.y.inline('link', {
      props: { url: 'https://example.com' },
      children: [editor.y.text('our site')]
    }),
    editor.y.text(' for more info.')
  ]
})

Nested Element Structures

For complex plugins with multiple levels:
packages/core/editor/src/editor/elements/create-element-structure.ts
editor.y('accordion-list', {
  children: [
    editor.y('accordion-list-item', {
      props: { isExpanded: false },
      children: [
        editor.y('accordion-list-item-heading', {
          children: [editor.y.text('Section 1')]
        }),
        editor.y('accordion-list-item-content', {
          children: [
            editor.y('paragraph', {
              children: [editor.y.text('Content here')]
            }),
            editor.y('heading-one', {
              children: [editor.y.text('Nested heading')]
            })
          ]
        })
      ]
    }),
    editor.y('accordion-list-item', {
      props: { isExpanded: true },
      children: [
        editor.y('accordion-list-item-heading', {
          children: [editor.y.text('Section 2')]
        }),
        editor.y('accordion-list-item-content', {
          children: [editor.y.text('More content')]
        })
      ]
    })
  ]
})

Element Operations

Yoopta Editor provides comprehensive methods for working with elements:

Inserting Elements

// Insert element at specific path
editor.insertElement({
  blockId: 'block-123',
  element: editor.y('paragraph', {
    children: [editor.y.text('New paragraph')]
  }),
  at: [0, 0]  // Slate path
});

// Insert at current selection
editor.insertElement({
  blockId: currentBlockId,
  element: editor.y.inline('link', {
    props: { url: 'https://example.com' },
    children: [editor.y.text('link')]
  })
});

Updating Elements

// Update element properties
editor.updateElement({
  blockId: 'block-123',
  elementId: 'elem-456',
  props: {
    url: 'https://new-url.com',
    target: '_blank'
  }
});

// Update element children
editor.updateElement({
  blockId: 'block-123',
  elementId: 'elem-456',
  children: [
    editor.y.text('Updated text')
  ]
});

Deleting Elements

// Delete specific element
editor.deleteElement({
  blockId: 'block-123',
  elementId: 'elem-456'
});

// Delete element at path
editor.deleteElement({
  blockId: 'block-123',
  at: [0, 1]
});

Getting Elements

// Get single element by ID
const element = editor.getElement({
  blockId: 'block-123',
  elementId: 'elem-456'
});

if (element) {
  console.log('Element type:', element.type);
  console.log('Element props:', element.props);
}

// Get element at specific path
const elementAtPath = editor.getElement({
  blockId: 'block-123',
  at: [0, 0, 1]
});

// Get all elements in a block
const elements = editor.getElements({
  blockId: 'block-123'
});

// Get elements of specific type
const links = editor.getElements({
  blockId: 'block-123',
  filter: (element) => element.type === 'link'
});

Getting Element Paths

// Get element path (Slate path)
const path = editor.getElementPath({
  blockId: 'block-123',
  elementId: 'elem-456'
});

if (path) {
  console.log('Element path:', path); // e.g., [0, 1, 2]
}

// Get parent element path
const parentPath = editor.getParentElementPath({
  blockId: 'block-123',
  elementId: 'elem-456'
});

Getting Element Entries

An entry is a tuple of [element, path]:
// Get element entry
const entry = editor.getElementEntry({
  blockId: 'block-123',
  elementId: 'elem-456'
});

if (entry) {
  const [element, path] = entry;
  console.log('Element:', element);
  console.log('Path:', path);
}
Element entries are useful when you need both the element data and its location in the document tree.

Getting Element Children

// Get direct children of element
const children = editor.getElementChildren({
  blockId: 'block-123',
  elementId: 'elem-456'
});

children.forEach(child => {
  if ('text' in child) {
    console.log('Text node:', child.text);
  } else {
    console.log('Element node:', child.type);
  }
});

Checking if Element is Empty

// Check if element has no content
const isEmpty = editor.isElementEmpty({
  blockId: 'block-123',
  elementId: 'elem-456'
});

if (isEmpty) {
  console.log('Element is empty');
}

Getting Element DOM Rect

// Get element's bounding rectangle
const rect = editor.getElementRect({
  blockId: 'block-123',
  elementId: 'elem-456'
});

if (rect) {
  console.log('Position:', rect.x, rect.y);
  console.log('Size:', rect.width, rect.height);
}
Useful for positioning tooltips, popovers, or other UI elements relative to content.

Using the Elements Namespace

All element operations are also available via the Elements namespace:
import { Elements } from '@yoopta/editor';

// All operations available:
Elements.insertElement(editor, { ... });
Elements.updateElement(editor, { ... });
Elements.deleteElement(editor, { ... });
Elements.getElement(editor, { ... });
Elements.getElements(editor, { ... });
Elements.getElementEntry(editor, { ... });
Elements.getElementPath(editor, { ... });
Elements.getParentElementPath(editor, { ... });
Elements.getElementChildren(editor, { ... });
Elements.isElementEmpty(editor, { ... });

Element Props

Element props are custom properties specific to each element type:
packages/core/editor/src/plugins/types.ts
type PluginElementProps<T> = PluginDefaultProps & T;

type PluginDefaultProps = { 
  nodeType?: 'block' | 'inline' | 'void' | 'inlineVoid' 
};
Plugins define their own prop types:
// Link element props
type LinkProps = {
  nodeType: 'inline';
  url: string;
  target?: '_blank' | '_self';
  rel?: string;
};

// Image element props
type ImageProps = {
  nodeType: 'void';
  src: string;
  alt?: string;
  width?: number;
  height?: number;
};

// Callout element props
type CalloutProps = {
  nodeType: 'block';
  theme?: 'info' | 'warning' | 'error' | 'success';
};

Element Configuration in Plugins

When creating a plugin, you define element configurations:
import { createYooptaPlugin } from '@yoopta/editor';

const MyPlugin = createYooptaPlugin({
  type: 'MyPlugin',
  elements: {
    'my-root-element': {
      render: MyRootComponent,
      props: { nodeType: 'block' },
      asRoot: true,  // This is the root element
      children: ['my-child-element'],  // Allowed children
    },
    'my-child-element': {
      render: MyChildComponent,
      props: { nodeType: 'block' },
      placeholder: 'Type something...',
    },
    'my-inline-element': {
      render: MyInlineComponent,
      props: { nodeType: 'inline' },
    }
  }
});

Element Configuration Options

packages/core/editor/src/plugins/types.ts
type PluginElement<TKeys, T> = {
  render?: (props: PluginElementRenderProps) => JSX.Element;
  props?: PluginElementProps<T>;
  asRoot?: boolean;                    // Is this the root element?
  children?: TKeys[];                  // Allowed child element types
  injectElementsFromPlugins?: string[];  // Allow elements from other plugins
  rootPlugin?: string;                 // Plugin this element belongs to
  placeholder?: string;                // Placeholder text when empty
};
Use injectElementsFromPlugins to allow elements from other plugins to be nested. For example, an accordion content area can accept paragraphs, headings, images, etc.

Text Nodes

Text nodes are special leaf nodes that contain actual text content:
packages/core/editor/src/editor/types.ts
type SlateElementTextNode = {
  text: string;
  bold?: boolean;
  italic?: boolean;
  underline?: boolean;
  code?: boolean;
  strike?: boolean;
  highlight?: any;
};
Text nodes are always leaves in the element tree and cannot have children.

Working with Slate Paths

Elements are located using Slate paths - arrays of indices:
// Path examples:
[0]           // First element in block
[0, 0]        // First child of first element
[0, 1, 2]     // Third child of second child of first element

// Get element at path
const block = editor.getBlock({ id: blockId });
const slate = editor.blockEditorsMap[blockId];
const [element] = Editor.node(slate, [0, 1]);
Slate paths are relative to the block’s Slate editor instance, not the entire document. Each block maintains its own independent Slate editor.

Best Practices

1. Always Use the Builder API

// Good: Type-safe and validated
const element = editor.y('paragraph', {
  children: [editor.y.text('Hello')]
});

// Bad: Manual construction can miss required properties
const element = {
  id: 'manual-id',
  type: 'paragraph',
  children: [{ text: 'Hello' }]
};

2. Validate Element IDs

When working with element IDs, always check if the element exists:
const element = editor.getElement({ blockId, elementId });
if (!element) {
  console.error('Element not found');
  return;
}

// Now safe to use element

3. Use Element Entries for Operations

When you need both element data and location:
const entry = editor.getElementEntry({ blockId, elementId });
if (entry) {
  const [element, path] = entry;
  // Perform operations with both element and path
}

4. Batch Element Operations

editor.batchOperations(() => {
  // Multiple element operations
  editor.insertElement({ ... });
  editor.updateElement({ ... });
  editor.deleteElement({ ... });
});
// Single change event emitted

Next Steps

Block System

Understand block-level operations

Marks

Learn about text formatting

Plugin System

Create custom element types

Editor Instance

Back to editor API reference

Build docs developers (and LLMs) love