Skip to main content
AppFlowy Editor uses a combination of Nodes for document structure and Deltas for rich text content. This page explains both concepts in detail.

Delta Format

Delta is a format for representing rich text content. It’s based on Quill Delta and provides a simple, JSON-serializable way to describe text and formatting.

What is a Delta?

A Delta is a list of operations that describe text content and its formatting:
class Delta extends Iterable<TextOperation> {
  Delta({List<TextOperation>? operations});
}

Text Operations

There are three types of text operations:

TextInsert

Inserts text with optional attributes:
class TextInsert extends TextOperation {
  String text;
  Attributes? attributes;
  
  int get length => text.length;
}
Example:
{"insert": "Hello", "attributes": {"bold": true}}

TextRetain

Retains characters, optionally applying attributes:
class TextRetain extends TextOperation {
  int length;
  Attributes? attributes;
}
Example:
{"retain": 5, "attributes": {"italic": true}}

TextDelete

Deletes a specified number of characters:
class TextDelete extends TextOperation {
  int length;
}
Example:
{"delete": 3}

Working with Deltas

Creating Deltas

// From JSON
final delta = Delta.fromJson([
  {'insert': 'Hello '},
  {'insert': 'World', 'attributes': {'bold': true}},
  {'insert': '!'},
]);

// Programmatically
final delta = Delta()
  ..insert('Hello ')
  ..insert('World', attributes: {'bold': true})
  ..insert('!');

Common Delta Attributes

// Text styling
'bold': true
'italic': true
'underline': true
'strikethrough': true

// Text decoration
'code': true
'backgroundColor': '0xFF000000'
'color': '0xFF000000'

// Links
'href': 'https://example.com'

// Font attributes
'font_family': 'Roboto'

Delta Operations

Insert Text

final delta = Delta()..insert('Hello, world!');

Retain Characters

final delta = Delta()
  ..retain(5)  // Keep first 5 characters
  ..insert(' there');

Delete Characters

final delta = Delta()
  ..retain(5)   // Keep first 5 characters
  ..delete(7);  // Delete next 7 characters

Composing Deltas

Combine two deltas sequentially:
final delta1 = Delta()..insert('Hello');
final delta2 = Delta()
  ..retain(5)
  ..insert(', world!');

final composed = delta1.compose(delta2);
// Result: Delta with "Hello, world!"

Computing Differences

Find the difference between two deltas:
final oldDelta = Delta()..insert('Hello');
final newDelta = Delta()..insert('Hello, world!');

final diff = oldDelta.diff(newDelta);
// Result: Delta with operations to transform oldDelta to newDelta

Slicing Deltas

Extract a portion of a delta:
final delta = Delta()..insert('Hello, world!');

// Extract "Hello"
final slice = delta.slice(0, 5);

// Extract "world"
final worldSlice = delta.slice(7, 12);

Converting to Plain Text

final delta = Delta()
  ..insert('Hello ')
  ..insert('World', attributes: {'bold': true});

final text = delta.toPlainText();
// Returns: "Hello World"

Inverting Deltas

Create an inverse delta (for undo operations):
final base = Delta()..insert('Hello');
final change = Delta()
  ..retain(5)
  ..insert(', world!');

final inverted = change.invert(base);
// Applying inverted to the result of change will restore base

Delta in Nodes

Nodes store delta content in their attributes:
final node = Node(
  type: 'paragraph',
  attributes: {
    'delta': [
      {'insert': 'Hello '},
      {'insert': 'World', 'attributes': {'bold': true}},
    ],
  },
);

// Access the delta
final delta = node.delta;  // Returns Delta object

Getting Delta from Node

// The delta getter automatically parses the attributes
Delta? get delta {
  if (attributes['delta'] is List) {
    return Delta.fromJson(attributes['delta']);
  }
  return null;
}

Text Node Types

Different node types use deltas for text content:

Paragraph

final paragraph = Node(
  type: 'paragraph',
  attributes: {
    'delta': [
      {'insert': 'This is a paragraph.'}
    ],
  },
);

Heading

final heading = Node(
  type: 'heading',
  attributes: {
    'level': 1,
    'delta': [
      {'insert': 'Main Title'}
    ],
  },
);

Quote

