Skip to content

Repository files navigation

HyperRender logo

HyperRender

The only Flutter HTML renderer with CSS float layout.

pub.devpub pointslikesCILicense: MITFlutter

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

Quick Start · Why Switch? · API · Packages


Demos

CSS Float LayoutRuby / FuriganaCrash-Free Selection
CSS Float DemoRuby DemoSelection Demo
Text wraps around floated images — no other Flutter HTML renderer does thisFurigana centered above base glyphs, full Kinsoku line-breakingSelect across headings, paragraphs, tables — tested to 100 000 chars
Advanced TablesHead-to-HeadVirtualized Mode
Table DemoComparison DemoPerformance Demo
colspan · rowspan · W3C 2-pass column algorithmSame HTML in HyperRender vs flutter_widget_from_htmlVirtualized rendering — only visible sections are built and painted

🚀 Quick Start

dependencies:
hyper_render: ^1.6.0
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

Featureflutter_htmlflutter_widget_from_htmlHyperRender
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:

Metricflutter_htmlflutter_widget_from_htmlHyperRender
Widgets created~600~5003–5 chunks
First parse420 ms250 ms95 ms
Peak RAM28 MB15 MB8 MB
Scroll FPS~35~4560

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; } }
@keyframes flash { from { background-color:#fffbdd; } to { background-color:#ffffff; } }
.hero { animation: fadeIn 0.6scubic-bezier(0.4,0,0.2,1); }
.card { animation: slideUp 0.4s ease-out; }
.tick { animation: flash 1ssteps(4, end); }
</style><divclass="hero"><h1>Welcome</h1></div><divclass="card"><p>Animated without any Dart code.</p></div>

Parsed from <style> tags automatically — supports opacity, transform, color, background-color, vendor-prefixed variants, and percentage selectors.

Timing functions (@keyframes, animation, and transition): linear, ease, ease-in, ease-out, ease-in-out, cubic-bezier(x1, y1, x2, y2), steps(n, start|end), step-start, and step-end. color / background-color are interpolated for both @keyframes and transition.

XSS Sanitization — Safe by Default

// Safe — strips <script>, on* handlers, javascript: URLsHyperViewer(html: userGeneratedContent)
// Custom allowlist for stricter sandboxingHyperViewer(html: userContent, allowedTags: ['p', 'a', 'img', 'strong', 'em'])
// Disable only for fully trusted, internal HTMLHyperViewer(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 bytesfinal 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({
requiredString html,
String? baseUrl, // resolves relative <img src> and <a href>String? customCss, // injected after the document's own <style> tagsbool selectable =true,
bool sanitize =true,
List<String>? allowedTags,
HyperRenderMode mode =HyperRenderMode.auto, // sync | virtualized | paged | autobool enableZoom =false,
voidFunction(String)? onLinkTap,
HyperWidgetBuilder? widgetBuilder, // custom widget injectionWidgetBuilder? fallbackBuilder,
WidgetBuilder? placeholderBuilder,
GlobalKey? captureKey,
bool showSelectionMenu =true,
String? semanticLabel,
HyperViewerController? controller,
HyperPageController? pageController, // paged mode onlyHyperPluginRegistry? pluginRegistry, // custom tag pluginsvoidFunction(Object, StackTrace)? onError,
})
HyperViewer.delta(delta: jsonString, ...)
HyperViewer.markdown(markdown: markdownString, ...)

HyperRenderMode

ValueBehaviour
autoSync for ≤ 10 000 chars, async virtualized otherwise
syncAlways render synchronously in a single scroll view
virtualizedListView.builder — only visible sections built/painted
pagedPageView.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
classMyCardPluginimplementsHyperNodePlugin {
@overrideStringget tagName =>'my-card';
@overrideboolget isInline =>false;
@overrideWidget?build(HyperPluginBuildContext ctx) {
returnCard(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 isAtomicNode&& node.tagName =='iframe') {
returnYoutubePlayer(url: node.attributes['src'] ??'');
}
returnnull; // 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

NeedBetter choice
Execute JavaScriptwebview_flutter
Interactive web forms / inputwebview_flutter
Rich text editingsuper_editor, fleather
position: fixed, <canvas>, media querieswebview_flutter (use fallbackBuilder)
Maximum CSS coverage, float/CJK not requiredflutter_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.
<imgsrc="chart.png" alt="Q3 revenue chart — $2.4M, up 18% YoY"><ahref="/privacy" aria-label="Privacy policy (opens in new tab)">Privacy</a>

📦 Packages

Packagepub.devDescription
hyper_renderpubConvenience wrapper — HTML, Markdown, Delta, syntax highlight
hyper_render_corepubCore engine — UDT model, CSS resolver, RenderObject; zero native deps
hyper_render_htmlpubHTML + CSS parser
hyper_render_markdownpubMarkdown adapter (GFM)
hyper_render_highlightpubSyntax highlighting for <code> / <pre> blocks
hyper_render_devtoolspubFlutter 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.

Packagepub.devDescription
hyper_render_clipboardpubNative image copy / share via super_clipboard
hyper_render_mathpubLaTeX / MathML via flutter_math_fork

hyper_render_clipboard — Native image copy / share

dependencies:
hyper_render_clipboard: ^1.6.0
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.6.0
import'package:hyper_render_math/hyper_render_math.dart';
final registry =HyperPluginRegistry()..register(constMathPlugin());
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.


About

A custom layout engine for Flutter that renders HTML, Markdown, and Quill Delta in a single RenderObject. Supports CSS float, flexbox, grid, CJK Ruby/furigana, and cross-document text selection.

Topics

Resources

Stars

20 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages