Skip to main content

Overview

AppFlowy Editor supports two types of list blocks: bulleted (unordered) lists and numbered (ordered) lists. Both support nesting and rich text formatting.

Bulleted Lists

Block Keys

class BulletedListBlockKeys {
  static const String type = 'bulleted_list';
  static const String delta = blockComponentDelta;
  static const String backgroundColor = blockComponentBackgroundColor;
  static const String textDirection = blockComponentTextDirection;
}
Source: /lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart:6

Creating Bulleted List Nodes

// Simple bulleted list item
final bullet = bulletedListNode(
  text: 'First item',
);

// With rich text
final richBullet = bulletedListNode(
  delta: Delta()
    ..insert('Important: ')
    ..insert('Read this', attributes: {'bold': true}),
);

// With nested children
final nestedBullet = bulletedListNode(
  text: 'Parent item',
  children: [
    bulletedListNode(text: 'Child item 1'),
    bulletedListNode(text: 'Child item 2'),
  ],
);

// RTL text
final rtlBullet = bulletedListNode(
  text: 'عنصر القائمة',
  textDirection: 'rtl',
);
Source: /lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart:18

Bulleted List Icons

The default icons change based on nesting level:
static final bulletedListIcons = [
  '●',  // Level 0
  '◯',  // Level 1
  '□',  // Level 2
];
The icon cycles through these three styles based on nesting depth. Source: /lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart:206

Custom Icon Builder

Provide a custom icon builder:
BulletedListBlockComponentBuilder(
  iconBuilder: (context, node) {
    // Custom icon based on node attributes
    final level = _calculateLevel(node);
    final icons = ['🔵', '⚪', '🔸'];
    final icon = icons[level % icons.length];
    
    return Container(
      constraints: const BoxConstraints(minWidth: 26, minHeight: 22),
      padding: const EdgeInsets.only(right: 4.0),
      child: Center(
        child: Text(icon, style: const TextStyle(fontSize: 16)),
      ),
    );
  },
)

Markdown Shortcuts

Bulleted lists support markdown syntax:
* Item with asterisk
- Item with dash
Type * or - followed by a space to create bulleted lists.

Numbered Lists

Block Keys

class NumberedListBlockKeys {
  static const String type = 'numbered_list';
  static const String number = 'number';  // Starting number
  static const String delta = blockComponentDelta;
  static const String backgroundColor = blockComponentBackgroundColor;
  static const String textDirection = blockComponentTextDirection;
}
Source: /lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart:6

Creating Numbered List Nodes

// Simple numbered list
final numbered = numberedListNode(
  delta: Delta()..insert('First item'),
);

// Start at specific number
final customStart = numberedListNode(
  delta: Delta()..insert('Item'),
  number: 5,  // Starts at 5
);

// With nested children
final nestedNumbered = numberedListNode(
  delta: Delta()..insert('Parent item'),
  children: [
    numberedListNode(delta: Delta()..insert('Sub-item A')),
    numberedListNode(delta: Delta()..insert('Sub-item B')),
  ],
);
Source: /lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart:20

Number Formatting

Numbered lists use different formats based on nesting level:
final level = indexInRootLevel % 3;
final levelString = switch (level) {
  1 => indexInSameLevel.latin,     // a, b, c, ...
  2 => indexInSameLevel.roman,     // i, ii, iii, ...
  _ => '$indexInSameLevel',        // 1, 2, 3, ...
};
Formatting by Level:
  • Level 0 (root): 1, 2, 3, …
  • Level 1 (nested once): a, b, c, …
  • Level 2 (nested twice): i, ii, iii, …
  • Pattern repeats for deeper nesting
Source: /lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart:258

Custom Numbering

The number attribute sets the starting number:
// Create a list starting at 10
final transaction = editorState.transaction;
transaction.insertNode(
  path,
  numberedListNode(
    delta: Delta()..insert('Item 10'),
    number: 10,
  ),
);
editorState.apply(transaction);
Subsequent items automatically increment from this starting point.

Markdown Shortcuts

Create numbered lists with markdown:
1. First item
2. Second item
3. Third item
Type a number followed by . and space (e.g., 1. ).

Component Builders

Bulleted List Builder

BulletedListBlockComponentBuilder(
  configuration: BlockComponentConfiguration(
    padding: (node) => const EdgeInsets.symmetric(vertical: 2.0),
    placeholderText: (node) => 'List item',
    textStyle: (node, {textSpan}) => const TextStyle(
      fontSize: 16.0,
      height: 1.5,
    ),
  ),
  iconBuilder: (context, node) {
    // Custom icon
    return const Icon(Icons.circle, size: 8);
  },
)
Source: /lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart:38

Numbered List Builder

NumberedListBlockComponentBuilder(
  configuration: BlockComponentConfiguration(
    padding: (node) => const EdgeInsets.symmetric(vertical: 2.0),
    placeholderText: (node) => 'List item',
  ),
  iconBuilder: (context, node, direction) {
    // Custom number rendering
    final number = calculateNumber(node);
    return Container(
      constraints: const BoxConstraints(minWidth: 26),
      child: Text(
        '$number.',
        style: const TextStyle(fontWeight: FontWeight.w500),
      ),
    );
  },
)
Source: /lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart:49

Mixins Used

