Skip to content

fix: set width attribute on image and video elements in editor render#2740

Open
nperez0111 wants to merge 3 commits into
mainfrom
feat/readonly-image-width
Open

fix: set width attribute on image and video elements in editor render#2740
nperez0111 wants to merge 3 commits into
mainfrom
feat/readonly-image-width

Conversation

@nperez0111
Copy link
Copy Markdown
Contributor

@nperez0111 nperez0111 commented May 13, 2026

Summary

When the editor is rendered with editable: false, the <img> and <video> tags did not include a width attribute even though the block's previewWidth prop was set.

Fixes #2726

Rationale

The width was only applied as an inline style on the wrapper <div>, not on the media element itself. This prevented the browser from reserving space before the media loaded (CLS), and made the rendered DOM lossy for downstream consumers reading innerHTML from a read-only editor.

While auditing, I found the same issue in the video block — plus the React VideoToExternalHTML was dropping previewWidth on export entirely. Audio and File blocks don't have a width concept, so they're unaffected.

Changes

  • packages/react/src/blocks/Image/block.tsx: set width on ImagePreview's <img>.
  • packages/core/src/blocks/Image/block.ts: set image.width (when defined) in imageRender.
  • packages/react/src/blocks/Video/block.tsx: set width on VideoPreview's <video> and also on VideoToExternalHTML (was missing).
  • packages/core/src/blocks/Video/block.ts: guard the existing video.width = previewWidth against undefined (previously coerced to 0).
  • Updated ServerBlockNoteEditor snapshot to reflect the new width="256" attribute.

Impact

CSS width: 100% on .bn-visual-media still controls layout, so resize behavior in editable mode is unaffected — the HTML attribute is purely additive and advertises the intrinsic width to read-only consumers.

Testing

  • pnpm --filter @blocknote/server-util test passes (snapshot updated).
  • pnpm --filter @blocknote/react test passes.
  • pnpm --filter @blocknote/core test passes (an unrelated numbered-list performance test is flaky on this machine but is not touched by this change).

Summary by CodeRabbit

  • Bug Fixes
    • Image and Video block previews now consistently respect the preview width property across all rendering implementations, ensuring uniform preview sizing behavior.

Review Change Stack

When the editor is rendered with editable: false, the <img> tag did not
include a width attribute even though the block's previewWidth prop was
set — the width was only applied as inline style on the wrapper. This
prevented the browser from reserving space before the image loaded
(CLS) and made the rendered DOM lossy for downstream consumers reading
innerHTML.

Apply previewWidth directly to the <img> element in the editor render
path, mirroring what imageToExternalHTML and the core Video block
already do. CSS width: 100% on .bn-visual-media still controls layout
so resize behavior is unaffected.

Fixes #2726
@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview May 13, 2026 7:55am
blocknote-website Ready Ready Preview May 13, 2026 7:55am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Walkthrough

This PR applies previewWidth as an HTML width attribute in core and React preview/export renderers for Image and Video, so preview elements include the block's width when present.

Changes

Media preview width propagation

Layer / File(s) Summary
Apply previewWidth to image renders
packages/core/src/blocks/Image/block.ts, packages/react/src/blocks/Image/block.tsx
imageRender conditionally sets width from block.props.previewWidth on the HTML <img> element; ImagePreview sets width={props.block.props.previewWidth} on the preview <img> element.
Apply previewWidth to video renders
packages/core/src/blocks/Video/block.ts, packages/react/src/blocks/Video/block.tsx
Core video render now sets video.width only when block.props.previewWidth is provided; VideoPreview and VideoToExternalHTML include width={props.block.props.previewWidth} for preview-mode <video>.

🎯 3 (Moderate) | ⏱️ ~20 minutes

"I munch on bytes beneath the moonlit code,
Widths tucked in tags so pages hold their road,
No sudden jumps as pixels find their place,
Calm scrolls and steady views — a quiet interface. 🐰"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding width attributes to image and video elements during editor rendering.
Description check ✅ Passed The description covers all key sections: Summary, Rationale, Changes, Impact, and Testing. It provides clear context, explains the motivation, lists all modified files, and confirms test results.
Linked Issues check ✅ Passed The PR directly addresses the primary objective of #2726 by emitting width attributes on image and video elements during HTML serialization, preventing CLS and preserving round-trip fidelity.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing width attribute serialization on image and video elements; no unrelated modifications are present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/readonly-image-width

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Apply the same fix as the image block to video:
- React VideoPreview now sets width on the <video> element so read-only
  rendering includes it.