final quote = Node(
  type: 'quote',
  attributes: {
    'delta': [
      {'insert': 'This is a quote.'}
    ],
  },
);

Code Block

final code = Node(
  type: 'code',
  attributes: {
    'language': 'dart',
    'delta': [
      {'insert': 'void main() {\n  print("Hello");\n}'}
    ],
  },
);

Rich Text Attributes

Text Formatting

final delta = Delta()
  ..insert('Bold', attributes: {'bold': true})
  ..insert(' ')
  ..insert('Italic', attributes: {'italic': true})
  ..insert(' ')
  ..insert('Underline', attributes: {'underline': true})
  ..insert(' ')
  ..insert('Strike', attributes: {'strikethrough': true});

Colors and Backgrounds

final delta = Delta()
  ..insert(
    'Colored text',
    attributes: {
      'color': '0xFFFF0000',  // Red text
      'backgroundColor': '0xFFFFFF00',  // Yellow background
    },
  );
final delta = Delta()
  ..insert('Visit ')
  ..insert(
    'AppFlowy',
    attributes: {'href': 'https://appflowy.io'},
  );

Code Spans

final delta = Delta()
  ..insert('Use the ')
  ..insert('print()', attributes: {'code': true})
  ..insert(' function.');

Combining Attributes

final delta = Delta()
  ..insert(
    'Bold and Italic',
    attributes: {
      'bold': true,
      'italic': true,
    },
  );

Attribute Slicing

When inserting text, you may want to inherit attributes from surrounding text:
final delta = Delta()
  ..insert('Hello', attributes: {'bold': true})
  ..insert(' world');

// Get attributes at position
final attrs = delta.sliceAttributes(5);
// Returns: {'bold': true}

Custom Slice Behavior

Configure which attributes should be sliced:
// Default behavior slices BIUS attributes
AppFlowyEditorSliceAttributes? appflowyEditorSliceAttributes = (
  delta,
  index,
) {
  // Custom slicing logic
  return delta.slice(index - 1, index).firstOrNull?.attributes;
};

Working with Unicode

Delta handles Unicode correctly, including emoji and multi-byte characters:
final delta = Delta()..insert('Hello 👋 World');

// Get previous character position
final prevPos = delta.prevRunePosition(7);  // Accounts for emoji

// Get next character position
final nextPos = delta.nextRunePosition(6);

Practical Examples

Building Rich Text

final delta = Delta()
  ..insert('Welcome to ')
  ..insert('AppFlowy Editor', attributes: {'bold': true})
  ..insert('!\n\n')
  ..insert('Features:\n', attributes: {'bold': true})
  ..insert('• Rich text formatting\n')
  ..insert('• Block-based structure\n')
  ..insert('• Collaborative editing\n');

Applying Formatting

// Format existing text as bold
final format = Delta()
  ..retain(7)  // Skip "Welcome"
  ..retain(2, attributes: {'bold': true})  // Make "to" bold
  ..retain(14);  // Skip rest

final formatted = originalDelta.compose(format);

Removing Formatting

// Remove bold from text
final removeFormat = Delta()
  ..retain(7)
  ..retain(2, attributes: {'bold': null})  // null removes attribute
  ..retain(14);

final unformatted = boldDelta.compose(removeFormat);

Delta JSON Structure

Deltas serialize to clean JSON arrays:
[
  {"insert": "Hello "},
  {"insert": "World", "attributes": {"bold": true}},
  {"insert": "!"}
]
This makes them easy to:
  • Store in databases
  • Send over network
  • Version control
  • Debug and inspect

Transaction Integration

Use transactions to modify deltas in nodes:
final transaction = editorState.transaction;

// Insert text
transaction.insertText(node, 0, 'Hello');

// Delete text
transaction.deleteText(node, 0, 5);

// Format text
transaction.formatText(node, 0, 5, {'bold': true});

// Replace text
transaction.replaceText(node, 0, 5, 'Hi');

await editorState.apply(transaction);
See Transactions for more details.

Best Practices

Use Compose

Build complex deltas by composing simple ones for clarity.

Validate Attributes

Check that attributes exist before accessing them.

Handle Unicode

Use prevRunePosition and nextRunePosition for correct character positions.

JSON Serialization

Deltas are JSON-serializable for easy storage and transmission.

See Also

Build docs developers (and LLMs) love