GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GitHub - brewkits/hyper_render: 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.

BranchesTags
Open more actions menu

Latest commit

 

History

329 Commits
329 Commits

Folders and files

NameName
Last commit message
Last commit date
.agents
.agents
 
 
.github
.github
 
 
assets
assets
 
 
benchmark
benchmark
 
 
doc
doc
 
 
example
example
 
 
lib
lib
 
 
packages
packages
 
 
scripts
scripts
 
 
test
test
 
 
.gitignore
.gitignore
 
 
.metadata
.metadata
 
 
.pubignore
.pubignore
 
 
CHANGELOG.md
CHANGELOG.md
 
 
LICENSE
LICENSE
 
 
MIGRATION.md
MIGRATION.md
 
 
README.md
README.md
 
 
ROADMAP.md
ROADMAP.md
 
 
analysis_options.yaml
analysis_options.yaml
 
 
pubspec.yaml
pubspec.yaml
 
 
pubspec_publish_ready.yaml
pubspec_publish_ready.yaml
 
 
release_notes.txt
release_notes.txt
 
 

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 · AI/LLM streaming · CJK/Furigana · @keyframes · 2 490+ tests · XSS-safe · Zero Gradle config


Try Live Web Demo


Live Web Playground · Quick Start · AI Streaming · 30s Migration · Why Switch? · API · Packages


⚡ 30-Second Drop-in Migration from flutter_html

Already using flutter_html? You don't need to rewrite your widget tree or learn a new API. Just change your single import:

// 1. In your pubspec.yaml:
// dependencies:
//   hyper_render: ^1.13.1

// 2. In your Dart file — replace this single line:
// ❌ import 'package:flutter_html/flutter_html.dart';
import 'package:hyper_render/compat/flutter_html.dart';

// 3. Your existing code works out of the box — rendered by one RenderObject
//    instead of hundreds of nested widgets (see "Why Switch?" below).
Html(
  data: '<h1>Hello</h1><p>Text wraps seamlessly around floats!</p>',
  onLinkTap: (url, attributes, element) => launchUrl(Uri.parse(url!)),
)

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 — only visible sections are built and painted

🚀 Quick Start

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


🤖 AI / LLM Real-Time Streaming

Render live streaming token feeds from Google Gemini, OpenAI ChatGPT, Anthropic Claude, or WebSocket backends with frame-aligned, adaptively-throttled updates and automatic transient syntax repair.

final controller = HyperStreamingController();

// Bind directly to any Dart Stream (e.g. OpenAI / Gemini SDK):
controller.bindStream(aiTokenStream);

// Render with automatic stick-to-bottom auto-scroller and pulsing caret:
HyperViewer.streaming(
  streamingController: controller,
  contentType: HyperContentType.markdown,
  showTypingCaret: true,
  caretStyle: HyperTypingCaretStyle.bar,
  autoRepairSyntax: true,   // Auto-closes incomplete ```, **, $$, | on the fly
  autoScrollToBottom: true, // Smoothly tracks stream tail
)

🏗️ 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 ❌ ❌ ✅
AI / LLM Streaming ❌ ❌ ✅ Frame-aligned, adaptive throttle
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 ✅ Wrapping flex on a custom RenderObject¹
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 ✅ ✅ ✅

¹ flex-wrap: wrap on a row container performs real CSS line packing and distributes free space by flex-grow, honouring flex-basis / min-width / max-width including their % forms. Known gaps: align-content is not applied, flex-basis does not drive the nowrap path, and flex-direction: column + wrap packs lines without growth. Per-property status: CSS_PROPERTIES_MATRIX.md.

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

Responsive wrapping cards — flex-wrap: wrap packs items into lines and shares each line's free space by flex-grow, so the same markup a browser gets works here:

HyperViewer(html: '''
  <div style="display:flex; flex-wrap:wrap; gap:14px;">
    <div style="flex:1 1 220px; min-width:220px;">Card 1</div>
    <div style="flex:1 1 220px; min-width:220px;">Card 2</div>
    <div style="flex:1 1 220px; min-width:220px;">Card 3</div>
  </div>
''')

CSS custom properties and 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 custom properties work in <style> blocks and customCss, not just inline (since 1.10.0), and url() / calc() too:

