Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

brewkits/hyper_render

Open more actions menu

Repository files navigation

HyperRender logo

HyperRender

The only Flutter HTML renderer with CSS float layout.

pub.dev pub points likes CI License: MIT Flutter

CSS float · crash-free selection · CJK/Furigana · @keyframes · 1 646 tests · XSS-safe · Zero Gradle config

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.3.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. No Gradle setup required.


🏗️ 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 architecturally impossible 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 pass.

HyperRender renders the whole document inside one custom RenderObject. CSS float, crash-free selection, O(log N) binary-search hit-testing, and @keyframes animations all follow directly from that single architectural decision.

Feature Matrix

Feature flutter_html flutter_widget_from_html HyperRender
float: left / right
Text selection — large docs ❌ Crashes ❌ Crashes ✅ Crash-free
Ruby / Furigana + Kinsoku ❌ Raw text ❌ Raw text
RTL / BiDi (Arabic, Hebrew) ⚠️ ⚠️
CSS Variables var()
CSS @keyframes animation
Flexbox / Grid ⚠️ Partial ⚠️ Partial ✅ Full
box-shadow · filter
list-style-type (all 11 values) ⚠️ disc only ⚠️ disc only
<details> / <summary> ✅ Interactive
Quill Delta input
Markdown input ✅ GFM
Modular packages ❌ monolith ❌ monolith ✅ opt-in add-ons
Zero Gradle config

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)

Inline SVG note: <svg> and <math> are stripped by default because inline SVG can embed <script> payloads. External SVG via <img src="*.svg"> is fully supported. Add 'svg' to allowedTags only for content you fully control.

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();
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 | paged | auto
  bool enableZoom = false,
  void Function(String)? onLinkTap,
  HyperWidgetBuilder? widgetBuilder,           // custom widget injection
  WidgetBuilder? fallbackBuilder,
  WidgetBuilder? placeholderBuilder,
  GlobalKey? captureKey,
  bool showSelectionMenu = true,
  String? semanticLabel,
  HyperViewerController? controller,
  HyperPageController? pageController,         // paged mode only
  HyperPluginRegistry? pluginRegistry,         // custom tag plugins
  void Function(Object, StackTrace)? onError,
})

HyperViewer.delta(delta: jsonString, ...)
HyperViewer.markdown(markdown: markdownString, ...)

HyperRenderMode

Value Behaviour
auto Sync for ≤ 10 000 chars, async virtualized otherwise
sync Always render synchronously in a single scroll view
virtualized ListView.builder — only visible sections built/painted
paged PageView.builder — one section per page (e-book / reader UI)

HyperPageController (paged mode)

final ctrl = HyperPageController();

HyperViewer(html: html, mode: HyperRenderMode.paged, pageController: ctrl)

ctrl.nextPage(duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
ctrl.animateToPage(2, duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
ctrl.jumpToPage(0);

// Reactive page indicator:
ValueListenableBuilder<int>(
  valueListenable: ctrl.currentPage,
  builder: (_, page, __) => Text('Page ${page + 1} of ${ctrl.pageCount}'),
)

Plugin API — Custom HTML Tags

Register custom tag renderers via HyperPluginRegistry. Two tiers:

  • Block (isInline == false): full-width widget with CSS margins
  • Inline (isInline == true): flows with text; intrinsic size measured automatically
class MyCardPlugin implements HyperNodePlugin {
  @override String get tagName => 'my-card';
  @override bool get isInline => false;

  @override
  Widget? build(HyperPluginBuildContext ctx) {
    return Card(child: Text(ctx.node.textContent));
    // Return null to fall through to default rendering.
  }
}

final registry = HyperPluginRegistry()..register(MyCardPlugin());
HyperViewer(html: '<my-card>Hello</my-card>', pluginRegistry: registry)

HyperViewerController

final ctrl = HyperViewerController();
HyperViewer(html: html, controller: ctrl)

ctrl.scrollToId('section-2');   // scroll to <id="section-2">
ctrl.scrollToOffset(1200);        // absolute pixel offset

Custom Widget Injection

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
  • Single RenderObject — float layout and crash-free selection require one shared coordinate system; a widget tree cannot provide this
  • O(1) CSS rule lookup — rules 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 — unmodified chunks are composited, not repainted; incremental layout caches unchanged sections by content hash
  • 1 646 passing tests — unit, widget, integration, fuzz (43 cases), and golden pixel tests across 3 OS platforms

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

♿ Accessibility (WCAG 2.1 AA)

  • Image alt text (WCAG 1.1.1): <img alt="…"> elements produce a discrete SemanticsNode at the image's layout rect — screen-reader users can navigate to images element-by-element.
  • aria-label on links (WCAG 4.1.2): <a aria-label="…"> uses the attribute value as the accessible label instead of text content.
<img src="chart.png" alt="Q3 revenue chart — $2.4M, up 18% YoY">
<a href="/privacy" aria-label="Privacy policy (opens in new tab)">Privacy</a>

📦 Packages

Package pub.dev Description
hyper_render pub Convenience wrapper — HTML, Markdown, Delta, syntax highlight
hyper_render_core pub Core engine — UDT model, CSS resolver, RenderObject; zero native deps
hyper_render_html pub HTML + CSS parser
hyper_render_markdown pub Markdown adapter (GFM)
hyper_render_highlight pub Syntax highlighting for <code> / <pre> blocks
hyper_render_devtools pub Flutter DevTools extension — UDT inspector, computed styles, float visualizer

Optional add-ons

These packages bring native dependencies and are not bundled by default. Install only what you need.

Package pub.dev Description
hyper_render_clipboard pub Native image copy / share via super_clipboard
hyper_render_math pub LaTeX / MathML via flutter_math_fork

hyper_render_clipboard — Native image copy / share

dependencies:
  hyper_render_clipboard: ^1.3.3
import 'package:hyper_render_clipboard/hyper_render_clipboard.dart';

HyperViewer(
  html: html,
  imageClipboardHandler: SuperClipboardHandler(),
)

Android setup required: super_clipboard transitively pulls in irondash_engine_context, which requires compileSdk ≥ 34. Add this to android/build.gradle.kts (root file, not app/):

subprojects {
    afterEvaluate {
        extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)?.apply {
            compileSdk = 35
        }
    }
}

Tracked in #5.

hyper_render_math — LaTeX / MathML rendering

dependencies:
  hyper_render_math: ^1.3.3
import 'package:hyper_render_math/hyper_render_math.dart';

final registry = HyperPluginRegistry()..register(const MathPlugin());
HyperViewer(html: html, pluginRegistry: registry)

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

See Architecture Decision Records and Contributing Guide before submitting a PR.


License

MIT — see LICENSE.


Morty Proxy This is a proxified and sanitized view of the page, visit original site.