https://github.com/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.
https://github.com/brewkits/hyper_render
cjk css css-grid dart document-viewer flexbox float-layout flutter flutter-plugin html-parser html-renderer layout-engine markdown-renderer quill-delta render-engine rich-text ruby-furigana text-selection typography xss-sanitization
Last synced: 4 days ago
JSON representation
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.
- Host: GitHub
- URL: https://github.com/brewkits/hyper_render
- Owner: brewkits
- License: mit
- Created: 2025-12-25T00:02:29.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-04-02T05:18:33.000Z (7 days ago)
- Last Synced: 2026-04-02T17:37:22.489Z (7 days ago)
- Topics: cjk, css, css-grid, dart, document-viewer, flexbox, float-layout, flutter, flutter-plugin, html-parser, html-renderer, layout-engine, markdown-renderer, quill-delta, render-engine, rich-text, ruby-furigana, text-selection, typography, xss-sanitization
- Language: Dart
- Homepage: https://pub.dev/packages/hyper_render
- Size: 26.9 MB
- Stars: 12
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README

# HyperRender
### Fast, robust HTML rendering for Flutter.
[](https://pub.dev/packages/hyper_render)
[](https://pub.dev/packages/hyper_render/score)
[](https://pub.dev/packages/hyper_render/score)
[](https://github.com/brewkits/hyper_render/actions)
[](https://opensource.org/licenses/MIT)
[](https://flutter.dev)
**CSS float · crash-free selection · CJK/Furigana · `@keyframes` · Flexbox/Grid · XSS-safe**
[**Quick Start**](#-quick-start) · [**Why Switch?**](#️-why-switch-the-architecture-argument) · [**API**](#-api-reference) · [**Packages**](#-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
```yaml
dependencies:
hyper_render: ^1.2.2
```
```dart
import 'package:hyper_render/hyper_render.dart';
HyperViewer(
html: articleHtml,
onLinkTap: (url) => launchUrl(Uri.parse(url)),
)
```
Zero configuration. XSS sanitization is **on by default**.
> **Android note:** `hyper_render` depends on `super_clipboard` which transitively pulls in `irondash_engine_context`. That library was compiled against Android SDK 31, but its `androidx.fragment:1.7.1` dependency requires `compileSdk ≥ 34`. Add this one-time workaround to your `android/build.gradle.kts`:
>
> ```kotlin
> // android/build.gradle.kts (root — not app/build.gradle.kts)
> subprojects {
> afterEvaluate {
> extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)?.apply {
> compileSdk = 35
> }
> }
> }
> ```
>
> This overrides `compileSdk` for all library sub-projects so AGP's `checkAarMetadata` passes. Tracked in [#5](https://github.com/brewkits/hyper_render/issues/5).
---
## 🏗️ 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 | ✅ |
| `` / `` | ❌ | ❌ | ✅ Interactive |
| CSS Variables `var()` | ❌ | ❌ | ✅ |
| CSS `@keyframes` | ❌ | ❌ | ✅ |
| Flexbox / Grid | ⚠️ Partial | ⚠️ Partial | ✅ Full |
| Box shadow · `filter` | ❌ | ❌ | ✅ |
| SVG `
` | ⚠️ | ⚠️ | ✅ |
### 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
```dart
HyperViewer(html: '''
The Art of Layout
Text wraps around the image exactly like a browser — because HyperRender
uses the same block formatting context algorithm.
''')
```
### Crash-Free Text Selection
```dart
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
```dart
HyperViewer(html: '''
東京とうきょうで
日本語にほんごを学ぶ
''')
```
Furigana centered above base characters. Kinsoku shori applied across the full line.
Ruby copied to clipboard as `東京(とうきょう)`.
### CSS Variables · Flexbox · Grid
```dart
HyperViewer(html: '''
:root { --brand: #6750A4; --surface: #F3EFF4; }
Column one — themed with CSS custom properties
Column two — same token system
''')
```
### CSS `@keyframes` Animation
```html
@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; }
Welcome
Animated without any Dart code.
```
Parsed from `` tags automatically — supports `opacity`, `transform`, vendor-prefixed variants,
and percentage selectors.
### XSS Sanitization — Safe by Default
```dart
// 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
```dart
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
```dart
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
```dart
HyperViewer(
html: maybeComplexHtml,
fallbackBuilder: (context) => WebViewWidget(controller: _webViewController),
)
```
---
## 📖 API Reference
### `HyperViewer`
```dart
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)
```dart
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
```dart
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`
```dart
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
```dart
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
```dart
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
- **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
- **RepaintBoundary per chunk** — 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` |
---
## ♿ 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.
```html
<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`](https://pub.dev/packages/hyper_render) | [](https://pub.dev/packages/hyper_render) | Convenience wrapper — one dependency, everything included |
| [`hyper_render_core`](https://pub.dev/packages/hyper_render_core) | [](https://pub.dev/packages/hyper_render_core) | Core engine — UDT model, CSS resolver, RenderObject |
| [`hyper_render_html`](https://pub.dev/packages/hyper_render_html) | [](https://pub.dev/packages/hyper_render_html) | HTML + CSS parser |
| [`hyper_render_markdown`](https://pub.dev/packages/hyper_render_markdown) | [](https://pub.dev/packages/hyper_render_markdown) | Markdown adapter (GFM) |
| [`hyper_render_highlight`](https://pub.dev/packages/hyper_render_highlight) | [](https://pub.dev/packages/hyper_render_highlight) | Syntax highlighting for `<code>` / `<pre>` blocks |
| [`hyper_render_clipboard`](https://pub.dev/packages/hyper_render_clipboard) | [](https://pub.dev/packages/hyper_render_clipboard) | Image copy / share |
| [`hyper_render_devtools`](https://pub.dev/packages/hyper_render_devtools) | [](https://pub.dev/packages/hyper_render_devtools) | Flutter DevTools extension — UDT inspector, computed styles |
---
## Contributing
```bash
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](doc/adr/) and [Contributing Guide](doc/CONTRIBUTING.md) before submitting a PR.
---
## License
MIT — see [LICENSE](LICENSE).
---
<div align="center">
[](https://github.com/brewkits/hyper_render)
[pub.dev](https://pub.dev/packages/hyper_render) · [API docs](https://pub.dev/documentation/hyper_render/latest/) · [Changelog](CHANGELOG.md) · [Roadmap](doc/ROADMAP.md)
</div>