36 / Documents
DOCX Viewer
Renders a Word document as elements built through an allowlist, so a file you did not write cannot bring its own scripts or links.
"use client"
import * as React from "react"
import { SAMPLE_DOCX } from "@/components/demos/document-fixtures"
import { DocxViewer } from "mischief-ui/docx-viewer"
export function DocxViewerDemo() {
const [file, setFile] = React.useState<ArrayBuffer>()
React.useEffect(() => {
const controller = new AbortController()
void fetch(SAMPLE_DOCX, { signal: controller.signal })
.then((response) => response.arrayBuffer())
.then((buffer) => setFile(buffer))
.catch(() => undefined)
return () => controller.abort()
}, [])
return <DocxViewer className="w-full max-w-2xl" source={file} />
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/docx-viewerimport { DocxViewer } from "mischief-ui/docx-viewer"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { TriangleAlert } from "lucide-react"import { cn } from "@/lib/utils" export type DocxResult = { html: string messages?: string[]} export type DocxConverter = (source: ArrayBuffer) => Promise<DocxResult> export type DocxViewerProps = Omit<Usage
export function Contract({ file }: { file: File }) {
return <DocxViewer source={file} />
}What actually reaches the page
mammoth converts a .docx into an HTML string. That string is never handed to the browser as markup. It is parsed, walked, and rebuilt as React elements, keeping only tags on an allowlist and only attributes allowed for each of those tags, so a document from someone else cannot introduce script, styling, or event handlers into your page.
- Elements outside the allowlist are dropped, and script and style subtrees are dropped whole rather than unwrapped.
- href values are checked, and javascript: links are stripped.
- Whitespace-only text between structural tags is discarded, so tables and lists do not inherit stray gaps.
Widen or narrow the allowlist with allowedTags when your documents need something more, and keep it as small as the documents allow.
Fidelity
This is a structural view, not a page-faithful one. Headings, lists, tables, links, and emphasis survive; page geometry does not. Fonts, margins, columns, headers and footers, page breaks, and anything positioned absolutely are lost, because the source markup does not carry them.
When the layout is the point -- a contract that must look like the signed copy -- convert to PDF on the server and use the PDF Viewer instead.
API
sourceArrayBuffer | BlobThe document to convert.resultDocxResultAlready converted html and messages. Skips the converter.converter(source: ArrayBuffer) => Promise<DocxResult>Replaces the default converter. Supply this and mammoth is never loaded.allowedTagsreadonly string[]The tags permitted in the output. Anything else keeps its text and loses its wrapper.showWarningsbooleanLists conversion messages under the body.Accessibility
Converted markup is never injected. The HTML is parsed and rebuilt as React elements through a tag and attribute allowlist, so event handler attributes cannot survive and a javascript: link loses its href while keeping its text. Links that do survive open in a new tab with noreferrer. The region reports aria-busy while a document is converting.