File Viewer
Universal preview surface — detects the file type from MIME / extension and renders inline: PDF, images, video, audio, Markdown, source code, JSON, CSV, and plain text. Unknown types fall back to a download CTA. One component replaces a stack of bespoke per-type renderers.
Lumeo.FileViewer — install it alongside the core Lumeo
package. It transitively pulls Lumeo.PdfViewer
(for the PDF kind) and Lumeo.CodeEditor (for the Code / JSON
kinds), so a single install covers every supported preview.
When to Use
- Document or attachment lists where each row links to a heterogeneous file (PDF report, screenshot, CSV export, README)
- File managers / DMS surfaces that need a quick preview pane next to the tree
- Audit / compliance flows where the user has to inspect the actual content before approving an action
- Ticketing systems where customers attach mixed-format evidence
- Anywhere consumers would otherwise build a switch over Pdf / Image / Video / Markdown viewers by hand
How Detection Works
Resolution runs in this order — each step wins over the next:
- Explicit
Kindparameter (anything other thanAutoshort-circuits) - Explicit
MimeTypeparameter (most reliable for blob URLs / signed URLs where the extension is hidden) - Optional
HEADrequestContent-Type— off by default (AutoHead="true"to enable) because many CDNs reject HEAD with 405 - URL extension — the last-resort guess from the path segment
Lumeo FileViewer
A universal preview surface for Blazor — drop in a URL, get the right inline viewer.
Highlights
- Auto-detect by MIME, optional HEAD, or URL extension
- Built-in renderers for PDF, image, video, audio, markdown, source code, JSON, CSV, and plain text
- Auth-aware fetches via
HttpClientparameter orConfigureRequestdelegate - Safety caps —
MaxBytesfor text fetches,MaxCsvRowsfor tabular data - Pluggable per-kind overrides via
CustomRenderers
Quick start
<FileViewer Src="@DocumentUrl"
FileName="@Document.Name"
OnLoaded="HandleLoaded"
Class="h-[480px]" />
Resolution order
- Explicit
Kindparameter (anything other thanAutowins) - Explicit
MimeTypeparameter HEADrequestContent-Type(whenAutoHead="true")- URL extension
Markdown is rendered with
.DisableHtml()— raw<script>and<iframe>in user-supplied markdown never reach the DOM.
For more, see the docs site.
| component | category | nuget_package | first_release |
|---|---|---|---|
| Accordion | Navigation | Lumeo | 1.0.0 |
| DataGrid | Data Display | Lumeo.DataGrid | 2.0.0 |
| FileViewer | Data Display | Lumeo.FileViewer | 3.2.5 |
| Map | Data Display | Lumeo.Maps | 3.2.0 |
| PdfViewer | Data Display | Lumeo.PdfViewer | 3.1.0 |
| QueryBuilder | Forms | Lumeo | 2.1.0 |
| RichTextEditor | Forms | Lumeo.Editor | 2.0.0 |
| Scheduler | Data Display | Lumeo.Scheduler | 2.0.0 |
| Toast | Feedback | Lumeo | 1.0.0 |
| TreeView | Data Display | Lumeo | 1.0.0 |
Lumeo FileViewer
A universal preview surface for Blazor — drop in a URL, get the right inline viewer.
Highlights
- Auto-detect by MIME, optional HEAD, or URL extension
- Built-in renderers for PDF, image, video, audio, markdown, source code, JSON, CSV, and plain text
- Auth-aware fetches via
HttpClientparameter orConfigureRequestdelegate - Safety caps —
MaxBytesfor text fetches,MaxCsvRowsfor tabular data - Pluggable per-kind overrides via
CustomRenderers
Quick start
<FileViewer Src="@DocumentUrl"
FileName="@Document.Name"
OnLoaded="HandleLoaded"
Class="h-[480px]" />
Resolution order
- Explicit
Kindparameter (anything other thanAutowins) - Explicit
MimeTypeparameter HEADrequestContent-Type(whenAutoHead="true")- URL extension
Markdown is rendered with
.DisableHtml()— raw<script>and<iframe>in user-supplied markdown never reach the DOM.
For more, see the docs site.
FileKind.Markdown| <!DOCTYPE html> | |||||
|---|---|---|---|---|---|
| <html lang=en> | |||||
| <head> | |||||
| <meta charset=utf-8 /> | |||||
| <meta name=viewport content=width=device-width, initial-scale=1.0 /> | |||||
| <title>Lumeo — Blazor Component Library</title> | |||||
| <base href=/ /> | |||||
| <link href=_framework/dotnet.yhbg0qfzvu.js rel=preload as=script fetchpriority=high crossorigin=anonymous integrity=sha256-ygm0zZGACbeRLESIM3TIQgHjeWKOMFcnkZRreY23G/U= /> | |||||
| <!-- Preload the Blazor runtime JS so it downloads in parallel with HTML parsing --> | |||||
| <link rel=preload as=script href=_framework/blazor.webassembly.zxhwjtv6sc.js crossorigin> | |||||
| <link rel=icon type=image/svg+xml href=favicon.svg /> | |||||
| <!-- SEO + social sharing --> | |||||
| <meta name=description content=Lumeo — 154 accessible Blazor components inspired by shadcn/ui. AI primitives, motion, full DataGrid, 14 locales, Tailwind CSS v4. MIT, .NET 10. /> | |||||
| <meta name=theme-color content=#0a0a0a /> | |||||
| <!-- Open Graph (Discord | Slack | Facebook) --> | |||
| <meta property=og:site_name content=Lumeo /> | |||||
| <meta property=og:type content=website /> | |||||
| <meta property=og:title content=Lumeo — Blazor component library you'd actually want to ship /> | |||||
| <meta property=og:description content=154 accessible Blazor components across 7 packages — ~400 KB core + opt-in satellites (Charts, DataGrid, Editor, Scheduler, Gantt, PdfViewer, Maps). Tailwind v4 native, AI primitives, MIT, .NET 10. /> | |||||
| <meta property=og:url content=https://lumeo.nativ.sh /> | |||||
| <meta property=og:image content=https://lumeo.nativ.sh/social-preview.png /> | |||||
| <meta property=og:image:width content=1280 /> | |||||
| <meta property=og:image:height content=640 /> | |||||
| <!-- Twitter / X summary card --> | |||||
| <meta name=twitter:card content=summary_large_image /> | |||||
| <meta name=twitter:title content=Lumeo — Blazor component library /> | |||||
| <meta name=twitter:description content=154 components across 7 packages. ~400 KB core + opt-in satellites. Tailwind v4, AI, 14 locales, MIT. /> | |||||
| <meta name=twitter:image content=https://lumeo.nativ.sh/social-preview.png /> | |||||
| <!-- Consent banner FOUC guard — runs synchronously BEFORE Blazor boots so the | |||||
| banner never flashes for users who already gave (or rejected) consent. | |||||
| Adds .lumeo-consent-decided to <html> if the consent localStorage entry exists; | |||||
| a CSS rule below force-hides any element with class .lumeo-consent-banner. --> | |||||
| <script> | |||||
| (function () { | |||||
| try { | |||||
| if (localStorage.getItem('lumeo:consent:v1')) { | |||||
| document.documentElement.classList.add('lumeo-consent-decided'); | |||||
| } | |||||
| } catch (e) { /* private mode / cookies blocked — fall through | banner shows once */ } | ||||
| })(); | |||||
| </script> | |||||
| <!-- Splash screen — plain CSS | loads before Tailwind --> | ||||
| <style> | |||||
| html.lumeo-consent-decided .lumeo-consent-banner { display: none !important; } | |||||
| .lumeo-splash { | |||||
| position: fixed; inset: 0; z-index: 9999; | |||||
| display: flex; align-items: center; justify-content: center; | |||||
| background: #ffffff; color: hsl(240 5.9% 10%); | |||||
| } | |||||
| html.dark .lumeo-splash { background: hsl(240 10% 3.9%); color: hsl(0 0% 98%); } | |||||
| .lumeo-splash-inner { display: flex; flex-direction: column; align-items: center; gap: 2.5rem; text-align: center; } | |||||
| .lumeo-splash-logo-wrap { position: relative; width: 88px; height: 88px; display: flex; align-items: center; justify-content: center; } | |||||
| .lumeo-splash-ripple { position: absolute; inset: 0; border-radius: 50%; border: 1px solid currentColor; opacity: 0; } | |||||
| @keyframes splash-ripple { 0% { transform: scale(1); opacity: 0.5; } 100% { transform: scale(7); opacity: 0; } } | |||||
| .lumeo-splash-ripple.r1 { animation: splash-ripple 5.5s cubic-bezier(0 | 0.45 | 0.2 | 1) 1.2s infinite; } | ||
| .lumeo-splash-ripple.r2 { animation: splash-ripple 5.5s cubic-bezier(0 | 0.45 | 0.2 | 1) 3s infinite; } | ||
| .lumeo-splash-ripple.r3 { animation: splash-ripple 5.5s cubic-bezier(0 | 0.45 | 0.2 | 1) 4.8s infinite; } | ||
| .lumeo-splash-svg { position: relative; z-index: 1; width: 88px; height: 88px; animation: splash-logo-in 1.4s cubic-bezier(0.16 | 1 | 0.3 | 1) both; } | ||
| @keyframes splash-logo-in { from { opacity: 0; transform: scale(0.65); } to { opacity: 1; transform: scale(1); } } | |||||
| .lumeo-splash-ring { animation: splash-ring-breathe 4.5s ease-in-out 1.4s infinite; } | |||||
| @keyframes splash-ring-breathe { 0% | 100% { opacity: 0.28; } 50% { opacity: 0.65; } } | ||||
| .lumeo-splash-dot { transform-origin: 16px 16px; animation: splash-dot-breathe 4.5s ease-in-out 1.4s infinite; } | |||||
| @keyframes splash-dot-breathe { 0% | 100% { transform: scale(1); opacity: 0.7; } 50% { transform: scale(1.22); opacity: 1; } } | ||||
| .lumeo-splash-texts { display: flex; flex-direction: column; align-items: center; gap: 0.55rem; } | |||||
| .lumeo-splash-name { font-family: system-ui | -apple-system | sans-serif; font-size: 2rem; font-weight: 600; letter-spacing: 0.02em; line-height: 1; animation: splash-name-in 1s cubic-bezier(0.16 | 1 | 0.3 | 1) 0.8s both; } |
| @keyframes splash-name-in { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } } | |||||
| .lumeo-splash-tagline { font-family: system-ui | -apple-system | sans-serif; font-size: 0.68rem; letter-spacing: 0.18em; text-transform: uppercase; animation: splash-tagline-in 1s cubic-bezier(0.16 | 1 | 0.3 | 1) 1.3s both; } |
| @keyframes splash-tagline-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 0.35; transform: translateY(0); } } | |||||
| </style> | |||||
| <!-- Pre-built Tailwind CSS (compiled from Styles/tailwind.css via @tailwindcss/cli). | |||||
| `?v=d4faf93` is rewritten by the deploy workflow with the git SHA so the browser | |||||
| re-fetches whenever the CSS source changes — no more stale cached tailwind.out.css. --> | |||||
| <link rel=stylesheet href=css/tailwind.out.css?v=d4faf93 /> | |||||
| <!-- Lumeo theme CSS --> | |||||
| <link rel=stylesheet href=_content/Lumeo/css/lumeo.css?v=d4faf93 /> | |||||
| <link rel=stylesheet href=_content/Lumeo/css/themes/_blue.css?v=d4faf93 /> | |||||
| <link rel=stylesheet href=_content/Lumeo/css/themes/_green.css?v=d4faf93 /> | |||||
| <link rel=stylesheet href=_content/Lumeo/css/themes/_rose.css?v=d4faf93 /> | |||||
| <link rel=stylesheet href=_content/Lumeo/css/themes/_orange.css?v=d4faf93 /> | |||||
| <link rel=stylesheet href=_content/Lumeo/css/themes/_violet.css?v=d4faf93 /> | |||||
| <link rel=stylesheet href=_content/Lumeo/css/themes/_amber.css?v=d4faf93 /> | |||||
| <link rel=stylesheet href=_content/Lumeo/css/themes/_teal.css?v=d4faf93 /> | |||||
| <!-- Self-hosted fonts (GDPR compliant) --> | |||||
| <link rel=stylesheet href=css/fonts.css?v=d4faf93 /> | |||||
| <!-- App styles --> | |||||
| <link rel=stylesheet href=css/app.css?v=d4faf93 /> | |||||
| <link href=Lumeo.Docs.styles.css rel=stylesheet /> | |||||
| <script type=importmap>{ | |||||
| imports: { | |||||
| ./_framework/blazor.webassembly.js: ./_framework/blazor.webassembly.zxhwjtv6sc.js | |||||
| ./_framework/dotnet.native.js: ./_framework/dotnet.native.15yu4u8ihv.js | |||||
| ./_framework/dotnet.runtime.js: ./_framework/dotnet.runtime.r2kbxkuujc.js | |||||
| ./_framework/dotnet.js: ./_framework/dotnet.yhbg0qfzvu.js | |||||
| } | |||||
| scopes: {} | |||||
| integrity: { | |||||
| ./_framework/blazor.webassembly.js: sha256-1xlHuu1iNOJiMMz2BCq95uekwHf6pdpTcgVNKVBboPs= | |||||
| ./_framework/blazor.webassembly.zxhwjtv6sc.js: sha256-1xlHuu1iNOJiMMz2BCq95uekwHf6pdpTcgVNKVBboPs= | |||||
| ./_framework/dotnet.js: sha256-ygm0zZGACbeRLESIM3TIQgHjeWKOMFcnkZRreY23G/U= | |||||
| ./_framework/dotnet.native.15yu4u8ihv.js: sha256-qzvOAv5J3WbrnpQmSzAZbAih2NlXCpCAU3RsRjsUo+Y= | |||||
| ./_framework/dotnet.native.js: sha256-qzvOAv5J3WbrnpQmSzAZbAih2NlXCpCAU3RsRjsUo+Y= | |||||
| ./_framework/dotnet.runtime.js: sha256-YyudibIWETMKrLb+nAZdJ+xDY7HY6BWf6s32UAdcvCU= | |||||
| ./_framework/dotnet.runtime.r2kbxkuujc.js: sha256-YyudibIWETMKrLb+nAZdJ+xDY7HY6BWf6s32UAdcvCU= | |||||
| ./_framework/dotnet.yhbg0qfzvu.js: sha256-ygm0zZGACbeRLESIM3TIQgHjeWKOMFcnkZRreY23G/U= | |||||
| } | |||||
| }</script> | |||||
| <!-- Algolia search — keys injected at build time by the deploy workflow. --> | |||||
| <meta name=algolia-app-id content=HBUUWJXGUJ /> | |||||
| <meta name=algolia-search-key content=63e2253db63054def494cdb450f4e08d /> | |||||
| </head> | |||||
| <body class=bg-background text-foreground> | |||||
| <div id=app> | |||||
| <div class=lumeo-splash aria-hidden=true role=presentation> | |||||
| <div class=lumeo-splash-inner> | |||||
| <div class=lumeo-splash-logo-wrap> | |||||
| <div class=lumeo-splash-ripple r1></div> | |||||
| <div class=lumeo-splash-ripple r2></div> | |||||
| <div class=lumeo-splash-ripple r3></div> | |||||
| <svg class=lumeo-splash-svg viewBox=0 0 32 32 fill=none xmlns=http://www.w3.org/2000/svg> | |||||
| <circle cx=16 cy=16 r=9 fill=none stroke=currentColor stroke-width=2 class=lumeo-splash-ring /> | |||||
| <circle cx=16 cy=16 r=3 fill=currentColor class=lumeo-splash-dot /> | |||||
| </svg> | |||||
| </div> | |||||
| <div class=lumeo-splash-texts> | |||||
| <span class=lumeo-splash-name>Lumeo</span> | |||||
| <span class=lumeo-splash-tagline>Blazor Component Library</span> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div id=blazor-error-ui data-nosnippet style=display:none; position:fixed; bottom:0; width:100%; background:#ffcccc; padding:0.6rem 1rem; z-index:1000;> | |||||
| An unhandled error has occurred. | |||||
| <a href=. class=reload>Reload</a> | |||||
| <span class=dismiss style=cursor:pointer; margin-left:1rem;>X</span> | |||||
| </div> | |||||
| <!-- theme.js runs synchronously to apply dark/light class before first paint | |||||
| (avoids flash of wrong theme). All other scripts are deferred. --> | |||||
| <script src=_content/Lumeo/js/theme.js></script> | |||||
| <script src=js/docs.js defer></script> | |||||
| <script src=js/nav-scroll.js defer></script> | |||||
| <script src=js/algolia-search.js defer></script> | |||||
| <script src=js/constellation.js defer></script> | |||||
| <script src=_framework/blazor.webassembly.zxhwjtv6sc.js></script> | |||||
| </body> | |||||
| </html> |
Auth-Aware Fetching
Text-based kinds (Markdown, Code, JSON, CSV, Text) are fetched via HttpClient.
For signed-but-not-presigned URLs you have three knobs:
HttpClient="..."— pass a pre-configured client (with handlers attached) directlyConfigureRequest="..."— mutate the outgoingHttpRequestMessageper call (typical forAuthorizationheaders)- Register
HttpClientin DI — picked up automatically (the standard Blazor WASM pattern)
Safety Limits
MaxBytes(default 10 MB) — refused upfront viaContent-Lengthwhen known, truncated mid-stream otherwise. No 100 MB OOMs.MaxCsvRows(default 1000) — CSV parser stops after this many rows and adds a truncation notice.- Markdown is rendered with
.DisableHtml()— raw<script>/<iframe>in user-supplied.mdnever reach the DOM. - SVG renders via
<img>(not inline) — embedded scripts in SVG can't execute. - In-flight fetches are cancelled via
CancellationTokenwhenSrcchanges.
Accessibility
- Body region wears
role="document"with anaria-labelderived from the file name or kind. - Loading + error states announce via the default
Spinner/EmptyStatecomponents (both come pre-wired witharia-live). - Video / audio kinds use native browser controls with their built-in keyboard handling.
- The download anchor is keyboard-reachable with a visible focus ring on the toolbar.