Skip to content

0018. The media block stores an alignment and no width

Status: Accepted; the alignment's values and default are superseded by 0044 (2026-11-02)

Date: 2026-11-02

Context

docs/rendering.md section 3 already lists blocknote-align--center, --right and --justify for "a block", and BlockNoteBlockBase::blockAttributes() (src/BlockNoteBlockBase.php:267-286) already turns a declared textAlignment prop into one of those classes through StyleVocabulary::classes(), passing the format's RenderPolicy so a value the policy forbids emits nothing. Nothing in that path names a block type: the class is a function of what a block plugin's own getPropSchema() declares, not of a list this module maintains.

DocumentValidator::validateBlockAlignment() (src/Document/DocumentValidator.php:580-592) is keyed the same way: it checks isset($plugin->getPropSchema()['textAlignment']), not a list of block types, so a block that declares the property is covered by the save-time refusal from the moment it declares it, with no corresponding change to the validator.

@blocknote/react's TextAlignButton decides whether to draw its own alignment buttons for a selected block with its useEditorState selector's blockHasType(firstBlock, editor, firstBlock.type, { textAlignment: defaultProps.textAlignment }) call (node_modules/@blocknote/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx:60), where defaultProps.textAlignment.values is upstream's own four-value list, left, center, right, justify. blockHasType() delegates to editorHasBlockWithType() (node_modules/@blocknote/core/src/blocks/defaultBlockTypeGuards.ts:153-155), which requires every value in the caller's list to be present in the block's own declared list; it does not require the reverse. A media block that declares only three values therefore fails that check and upstream draws no button for it, while the prop name, the value shape and the PHP class emission stay the ones the shared vocabulary already uses.

The sibling Image block, for an uploaded file rather than a media entity, already declares textAlignment with all four values (src/Plugin/BlockNoteBlock/Image.php:31) and a previewWidth prop, PropSpec::number('previewWidth', NULL, optional: TRUE) (Image.php:37), a pixel number an author drags to in whatever viewport the browser happens to be at when they resize it, with no button, menu item or other affordance anywhere in this module or upstream that clears it back to unset. It is the one shape a width control for the media block could copy, and copying it means copying that gap.

docs/decisions/0006-media-through-a-submodule.md recorded alignment and width as deferred rather than decided (its Decision section: "Alignment and width are deferred and alt text is not here at all") and its Consequences section named the same gap. This record closes that deferral. 0006's own status and decision text are not edited; 0006 already anticipated a later record.

Decision

The block declares textAlignment, a string prop with values left, center, right and default left: PropSpec::string('textAlignment', 'left', ['left', 'center', 'right']) in DrupalMedia::getPropSchema(). The block's #[BlockNoteBlock] attribute's version is raised from 1 to 2, because docs/extending.md's rule is that a property schema change the client specification must agree with raises the version, and this is the block's first such change since it shipped.

No justify. A figure is not justified the way a paragraph of running text is, and leaving the member out of the block's own list is what keeps it out of the toolbar and the block menu; the shared policy's justify flag still exists for text blocks and has no effect here.

No width prop. Size is the site builder's question, answered with a view mode the same way it already is for every other dimension of how a media item looks; a per-block pixel width is the shape Image::previewWidth already has, dragged in one author's viewport with nothing that unsets it once set, and adding a second, differently-shaped size axis (a stored pixel number beside a curated view mode) would grow the public vocabulary without giving a site builder anything a view mode does not already give them.

No alignment check of its own. DrupalMedia::validate() gains no new code for this. The class this prop turns into on the page, the refusal of a value the format's policy forbids, and the strip of a stored value the format no longer permits are all the mechanism the paragraph and heading blocks already use for the same prop name, reached automatically because the validator and the renderer both key on the declared property rather than a block type.

No class vocabulary entry and no template edit. The two classes already exist in the vocabulary for "a block"; modules/blocknote_media/templates/blocknote-block--drupal-media.html.twig prints {{ attributes }}, which already carries whatever class blockAttributes() computed, so the template needs no change to emit the new class.

Alternatives considered

A media-specific property name, such as alignment. Rejected. It would be a second vocabulary member meaning the same thing textAlignment already means for every other block, forcing a theme or a later block to know two names for one concept.

Reusing upstream's TextAlignButton by declaring all four values. Rejected. It draws its buttons at upstream's own toolbar position, not the position this block's chrome (ADR 0019) puts every one of its own functions at, and declaring justify would put a justify button in front of an author for a block that has no reason to offer one.

A closed list of width classes, such as blocknote-media-width--half, --full. Rejected for the reason 0006 already gave for a pixel width: a site builder already has a sharper tool for size, a view mode, and a second, class-based size axis would still need someone to decide what "half" means for every theme it reaches, which a view mode already answers per theme without this module inventing a scale.

Consequences

The block's contract version, the #[BlockNoteBlock] attribute's version, is 2 as of this record; the document schema version is unchanged at 1 (DocumentParser::SCHEMA_VERSION). A document saved before this change carries no textAlignment at all and renders as left, because the validator checks only the properties a stored block actually carries.

MediaRenderTest::testAnAlignedBlockCarriesTheAlignmentClass() and ::testAnAlignmentOutsideTheBlocksListEmitsNoClass() pin the class on the figure directly, because no golden fixture reaches the submodule (0006's Consequences already recorded that gap).

An author who wants a media item aligned and sized both still needs two separate decisions: the toolbar's alignment buttons for position, and a site-provided view mode for size. An author of an uploaded image has a drag handle that sets Image::previewWidth and nothing that unsets it; the media block offers alignment through the toolbar and leaves size to a view mode instead.

References

@blocknote/core (vendored under node_modules/): src/blocks/defaultBlockTypeGuards.ts.

@blocknote/react: src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx.

This module: modules/blocknote_media/src/Plugin/BlockNoteBlock/DrupalMedia.php, src/BlockNoteBlockBase.php, src/Document/DocumentValidator.php, src/Render/StyleVocabulary.php, src/Plugin/BlockNoteBlock/Image.php, docs/rendering.md, tests/src/Kernel/MediaBlockSettingsTest.php, tests/src/Kernel/MediaRenderTest.php.

0006, 0017.