HyperViewer(
  html: '<p>Themed text</p>',
  customCss: ':root { --brand: #6750A4; } p { color: var(--brand); padding: calc(8px * 2); }',
)

DevTools — Inspect, Profile, Live-Edit

Add hyper_render_devtools to inspect any HyperViewer in Flutter DevTools: the UDT tree and computed styles, layout/paint timing per renderer (each virtualized chunk is one), a selection debugger (fragment boundaries, ruby offsets), live --var editing that re-renders instantly, and a JSON snapshot export.

void main() {
  assert(() { HyperRenderDevtools.register(); return true; }()); // debug only
  runApp(const MyApp());
}

Dark Mode

Under a dark Theme, unstyled text uses colorScheme.onSurface and re-resolves when the theme toggles (scroll position kept). Elements that paint their own light background (<blockquote>, <kbd>, <th>, style="background:#eee") keep readable text automatically.

// A surface that stays light under a dark theme (an email pane, a paper page):
HyperViewer(html: html, textColor: Colors.black87)

Links, <h6>, <code> / <pre> and <mark> switch to dark-surface variants (at least 4.5:1 against #121212) whenever the effective text color is light — a dark Theme, or a light textColor. A dark Theme with a dark textColor (a white email pane) keeps the light-surface palette. Your own CSS (a { color }, inline style) still wins. <blockquote>, <kbd> and table headers keep their light backgrounds; their text stays readable but they remain light blocks on a dark page.

textColor wins over the content's own html / :root / body color (so an app can force a reader theme over a publisher stylesheet) but not over an element's own color. body { color } and html { color } are honoured; other body properties are not.

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.6s cubic-bezier(0.4, 0, 0.2, 1); }
  .card { animation: slideUp 0.4s ease-out; }
  .tick { animation: flash 1s steps(4, end); }
</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, 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: 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,         // lower priority than the document's own <style> tags (they win at equal specificity; use !important to force)
  Color? textColor,          // document text color; wins over html/body/:root color, not over element colors
  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: (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
  • 2 495 passing tests — unit, widget, integration, fuzz (339 seeded-mutation cases), plus 28 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, layout fragments, layout/paint timeline, selection debugger, live CSS-variable editing, JSON snapshot export

Optional add-ons

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

Package pub.dev Description
hyper_render_epub pub Full EPUB 2/3 container reader with chapter pagination & in-memory zip decoding
hyper_render_clipboard pub Native image copy / share via super_clipboard
hyper_render_math pub LaTeX / MathML via flutter_math_fork

hyper_render_epub — EPUB 2/3 Reader Container

dependencies:
  hyper_render_epub: ^0.1.3
import 'package:hyper_render_epub/hyper_render_epub.dart';

// 1. Open the .epub (any Uint8List: File, asset, network)
final book = await EpubBook.open(epubBytes);

// 2. A controller holds the position; dispose() it when done
final controller = EpubReaderController(book: book);

// 3. Render the current chapter; drive next()/previous()/goTo() from your own UI
EpubReader(
  controller: controller,
  textColor: null, // null follows the theme; set it for a sepia/paper page
)

hyper_render_clipboard — Native image copy / share

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

// HyperViewer has no clipboard parameter: hand images to HyperImage yourself.
HyperViewer(
  html: html,
  widgetBuilder: (node) {
    if (node is AtomicNode && node.tagName == 'img' && node.src != null) {
      return HyperImage(src: node.src!, clipboardHandler: SuperClipboardHandler());
    }
    return null;
  },
)

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.7.0
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.


GitHub stars

pub.dev · API docs · Changelog · Roadmap

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.

pub.dev/packages/hyper_render

Topics

cjkcsscss-griddartdocument-viewerflexboxfloat-layoutflutterflutter-pluginhtml-parserhtml-rendererlayout-enginemarkdown-rendererquill-deltarender-enginerich-textruby-furiganatext-selectiontypographyxss-sanitization

Resources

Readme
MIT license
Activity
Custom properties

Stars

26 stars

Watchers

2 watching

Forks

6 forks
Report repository

Releases

Used by

Contributors

Languages

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.