- React VideoToExternalHTML now serializes previewWidth (it was
  previously dropped entirely on export).
- Core video render path guards `video.width = previewWidth` against
  undefined (assigning undefined to a numeric DOM property coerces to
  0).

Audio and File blocks don't have a previewWidth prop, so no change is
needed there.

Refs #2726
@nperez0111 nperez0111 changed the title fix: set width attribute on image in editor render (read-only mode) fix: set width attribute on image and video elements in editor render May 13, 2026
- Image snapshots now include the new width attribute on the <img>
  element when previewWidth is set.
- Video snapshots no longer include width="0" since the core video
  render now guards against an undefined previewWidth.

Refs #2726
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 13, 2026

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2740

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2740

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2740

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2740

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2740

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2740

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2740

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2740

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2740

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2740

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2740

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2740

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2740

commit: b1cf009

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/react/src/blocks/Video/block.tsx`:
- Line 30: The React implementation passes props.block.props.previewWidth
directly into the width attribute causing width="0" to be emitted; update
VideoPreview and VideoToExternalHTML so they only set the width attribute when
props.block.props.previewWidth is truthy (i.e., use a conditional guard like the
core block does) — locate the uses of props.block.props.previewWidth in the
VideoPreview and VideoToExternalHTML components and change them to only assign
width when that value is truthy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 418a9712-4fa6-4ba6-a89f-067ec85746cb

📥 Commits

Reviewing files that changed from the base of the PR and between b2aa79c and b1cf009.

⛔ Files ignored due to path filters (10)
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/basic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/nested.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noCaption.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noName.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video/withCaption.html is excluded by !**/__snapshots__/**
  • tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/basic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/nested.html is excluded by !**/__snapshots__/**
  • tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noCaption.html is excluded by !**/__snapshots__/**
  • tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noName.html is excluded by !**/__snapshots__/**
📒 Files selected for processing (2)
  • packages/core/src/blocks/Video/block.ts
  • packages/react/src/blocks/Video/block.tsx

: resolved.downloadUrl
}
controls={true}
width={props.block.props.previewWidth}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent handling of zero width compared to core implementation.

The React implementation passes previewWidth directly to the width attribute, which means React will render width="0" when previewWidth is 0. In contrast, the core implementation (lines 95-97 in packages/core/src/blocks/Video/block.ts) uses a truthiness check that prevents setting width when previewWidth is any falsy value, including 0.

While previewWidth=0 is unlikely in practice, the implementations should handle edge cases consistently.

🔧 Align with core's conditional guard pattern

For VideoPreview (line 30):

     <video
       className={"bn-visual-media"}
       src={
         resolved.loadingState === "loading"
           ? props.block.props.url
           : resolved.downloadUrl
       }
       controls={true}
-      width={props.block.props.previewWidth}
+      width={props.block.props.previewWidth || undefined}
       contentEditable={false}
       draggable={false}
     />

For VideoToExternalHTML (line 48):

   const video = props.block.props.showPreview ? (
-    <video src={props.block.props.url} width={props.block.props.previewWidth} />
+    <video src={props.block.props.url} width={props.block.props.previewWidth || undefined} />
   ) : (

This ensures React won't render the width attribute when previewWidth is falsy (including 0), matching the core behavior.

Also applies to: 48-48

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react/src/blocks/Video/block.tsx` at line 30, The React
implementation passes props.block.props.previewWidth directly into the width
attribute causing width="0" to be emitted; update VideoPreview and
VideoToExternalHTML so they only set the width attribute when
props.block.props.previewWidth is truthy (i.e., use a conditional guard like the
core block does) — locate the uses of props.block.props.previewWidth in the
VideoPreview and VideoToExternalHTML components and change them to only assign
width when that value is truthy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Image block's width is not serialized to HTML attributes, causing layout shift (CLS) when rendering exported HTML

1 participant