ComponentsData display

File Viewer

Shows a file without leaving the page: images, PDF, text, JSON, Markdown, source code in any language, Google Docs links and shortcuts, plus spreadsheets and Office files through renderers you register. Use FileViewer for a full-screen dialog, ViewableFileContent inline, andFileContent when you load the bytes yourself.

Open a file

Attachments open the full-screen viewer. Local files stay in browser memory; linked files load from their URL; Google links embed Google’s own viewer.

Empty, invalid, and unsupported files

Each file explains what went wrong and how to recover. Errors offer Try again; formats the browser can’t show suggest downloading.

Inline in a panel

ViewableFileContent fills any flex column, such as a chat canvas tab or a fixed-height preview.

Opening file

Preparing the document viewer.

Opening file

Preparing the document viewer.

Files you load yourself

A file page or canvas tab fetches bytes its own way, then shows LoadingFile, FileError, or FileContent.

Opening file

Preparing the document viewer.

Google file in a canvas

A Google Drive file downloaded as Office bytes and shown with the renderers registered by FileRenderersProvider.

Opening hours

Current Google file

Opening file

Preparing the document viewer.

Proposed upload

An approval card shows the exact file an action will upload before anyone approves it.

Exact proposed file · 1 KB

Opening file

Preparing the document viewer.

Without renderers

With no sheet or Office renderer registered, those formats ask people to download the original. Markdown falls back to plain text.

Opening file

Preparing the document viewer.

Slides in a canvas tab

A file attached in chat opens in the canvas with ViewableFileContent, here rendered by the registered Office renderer.

Opening file

Preparing the document viewer.

Dialog

Open an attachmentTSX
import { FileAttachment } from "@tulipfarm/sprout/attachment";
import { FileViewer } from "@tulipfarm/sprout/file-viewer";

<FileAttachment file={attachment} preview viewable />

<FileViewer
  file={{ name: "Returns policy.pdf", mediaType: "application/pdf", source: { kind: "local", file } }}
  open={open}
  onOpenChange={setOpen}
/>

file.source is { kind: "local", file } for aFile in browser memory or { kind: "remote", url }for a link. The header shows the name and “Local file · Not uploaded”, “Linked file”, or “Google document”. Docs, Sheets, and Slides links on docs.google.com embed Google’s viewer and add Open in Google. Other files load up to 50 MB, time out after 15 seconds, and get Reload file and Download.

Stepping through files

One viewer for a set of filesTSX
const [index, setIndex] = useState<number | null>(null);

<FileViewer
  files={files}
  index={index ?? 0}
  onIndexChange={setIndex}
  open={index !== null}
  onOpenChange={(open) => !open && setIndex(null)}
  finalFocus={() => buttons.current[index ?? 0]}
/>

Pass files, index, andonIndexChange instead of file. With more than one file the header adds Previous file and Next file buttons and an “N of M” count. Left and Right arrows step too. The arrows stop at the first and last file. They are ignored while focus is in a text field, an embed, or a scrollable region, and when a modifier key is held.finalFocus picks where focus lands on close, usually the button for the file that was last on screen. AttachmentGroupwires this up for you.

Inline and your own bytes

A file page that fetches its own contentTSX
import { FileContent, FileError, LoadingFile, renderableType } from "@tulipfarm/sprout/file-viewer";

const url = URL.createObjectURL(new Blob([bytes], { type: renderableType(name, mediaType) }));

<section aria-label="File content" className="flex h-160 flex-col overflow-hidden rounded-xl border">
  {content.kind === "loading" ? <LoadingFile />
    : content.kind === "failed" ? <FileError message="The file content couldn’t be loaded. Your file is unchanged." onRetry={retry} />
    : <FileContent name={name} mediaType={mediaType} bytes={content.bytes} url={url} onRetry={retry} />}
</section>

All three fill a flex column, so give the parent a height.renderableType picks a safe type for the blob URL: PDF and raster images keep theirs, and anything a browser could run, including SVG, becomesapplication/octet-stream.

  • Images fit the frame; Actual size scrolls them at full resolution.
  • Text, code, JSON, and Markdown source show in a CodeBlockwith syntax colors and Copy. The language comes from the file name, such as.py, .rs, or Dockerfile. Code keeps line numbers; notes and Markdown wrap.
  • JSON is pretty-printed. Invalid JSON shows its original text with a warning.
  • Markdown is formatted, with a Show source toggle, when a Markdown renderer is registered.
  • FilePreview shows a small, non-interactive peek: the image, or the first 24 lines of text and code. isPreviewableFilesays whether a file has one.
  • Empty files, text that isn’t UTF-8, legacy .ppt, and other formats show a state that says what to do next.
  • An embed that hasn’t loaded after 12 seconds offers Reload viewer.

Renderers

Register spreadsheet, Office, and Markdown renderers onceTSX
import { FileRenderersProvider } from "@tulipfarm/sprout/file-viewer";

const SpreadsheetFile = lazy(() => import("./spreadsheet-file"));
const OfficeFile = lazy(() => import("./office-file"));

<FileRenderersProvider
  renderers={{
    sheet: (props) => <SpreadsheetFile {...props} />,      // { name, bytes, onRetry }
    office: (props) => <OfficeFile {...props} />,          // { name, bytes, kind: "document" | "slides", onRetry }
    markdown: ({ text }) => <MessageResponse>{text}</MessageResponse>,
  }}
>
  <App />
</FileRenderersProvider>

Spreadsheet and Office parsers are large, so Sprõut doesn’t bundle them. Each renderer runs inside Suspense with LoadingFile as the fallback, so lazy components work as they are. Renderers can useLoadingFile and FileError for their own states. Any viewer also takes a renderers prop that overrides the provider. The demo above uses small sample renderers.

Accessibility

The dialog is titled by the file name and traps focus; Escape or “Close file viewer” closes it and returns focus to the button that opened it. Loading is announced as a status and errors as an alert, each with a Try again button. Actual size is a toggle button witharia-pressed, and the zoomed image is a scrollable, focusable region. Text and code scroll in a focusable region named after the language, such as “Python code”, and formatted Markdown in one named after the file. Show source is a toggle button witharia-pressed. When stepping, the new file’s name and position, such as “README.md, file 2 of 6”, are announced politely. Embedded frames are named after the file. Images fade in over 180ms and the slow-viewer notice over 120ms; with reduced motion both appear at once.

Theme tokens

The content area uses --sprout-canvas; pages, the header, and toolbars use --sprout-surface and--sprout-line. Warnings use --sprout-danger, links --sprout-primary, and focus rings--sprout-focus.