Skip to main content

Overview

Image blocks display images with support for resizing, alignment, and various image sources (URLs, local files, base64). They provide a rich image editing experience with interactive controls.

Block Keys

class ImageBlockKeys {
  static const String type = 'image';
  static const String align = 'align';     // 'left', 'center', 'right'
  static const String url = 'url';         // Image source
  static const String width = 'width';     // Width in pixels
  static const String height = 'height';   // Height in pixels
}
Source: /lib/src/editor/block_component/image_block_component/image_block_component.dart:5

Creating Image Nodes

Use the imageNode() helper function:
// Simple image with URL
final image = imageNode(
  url: 'https://example.com/image.jpg',
);

// Image with custom alignment
final leftImage = imageNode(
  url: 'https://example.com/image.jpg',
  align: 'left',
);

// Image with specific dimensions
final sizedImage = imageNode(
  url: 'https://example.com/image.jpg',
  width: 400,
  height: 300,
);

// Center-aligned (default)
final centeredImage = imageNode(
  url: 'https://example.com/image.jpg',
  align: 'center',
);
Alignment Options:
  • 'left' - Left-aligned
  • 'center' - Center-aligned (default)
  • 'right' - Right-aligned
Source: /lib/src/editor/block_component/image_block_component/image_block_component.dart:33

Component Builder

Create an image component with custom configuration:
final imageBuilder = ImageBlockComponentBuilder(
  configuration: BlockComponentConfiguration(
    padding: (node) => const EdgeInsets.symmetric(vertical: 8.0),
  ),
  showMenu: true,
  menuBuilder: (node, state) {
    // Custom menu overlay
    return Positioned(
      top: 10,
      right: 10,
      child: Row(
        children: [
          IconButton(
            icon: const Icon(Icons.delete),
            onPressed: () => deleteImage(node),
          ),
          IconButton(
            icon: const Icon(Icons.edit),
            onPressed: () => editImage(node),
          ),
        ],
      ),
    );
  },
);
Configuration Options:
  • showMenu - Show menu on hover (default: false)
  • menuBuilder - Custom menu widget builder
Source: /lib/src/editor/block_component/image_block_component/image_block_component.dart:55

Widget Structure

class ImageBlockComponentWidget extends BlockComponentStatefulWidget {
  const ImageBlockComponentWidget({
    super.key,
    required super.node,
    super.showActions,
    super.actionBuilder,
    super.actionTrailingBuilder,
    super.configuration = const BlockComponentConfiguration(),
    this.showMenu = false,
    this.menuBuilder,
  });

  final bool showMenu;
  final ImageBlockComponentMenuBuilder? menuBuilder;
}
Source: /lib/src/editor/block_component/image_block_component/image_block_component.dart:95

Resizable Images

Images use the ResizableImage widget:
Widget child = ResizableImage(
  src: src,
  width: width,
  height: height,
  editable: editorState.editable,
  alignment: alignment,
  onResize: (width) {
    final transaction = editorState.transaction
      ..updateNode(node, {
        ImageBlockKeys.width: width,
      });
    editorState.apply(transaction);
  },
);
Features:
  • Drag to resize - Interactive resize handles
  • Maintains aspect ratio - Automatic height calculation
  • Alignment aware - Respects image alignment
  • Editable control - Can be disabled in read-only mode
Source: /lib/src/editor/block_component/image_block_component/image_block_component.dart:155

Alignment

Images support three alignment options:
final alignment = AlignmentExtension.fromString(
  attributes[ImageBlockKeys.align] ?? 'center',
);

extension AlignmentExtension on Alignment {
  static Alignment fromString(String name) {
    switch (name) {
      case 'left':
        return Alignment.centerLeft;
      case 'right':
        return Alignment.centerRight;
      default:
        return Alignment.center;
    }
  }
}
Source: /lib/src/editor/block_component/image_block_component/image_block_component.dart:308 When showMenu is enabled, a menu appears on hover:
if (widget.showMenu && widget.menuBuilder != null) {
  child = MouseRegion(
    onEnter: (_) => showActionsNotifier.value = true,
    onExit: (_) {
      if (!alwaysShowMenu) {
        showActionsNotifier.value = false;
      }
    },
    hitTestBehavior: HitTestBehavior.opaque,
    opaque: false,
    child: ValueListenableBuilder<bool>(
      valueListenable: showActionsNotifier,
      builder: (context, value, child) {
        return Stack(
          children: [
            child!,
            if (value) widget.menuBuilder!(widget.node, this),
          ],
        );
      },
      child: child,
    ),
  );
}
Source: /lib/src/editor/block_component/image_block_component/image_block_component.dart:197

