hyper_render 1.1.3
hyper_render: ^1.1.3 copied to clipboard
Render HTML/Markdown/Delta at 60 FPS. The only Flutter renderer with CSS float layout, crash-free text selection, and CJK Ruby typography. Drop-in flutter_html alternative.
HyperRender #
The Flutter HTML renderer that actually works. #
60 FPS · 8 MB RAM · CSS float · Ruby typography · XSS-safe by default
CSS float layout · crash-free text selection · CJK/Furigana · @keyframes · Flexbox/Grid
Drop-in replacement for flutter_html and flutter_widget_from_html.
Quick Start · Why Switch? · API · Packages
Demos #
| CSS Float Layout | Ruby / Furigana | Crash-Free Selection |
|---|---|---|
![]() |
![]() |
![]() |
| Text wraps around floated images — no other Flutter HTML renderer does this | Furigana centered above base glyphs, full Kinsoku line-breaking | Select across headings, paragraphs, tables — tested to 100 000 chars |
| Advanced Tables | Head-to-Head | Virtualized Mode |
|---|---|---|
![]() |
![]() |
![]() |
colspan · rowspan · W3C 2-pass column algorithm |
Same HTML in HyperRender vs flutter_widget_from_html | Virtualized rendering — 60 FPS on documents of any length |
Quick Start #
dependencies:
hyper_render: ^1.1.3
import 'package:hyper_render/hyper_render.dart';
HyperViewer(
html: articleHtml,
onLinkTap: (url) => launchUrl(Uri.parse(url)),
)
Zero configuration. XSS sanitization is on by default. Works for articles, emails, docs, newsletters, and CJK content.
Why Switch? The Architecture Argument #
Most Flutter HTML libraries map each HTML tag to a Flutter widget. A 3 000-word article becomes 500+ nested widgets — and some layout primitives simply cannot be expressed that way:
CSS
floatis not possible in a widget tree. Wrapping text around a floated image requires every fragment's coordinates before adjacent text can be composed. That geometry only exists when a singleRenderObjectowns the entire layout.
HyperRender renders the whole document inside one custom RenderObject. Float, crash-free selection, and sub-millisecond binary-search hit-testing all follow from that single design decision.
Feature Matrix #
| Feature | flutter_html |
flutter_widget_from_html |
HyperRender |
|---|---|---|---|
float: left / right |
❌ | ❌ | ✅ |
| Text selection — large docs | ❌ Crashes | ❌ Crashes | ✅ Crash-free |
| Ruby / Furigana | ❌ Raw text | ❌ Raw text | ✅ |
<details> / <summary> |
❌ | ❌ | ✅ Interactive |
CSS Variables var() |
❌ | ❌ | ✅ |
CSS @keyframes |
❌ | ❌ | ✅ |
| Flexbox / Grid | ⚠️ Partial | ⚠️ Partial | ✅ Full |
Box shadow · filter |
❌ | ❌ | ✅ |
SVG <img src="*.svg"> |
⚠️ | ⚠️ | ✅ |
| Scroll FPS (25 K-char doc) | ~35 | ~45 | 60 |
| RAM (same doc) | 28 MB | 15 MB | 8 MB |
Benchmarks #
Measured on iPhone 13 + Pixel 6 with a 25 000-character article:
| Metric | flutter_html |
flutter_widget_from_html |
HyperRender |
|---|---|---|---|
| Widgets created | ~600 | ~500 | 3–5 chunks |
| First parse | 420 ms | 250 ms | 95 ms |
| Peak RAM | 28 MB | 15 MB | 8 MB |
| Scroll FPS | ~35 | ~45 | 60 |
Features #
CSS Float — Magazine Layouts #
HyperViewer(html: '''
<article>
<img src="photo.jpg" style="float:left; width:180px; margin:0 16px 8px 0; border-radius:8px;" />
<h2>The Art of Layout</h2>
<p>Text wraps around the image exactly like a browser — because HyperRender
uses the same block formatting context algorithm.</p>
</article>
''')
Crash-Free Text Selection #
HyperViewer(
html: longArticleHtml,
selectable: true,
showSelectionMenu: true,
selectionHandleColor: Colors.blue,
)
One continuous span tree. Selection crosses headings, paragraphs, and table cells. O(log N) binary-search hit-testing stays instant on 1 000-line documents.
CJK Typography — Ruby / Furigana #
HyperViewer(html: '''
<p style="font-size:20px; line-height:2;">
<ruby>東京<rt>とうきょう</rt></ruby>で
<ruby>日本語<rt>にほんご</rt></ruby>を学ぶ
</p>
''')
Furigana centered above base characters. Kinsoku shori applied across the full line.
Ruby copied to clipboard as 東京(とうきょう).
CSS Variables · Flexbox · Grid #
HyperViewer(html: '''
<style>
:root { --brand: #6750A4; --surface: #F3EFF4; }
</style>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
<div style="background:var(--brand); color:white; padding:16px; border-radius:12px;">
Column one — themed with CSS custom properties
</div>
<div style="background:var(--surface); padding:16px; border-radius:12px;">
Column two — same token system
</div>
</div>
''')
CSS @keyframes Animation #
<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translateY(24px); opacity: 0; }
to { transform: translateY(0); opacity: 1; } }
.hero { animation: fadeIn 0.6s ease-out; }
.card { animation: slideUp 0.4s ease-out; }
</style>
<div class="hero"><h1>Welcome</h1></div>
<div class="card"><p>Animated without any Dart code.</p></div>
Parsed from <style> tags automatically — supports opacity, transform, vendor-prefixed variants,
and percentage selectors.
XSS Sanitization — Safe by Default #
// Safe — strips <script>, on* handlers, javascript: URLs
HyperViewer(html: userGeneratedContent)
// Custom allowlist for stricter sandboxing
HyperViewer(html: userContent, allowedTags: ['p', 'a', 'img', 'strong', 'em'])
// Disable only for fully trusted, internal HTML
HyperViewer(html: trustedCmsHtml, sanitize: false)
Multi-Format Input #
HyperViewer(html: '<h1>Hello</h1><p>World</p>')
HyperViewer.delta(delta: '{"ops":[{"insert":"Hello\\n"}]}')
HyperViewer.markdown(markdown: '# Hello\n\n**Bold** and _italic_.')
Screenshot Export #
final captureKey = GlobalKey();
HyperViewer(html: articleHtml, captureKey: captureKey)
// Export to PNG bytes
final png = await captureKey.toPngBytes();
// Export with custom pixel ratio
final hd = await captureKey.toPngBytes(pixelRatio: 3.0);
Hybrid WebView Fallback #
HyperViewer(
html: maybeComplexHtml,
fallbackBuilder: (context) => WebViewWidget(controller: _webViewController),
)
API Reference #
HyperViewer #
HyperViewer({
required String html,
String? baseUrl, // resolves relative <img src> and <a href>
String? customCss, // injected after the document's own <style> tags
bool selectable = true,
bool sanitize = true,
List<String>? allowedTags,
HyperRenderMode mode = HyperRenderMode.auto, // sync | virtualized | auto
void Function(String)? onLinkTap,
HyperWidgetBuilder? widgetBuilder, // custom widget injection
WidgetBuilder? fallbackBuilder,
WidgetBuilder? placeholderBuilder,
GlobalKey? captureKey,
bool showSelectionMenu = true,
String? semanticLabel,
HyperViewerController? controller,
void Function(Object, StackTrace)? onError,
})
HyperViewer.delta(delta: jsonString, ...)
HyperViewer.markdown(markdown: markdownString, ...)
HyperViewerController #
final ctrl = HyperViewerController();
HyperViewer(html: html, controller: ctrl)
ctrl.jumpToAnchor('section-2'); // scroll to <a name="section-2">
ctrl.scrollToOffset(1200); // absolute pixel offset
Custom Widget Injection #
Replace any HTML element with an arbitrary Flutter widget:
HyperViewer(
html: html,
widgetBuilder: (context, node) {
if (node is AtomicNode && node.tagName == 'iframe') {
return YoutubePlayer(url: node.attributes['src'] ?? '');
}
return null; // fall back to default rendering
},
)
HtmlHeuristics — Introspect Before Rendering #
if (HtmlHeuristics.isComplex(html)) {
// use HyperRenderMode.virtualized for long documents
}
HtmlHeuristics.hasComplexTables(html)
HtmlHeuristics.hasUnsupportedCss(html)
HtmlHeuristics.hasUnsupportedElements(html)
Architecture #
HTML / Markdown / Quill Delta
│
▼
ADAPTER LAYER HtmlAdapter · MarkdownAdapter · DeltaAdapter
│
▼
UNIFIED DOCUMENT TREE BlockNode · InlineNode · AtomicNode
RubyNode · TableNode · FlexContainerNode · GridNode
│
▼
CSS RESOLVER specificity cascade · var() · calc() · inheritance
│
▼
SINGLE RenderObject BFC · IFC · Float · Flexbox · Grid · Table
Canvas painting · continuous span tree
Kinsoku · O(log N) binary-search selection
Key engineering decisions:
- Single RenderObject — float layout and crash-free selection require one shared coordinate system; no widget-tree library can provide this
- O(1) CSS rule lookup — rules are indexed by tag / class / ID; constant time regardless of stylesheet size
- O(log N) hit-testing —
_lineStartOffsets[]precomputed at layout time; each touch is a binary search, not a linear scan - RepaintBoundary per chunk — each
ListView.builderchunk gets its own GPU layer; unmodified chunks are composited, not repainted
When NOT to Use #
| Need | Better choice |
|---|---|
| Execute JavaScript | webview_flutter |
| Interactive web forms / input | webview_flutter |
| Rich text editing | super_editor, fleather |
position: fixed, <canvas>, media queries |
webview_flutter (use fallbackBuilder) |
| Maximum CSS coverage, float/CJK not required | flutter_widget_from_html |
Packages #
| Package | pub.dev | Description |
|---|---|---|
hyper_render |
Convenience wrapper — one dependency, everything included | |
hyper_render_core |
Core engine — UDT model, CSS resolver, RenderObject, design tokens | |
hyper_render_html |
HTML + CSS parser | |
hyper_render_markdown |
Markdown adapter | |
hyper_render_highlight |
Syntax highlighting for <code> / <pre> blocks |
|
hyper_render_clipboard |
Image copy / share via super_clipboard |
|
hyper_render_devtools |
Flutter DevTools extension — UDT inspector, computed styles, demo mode |
Contributing #
git clone https://github.com/brewkits/hyper_render.git
cd hyper_render
flutter pub get
flutter test
dart format --set-exit-if-changed .
flutter analyze --fatal-infos
Read the Architecture Decision Records and Contributing Guide before submitting a PR.
License #
MIT — see LICENSE.






