View S3 Attachment
A dialog preview for a stored attachment. Handles images, PDFs, video and audio, and always offers open-in-tab and download.
'use client'
import * as React from 'react'
import { PaperclipIcon } from 'lucide-react'
import { ViewS3Attachment } from '@/components/custom/view-s3-attachment'
import { StorageProvider, type StorageAdapter } from '@/lib/kiota-storage'
const PAYSLIP_SVG =
'data:image/svg+xml;utf8,' +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="640" height="420">
<rect width="640" height="420" fill="#d7ece8"/>
<text x="320" y="215" font-family="sans-serif" font-size="26" fill="#0d5a4e" text-anchor="middle">Payslip — August 2026</text>
</svg>`,
)
const demoAdapter: StorageAdapter = {
signFile: async () => ({ data: { url: '/api/demo-upload', key: 'demo' } }),
getFile: async () => ({ data: { url: PAYSLIP_SVG, contentType: 'image/svg+xml' } }),
deleteFile: async () => ({ data: { ok: true } }),
}
export default function ViewS3AttachmentDemo() {
return (
<StorageProvider adapter={demoAdapter}>
<ViewS3Attachment
fileUrl="/payslips/1723459200_august-2026.png"
fileName="august-2026.png"
className="inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm hover:bg-accent"
>
<PaperclipIcon className="size-4" />
august-2026.png
</ViewS3Attachment>
</StorageProvider>
)
}Installation
pnpm dlx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/view-s3-attachment.jsonnpx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/view-s3-attachment.jsonyarn dlx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/view-s3-attachment.jsonbunx --bun shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/view-s3-attachment.jsonInstalls the component, the shadcn dialog and button primitives, and lib/kiota-storage.tsx. Wire the adapter once — see Storage adapter.
Usage
import { ViewS3Attachment } from '@/components/custom/view-s3-attachment'
export function AttachmentCell({ row }) {
return (
<ViewS3Attachment fileUrl={row.original.fileKey} fileName={row.original.fileName}>
<span className="text-sm underline">{row.original.fileName}</span>
</ViewS3Attachment>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
fileUrl | string | null | — | A storage key, or an http/data/blob URL. |
fileName | string | null | — | Heading and download filename. |
title | string | null | — | Overrides the heading. |
children | ReactNode | — | Required. The trigger — style it per call site. |
open / onOpenChange | boolean / (open: boolean) => void | — | Control the dialog from outside. |
className | string | — | Applied to the trigger wrapper. |
adapter | StorageAdapter | — | Overrides the provider for this instance. |
Behaviour notes
Signing is deferred until the dialog opens. A table of fifty rows would otherwise sign fifty URLs on mount, nearly all of which nobody looks at and most of which expire before they would be used.
Unknown types try an image first. An S3 key with no extension and no content type is rendered as an <img>, which contains rather than stretches; if that fails, the onError handler swaps in an <iframe> so PDFs and documents still load inline.
Kind detection prefers the content type and falls back to the extension, with the query string stripped first — a presigned URL ends in signature parameters, not .pdf.
Download and open-in-tab are always available, including for types the dialog cannot render. There is no file the dialog can open but not let you take away.