Selection Behavior

Images support block selection:
child = BlockSelectionContainer(
  node: node,
  delegate: this,
  listenable: editorState.selectionNotifier,
  remoteSelection: editorState.remoteSelections,
  blockColor: editorState.editorStyle.selectionColor,
  supportTypes: const [
    BlockSelectionType.block,
  ],
  child: child,
);
  • Click to select - Entire image block
  • Keyboard navigation - Arrow keys work
  • Block operations - Delete, copy, cut, paste
Source: /lib/src/editor/block_component/image_block_component/image_block_component.dart:175

Validation

Image blocks must not have delta or children:
@override
BlockComponentValidate get validate =>
    (node) => node.delta == null && node.children.isEmpty;
Source: /lib/src/editor/block_component/image_block_component/image_block_component.dart:91

Using in Editor

Add image support to your editor:
AppFlowyEditor(
  editorState: editorState,
  blockComponentBuilders: {
    ImageBlockKeys.type: ImageBlockComponentBuilder(
      configuration: standardBlockComponentConfiguration,
      showMenu: true,
      menuBuilder: (node, state) {
        return ImageMenuWidget(node: node, state: state);
      },
    ),
    // ... other block types
  },
);

Inserting Images

From URL

// Insert image from URL
final transaction = editorState.transaction;
transaction.insertNode(
  path,
  imageNode(
    url: 'https://example.com/image.jpg',
    align: 'center',
  ),
);
editorState.apply(transaction);

From File Picker

import 'package:file_picker/file_picker.dart';

Future<void> insertImageFromFile() async {
  final result = await FilePicker.platform.pickFiles(
    type: FileType.image,
  );
  
  if (result != null && result.files.single.path != null) {
    final path = result.files.single.path!;
    final selection = editorState.selection;
    
    if (selection != null) {
      final transaction = editorState.transaction;
      transaction.insertNode(
        selection.end.path.next,
        imageNode(
          url: 'file://$path',
          align: 'center',
        ),
      );
      editorState.apply(transaction);
    }
  }
}

From Base64 (Web)

import 'dart:convert';
import 'dart:html' as html;

Future<void> insertImageFromUpload() async {
  final input = html.FileUploadInputElement()..accept = 'image/*';
  input.click();
  
  await input.onChange.first;
  if (input.files!.isEmpty) return;
  
  final file = input.files![0];
  final reader = html.FileReader();
  reader.readAsDataUrl(file);
  
  await reader.onLoad.first;
  final base64 = reader.result as String;
  
  final selection = editorState.selection;
  if (selection != null) {
    final transaction = editorState.transaction;
    transaction.insertNode(
      selection.end.path.next,
      imageNode(
        url: base64,
        align: 'center',
      ),
    );
    editorState.apply(transaction);
  }
}

Updating Images

Change Alignment

void updateImageAlignment(Node node, String alignment) {
  final transaction = editorState.transaction;
  transaction.updateNode(node, {
    ImageBlockKeys.align: alignment,
  });
  editorState.apply(transaction);
}

// Usage:
updateImageAlignment(imageNode, 'left');

Resize Image

void resizeImage(Node node, double width) {
  final transaction = editorState.transaction;
  transaction.updateNode(node, {
    ImageBlockKeys.width: width,
  });
  editorState.apply(transaction);
}

// Usage:
resizeImage(imageNode, 600);

Change Image Source

void changeImageUrl(Node node, String newUrl) {
  final transaction = editorState.transaction;
  transaction.updateNode(node, {
    ImageBlockKeys.url: newUrl,
  });
  editorState.apply(transaction);
}

Custom Image Menu

class ImageMenuWidget extends StatelessWidget {
  const ImageMenuWidget({
    super.key,
    required this.node,
    required this.state,
  });

  final Node node;
  final ImageBlockComponentWidgetState state;

  @override
  Widget build(BuildContext context) {
    return Positioned(
      top: 10,
      right: 10,
      child: Container(
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(8),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.1),
              blurRadius: 8,
            ),
          ],
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            IconButton(
              icon: const Icon(Icons.align_horizontal_left),
              onPressed: () => _updateAlign('left'),
              tooltip: 'Align left',
            ),
            IconButton(
              icon: const Icon(Icons.align_horizontal_center),
              onPressed: () => _updateAlign('center'),
              tooltip: 'Align center',
            ),
            IconButton(
              icon: const Icon(Icons.align_horizontal_right),
              onPressed: () => _updateAlign('right'),
              tooltip: 'Align right',
            ),
            IconButton(
              icon: const Icon(Icons.delete),
              onPressed: _deleteImage,
              tooltip: 'Delete',
            ),
          ],
        ),
      ),
    );
  }

  void _updateAlign(String align) {
    final editorState = state.editorState;
    final transaction = editorState.transaction;
    transaction.updateNode(node, {
      ImageBlockKeys.align: align,
    });
    editorState.apply(transaction);
  }

  void _deleteImage() {
    final editorState = state.editorState;
    final transaction = editorState.transaction;
    transaction.deleteNode(node);
    editorState.apply(transaction);
  }
}