Both list types use the same mixins:
class _BulletedListBlockComponentWidgetState
    extends State<BulletedListBlockComponentWidget>
    with
        SelectableMixin,
        DefaultSelectableMixin,
        BlockComponentConfigurable,
        BlockComponentBackgroundColorMixin,
        NestedBlockComponentStatefulWidgetMixin,  // Important for nesting!
        BlockComponentTextDirectionMixin,
        BlockComponentAlignMixin
Key Mixin:
  • NestedBlockComponentStatefulWidgetMixin - Enables child list items
Source: /lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart:89

Nesting and Indentation

Indent/Outdent Commands

Lists support indentation:
// In standard command shortcuts:
indentCommand,   // Tab key
outdentCommand,  // Shift+Tab key

Creating Nested Lists

final nestedList = bulletedListNode(
  text: 'Level 1',
  children: [
    bulletedListNode(
      text: 'Level 2',
      children: [
        bulletedListNode(text: 'Level 3'),
      ],
    ),
  ],
);

Indent Padding Configuration

BlockComponentConfiguration(
  indentPadding: (node, textDirection) {
    switch (textDirection) {
      case TextDirection.ltr:
        return const EdgeInsets.only(left: 24.0);
      case TextDirection.rtl:
        return const EdgeInsets.only(right: 24.0);
    }
  },
)

Using Lists in Editor

AppFlowyEditor(
  editorState: editorState,
  blockComponentBuilders: {
    BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
      configuration: standardBlockComponentConfiguration.copyWith(
        placeholderText: (_) => 'List item',
      ),
    ),
    NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
      configuration: standardBlockComponentConfiguration.copyWith(
        placeholderText: (_) => 'List item',
      ),
    ),
    // ... other block types
  },
  characterShortcutEvents: [
    formatAsteriskToBulletedList,
    formatMinusToBulletedList,
    formatNumberToNumberedList,
    // ... other shortcuts
  ],
  commandShortcutEvents: [
    indentCommand,
    outdentCommand,
    // ... other commands
  ],
);

Inserting List Items

// Insert bulleted list
final transaction = editorState.transaction;
transaction.insertNode(
  path,
  bulletedListNode(text: 'New item'),
);
editorState.apply(transaction);

// Convert paragraph to list
final selection = editorState.selection;
if (selection != null) {
  final node = editorState.getNodeAtPath(selection.start.path);
  if (node != null && node.type == ParagraphBlockKeys.type) {
    final delta = node.delta;
    final transaction = editorState.transaction;
    transaction
      ..deleteNode(node)
      ..insertNode(
        selection.start.path,
        bulletedListNode(delta: delta),
      );
    editorState.apply(transaction);
  }
}

Converting Between List Types

// Convert bulleted to numbered
void convertToNumberedList(Node node) {
  if (node.type != BulletedListBlockKeys.type) return;
  
  final transaction = editorState.transaction;
  transaction.updateNode(node, {
    'type': NumberedListBlockKeys.type,
    NumberedListBlockKeys.number: 1,
  });
  editorState.apply(transaction);
}

// Convert numbered to bulleted
void convertToBulletedList(Node node) {
  if (node.type != NumberedListBlockKeys.type) return;
  
  final transaction = editorState.transaction;
  final attributes = Map<String, dynamic>.from(node.attributes);
  attributes.remove(NumberedListBlockKeys.number);
  attributes['type'] = BulletedListBlockKeys.type;
  
  transaction.updateNode(node, attributes);
  editorState.apply(transaction);
}

Handling New Lines

Special behavior for Enter key:
// In character shortcuts:
insertNewLineAfterBulletedList,
insertNewLineAfterNumberedList,
Behavior:
  • Empty list item + Enter → Convert to paragraph
  • Non-empty item + Enter → Create new list item
  • End of nested item + Enter → Create sibling item

Custom Styling Examples

Colored Bullets

BulletedListBlockComponentBuilder(
  iconBuilder: (context, node) {
    final level = _calculateLevel(node);
    final colors = [Colors.blue, Colors.green, Colors.orange];
    final color = colors[level % colors.length];
    
    return Container(
      constraints: const BoxConstraints(minWidth: 26, minHeight: 22),
      padding: const EdgeInsets.only(right: 4.0),
      child: Center(
        child: Icon(
          Icons.circle,
          size: 8,
          color: color,
        ),
      ),
    );
  },
)

Checkmark Lists

BulletedListBlockComponentBuilder(
  iconBuilder: (context, node) {
    final isComplete = node.attributes['complete'] == true;
    
    return GestureDetector(
      onTap: () => toggleComplete(node),
      child: Icon(
        isComplete ? Icons.check_box : Icons.check_box_outline_blank,
        size: 20,
      ),
    );
  },
)

Key Features

Bulleted Lists

  • Automatic bullet icons - Three icon styles for nesting
  • Custom icons - Full icon customization
  • Markdown shortcuts - * and - syntax
  • Nested support - Unlimited nesting levels
  • Rich text - Full Delta formatting

Numbered Lists

  • Smart numbering - Automatic sequential numbering
  • Multiple formats - Numbers, letters, roman numerals
  • Custom start - Begin at any number
  • Level-based formatting - Different styles per level
  • Markdown shortcuts - 1. syntax

Best Practices

  1. Use bulleted lists for unordered items of equal importance
  2. Use numbered lists for sequential steps or ranked items
  3. Limit nesting to 3-4 levels for readability
  4. Keep items concise - Break long items into sub-items
  5. Maintain consistency - Don’t mix list types at the same level

Quote Blocks

Styled quote blocks

Paragraph Blocks

Basic text blocks

Build docs developers (and LLMs) love