
lexical_image
Images for lexical_core, with a
resizable Flutter widget: drag handles, a configurable minimum and maximum,
aspect-ratio preservation and optional captions.
dependencies:
lexical_image: ^1.0.0
final editor = LexicalEditor(nodes: imageNodes);
registerImage(editor);
LexicalEditable(
editor: editor,
theme: theme,
decoratorBuilders: imageDecoratorBuilders(
editor: editor,
limits: const ImageSizeLimits(minWidth: 80, maxWidth: 640),
),
);
editor.dispatchCommand(
insertImageCommand,
const ImageAttributes(src: 'https://…/flowers.jpg', altText: 'Blumen'),
);
Compatible with the Lexical playground
The wire format matches the playground's ImageNode field for field, including
two conventions that are easy to get wrong:
widthandheightare0for "as large as it comes" — notnull, not absent. Upstream keeps'inherit'internally and writes0.maxWidthtravels in the document. It is arguably presentation policy, but dropping it would lose data on a round trip, so it is stored andImageSizeLimitsnarrows it per application.
image is not part of any published @lexical/* package — it lives in the
playground — so this shape is transcribed from that source rather than
generated by gen_fixtures.mjs like the core types. That is why the test suite
writes a playground document out in full and asserts a fixed point on it: the
compatibility claim is checked, not assumed.
A GIF is an image. Lexical has no separate node for one, and neither does
this: the playground's "GIF" entry dispatches the ordinary insert command with
a .gif source. Flutter animates it without being asked.
Captions
Upstream's caption is a nested editor — a second editor instance per image,
with its own history and selection. This port has no nested editors, so a
caption is kept verbatim as the state it arrived as and exposed as plain
text through captionText.
What that means in practice: a caption written on the web keeps its formatting through any number of round trips here, and editing one in Flutter replaces it with plain text. Losing formatting in a caption somebody deliberately edited is a fair trade; losing it by merely opening the document would not be.
Resizing
resizeImage is a pure function and tested as one — the arithmetic is the part
that goes wrong, not the gesture:
- corner drags follow whichever axis the pointer moved further along, so a diagonal drag tracks the pointer instead of only its horizontal part;
- edge drags derive the other axis, so an image never distorts;
- clamping scales both axes by the tightest constraint. Clamping them separately stretches the picture the moment one hits its limit, which looks like a bug because it is one.
A drag writes to the document once, when it ends. Resizing has to be one undo step, not one per pointer move.
A stored size is a size somebody chose on their screen, so an image wider than the column it lands in is drawn scaled down — proportionally, and for display only. The document keeps what it was given, and the image is full size again on a screen with room for it.
Where an image sits
Inside a paragraph. DecoratorNode.isInline() is true upstream and the
playground's ImageNode does not override it, so root > paragraph > image is
the shape every Lexical client writes and root > image is not one. The insert
command mirrors upstream's: put the image at the caret, and wrap it in a
paragraph if that turns out to be the root.
Markdown
final transformers = defaultMarkdownTransformers.extend(
textMatches: [imageTransformer],
);
Opt-in, exactly as upstream: @lexical/markdown ships no image rule and the
playground adds its own to the list it passes in. Only the alt text and the
source survive  — size, caption and maxWidth have no markdown
spelling, and upstream loses the same fields. Use the JSON wire format or
lexical_file to move a document without losing anything.
Sources are untrusted
defaultImageResolver accepts http(s):, data: and asset paths, and returns
null for everything else — a file: URL in a document from someone else has
no business reading the local disk. Replace it with your own resolver to add a
cache, an allow-list of hosts or an offline placeholder.
Licence
MIT. Derived from Lexical, © Meta Platforms, Inc., also MIT. See NOTICE.
Libraries
- lexical_image
- Images for
lexical_core, with a resizable Flutter widget.