Skip to content

brewkits/hyper_render

Repository files navigation

HyperRender logo

HyperRender

The Flutter HTML renderer that actually works.

pub.dev pub points likes CI License: MIT Flutter

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
CSS Float Demo Ruby Demo Selection Demo
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
Table Demo Comparison Demo Performance Demo
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 float is 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 single RenderObject owns 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.builder chunk 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 pub Convenience wrapper — one dependency, everything included
hyper_render_core pub Core engine — UDT model, CSS resolver, RenderObject, design tokens
hyper_render_html pub HTML + CSS parser
hyper_render_markdown pub Markdown adapter
hyper_render_highlight pub Syntax highlighting for <code> / <pre> blocks
hyper_render_clipboard pub Image copy / share via super_clipboard
hyper_render_devtools pub 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.