Image Upload Widget

AppFlowy Editor includes an ImageUploadWidget for image uploads:
import 'package:appflowy_editor/appflowy_editor.dart';

// Use the built-in image upload widget
ImageUploadWidget(
  onSubmit: (url) async {
    final selection = editorState.selection;
    if (selection != null) {
      final transaction = editorState.transaction;
      transaction.insertNode(
        selection.end.path.next,
        imageNode(url: url),
      );
      editorState.apply(transaction);
    }
  },
)

Slash Menu Integration

final imageMenuItem = SelectionMenuItem(
  getName: () => 'Image',
  icon: (editorState, isSelected, style) => SelectionMenuIconWidget(
    icon: Icons.image,
    isSelected: isSelected,
    style: style,
  ),
  keywords: ['image', 'picture', 'photo', 'img'],
  handler: (editorState, menuService, context) async {
    final selection = editorState.selection;
    if (selection == null) return;

    // Show image upload dialog
    final url = await showImageUploadDialog(context);
    if (url == null) return;

    final node = editorState.getNodeAtPath(selection.start.path);
    if (node == null) return;

    final transaction = editorState.transaction;
    final delta = node.delta;
    
    if (delta != null && delta.isEmpty) {
      // Replace empty block
      transaction
        ..insertNode(selection.start.path, imageNode(url: url))
        ..deleteNode(node);
    } else {
      // Insert after current block
      transaction.insertNode(
        selection.start.path.next,
        imageNode(url: url),
      );
    }
    
    editorState.apply(transaction);
  },
);

Loading States

Handle image loading:
class ImageWithLoading extends StatelessWidget {
  const ImageWithLoading({super.key, required this.url});

  final String url;

  @override
  Widget build(BuildContext context) {
    return Image.network(
      url,
      loadingBuilder: (context, child, loadingProgress) {
        if (loadingProgress == null) return child;
        
        return Center(
          child: CircularProgressIndicator(
            value: loadingProgress.expectedTotalBytes != null
                ? loadingProgress.cumulativeBytesLoaded /
                    loadingProgress.expectedTotalBytes!
                : null,
          ),
        );
      },
      errorBuilder: (context, error, stackTrace) {
        return Container(
          color: Colors.grey[200],
          child: const Center(
            child: Icon(Icons.broken_image, size: 48),
          ),
        );
      },
    );
  }
}

Image Caching

Use cached network images for better performance:
import 'package:cached_network_image/cached_network_image.dart';

CachedNetworkImage(
  imageUrl: imageUrl,
  placeholder: (context, url) => const CircularProgressIndicator(),
  errorWidget: (context, url, error) => const Icon(Icons.error),
  fit: BoxFit.contain,
)

Key Features

  • Multiple Sources - URLs, local files, base64 data
  • Resizable - Interactive drag-to-resize
  • Alignment - Left, center, right alignment
  • Custom Menus - Hover menus with actions
  • Block Selection - Full selection support
  • Aspect Ratio - Automatic aspect ratio maintenance
  • Editable Control - Read-only mode support
  • Keyboard Navigation - Arrow key navigation

Best Practices

  1. Optimize images - Compress before uploading
  2. Use appropriate formats - JPEG for photos, PNG for graphics
  3. Provide alt text - Store in node attributes for accessibility
  4. Handle errors - Show placeholder on load failure
  5. Cache images - Use caching for network images
  6. Set reasonable max width - Prevent oversized images
  7. Support dark mode - Adjust image styling if needed

Accessibility

// Add alt text to images
final accessibleImage = Node(
  type: ImageBlockKeys.type,
  attributes: {
    ImageBlockKeys.url: url,
    'alt': 'Description of the image',
  },
);

// Use in custom image widget
Semantics(
  label: node.attributes['alt'] ?? 'Image',
  child: Image.network(url),
)

Table Blocks

Create and edit tables

Custom Blocks

Create custom block components

Build docs developers (and LLMs) love