This matrix reports compile-time compatibility: for every component under ui/, whether compileJSX plus each adapter's generate() step produces diagnostics-free output. Compile-time compatibility (compileJSX + adapter generate() diagnostics). NOT render identity — rendered-output parity is owned by the adapter conformance suite and the eval vector corpus (spec/testing.md Layer 3).
62 components × 9 adapters — 558 / 558 cells compile clean
#Matrix
Each component name links to its component page (or its source, for components without one); the Description column says what it is.
| Component | Description | hono | blade | erb | go-template | jinja | minijinja | mojolicious | twig | xslate |
|---|---|---|---|---|---|---|---|---|---|---|
accordion |
A vertically stacked set of interactive headings that each reveal an associated section of content. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
alert |
Displays a callout for important content. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
alert-dialog |
A modal dialog that interrupts the user with important content and expects a response. Unlike… | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
aspect-ratio |
Displays content within a desired ratio. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
avatar |
An image element with a fallback for representing the user. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
badge |
Displays a badge or a component that looks like a badge. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
breadcrumb |
Displays the path to the current resource using a hierarchy of links. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
button |
Displays a button or a component that looks like a button. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
button-group |
Container for grouping related buttons with merged borders and corners. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
calendar |
A date picker component with month navigation and single or range selection. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
card |
Displays a card with header, content, and footer. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
carousel |
A carousel with motion and swipe built on top of Embla Carousel. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
chart |
Every chart component (containers, primitives, and tooltips) is JSX-native: | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
checkbox |
A control that allows the user to toggle between checked and not checked. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
collapsible |
An interactive component which expands/collapses a panel. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
combobox |
Autocomplete input with searchable dropdown. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
command |
A command menu with search, keyboard navigation, and filtering. Use it as an inline menu or inside… | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
context-menu |
A menu triggered by right-click, displayed at the cursor position. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
data-table |
Sortable column headers and pagination controls for data tables. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
date-picker |
A date picker component with calendar popup. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
dialog |
A modal dialog that displays content in a layer above the page. Supports ESC key, overlay click,… | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
direction |
A provider for setting text direction (LTR/RTL) on child content. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
drawer |
A panel that slides in from the edge of the screen, typically from the bottom. Ideal for… | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
dropdown-menu |
A menu of actions triggered by a button. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
empty |
Empty state placeholder with icon, title, and action. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
field |
Form field wrapper with label, description, and error message. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
hover-card |
A floating card that appears on hover to display rich content. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
icon |
SVG icons based on Lucide icon definitions. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
input |
Displays an input field for user text entry. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
input-group |
Input with addons, prefixes, and suffixes. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
input-otp |
An accessible one-time password input component with copy-paste support. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
item |
A generic list/menu item component with composable sub-components for building notification feeds,… | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
kbd |
Displays a keyboard key or keyboard shortcut. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
label |
Renders an accessible label associated with controls. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
menubar |
A visually persistent menu common in desktop applications. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
native-select |
A styled native HTML select element. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
navigation-menu |
A collection of links for navigating websites, with hover-activated content panels. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
pagination |
Pagination with page navigation, next and previous links. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
popover |
A floating panel that appears relative to a trigger element. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
portal |
Renders children into a different part of the DOM tree. Useful for modals, tooltips, and dropdowns. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
progress |
Displays an indicator showing the completion progress of a task. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
radio-group |
A set of checkable buttons where only one can be checked at a time. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
resizable |
Accessible resizable panel groups and layouts with drag and keyboard support. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
scroll-area |
Augments native scroll functionality for custom, cross-browser styling. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
select |
Displays a list of options for the user to pick from, triggered by a button. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
separator |
Visually or semantically separates content. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
sheet |
A panel that slides in from the edge of the screen. Extends the Dialog pattern with side-based… | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
sidebar |
A composable, collapsible sidebar component with responsive mobile support | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
skeleton |
Use to show a placeholder while content is loading. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
slider |
An input where the user selects a value from within a given range. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
slot |
A polymorphic component that merges props with child element | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
spinner |
An animated loading indicator for async operations. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
switch |
A control that allows the user to toggle between checked and not checked. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
table |
A responsive table component with composable sub-components. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
tabs |
A set of layered sections of content—known as tab panels—that are displayed one at a time. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
textarea |
Displays a multi-line text input field. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
toast |
A non-blocking notification that displays brief messages to users. Supports auto-dismiss, multiple… | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
toggle |
A two-state button that can be either on or off. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
toggle-group |
A set of two-state buttons that can be toggled on or off. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
tooltip |
A popup that displays contextual information on hover or focus. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
typography |
Styled text elements for headings, paragraphs, and prose content. | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
xyflow |
Signal-based graph editor — Flow, Background, Controls, MiniMap, Handle, NodeWrapper, SimpleEdge | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
#Legend
Each diagnostic code below links to the known-limitation issue(s) tracking it. Codes with no linked issue fall back to the repo-wide known-limitation label.
No diagnostics were reported — every component compiles cleanly on every adapter.
#Render Conformance (fixture corpus)
The component matrix above is compile-time only. This section answers the question that actually matters: does the construct work, on each adapter's real backend (Go, Ruby, Perl, PHP, Python, Rust), byte-compared against the Hono reference — not just whether it compiles. A construct works either as written (✓) or with a documented /* @client */ comment (✓†, a verified, supported escape — most refusals have one). Fixtures absent from the table below work on every adapter — most as written, some via that documented escape; the headline says how many of each.
The table lists only fixtures that need attention: at least one adapter where nothing works today. Each cell's meaning is in the legend below it; diagnostic codes link to their known-limitation issue, and the reason a code is led by ✗ is in that fixture's detail entry. Escape verification is per adapter, so the same fixture can show ✓† in one column and a code needing attention in another — see escape-coverage.ts.
337 / 345 fixtures work on every adapter (14 of those need a /* @client */ comment on at least one adapter). The 8 below need attention:
| Fixture | hono | blade | erb | go-template | jinja | minijinja | mojolicious | twig | xslate |
|---|---|---|---|---|---|---|---|---|---|
date-method-uncatalogued |
BF021 | BF021 | BF021 | BF021 | BF021 | BF021 | BF021 | BF021 | BF021 |
jsx-element-prop-array |
BF021 | BF021 | BF021 | BF021 | BF021 | BF021 | BF021 | BF021 | BF021 |
jsx-element-prop-fragment-conditional |
✓ | ✓ | ✓ | ≠ | ✓ | ✓ | ✓ | ✓ | ✓ |
jsx-element-prop-ternary |
BF021 | BF021 | BF021 | BF021 | BF021 | BF021 | BF021 | BF021 | BF021 |
preamble-cells |
✓ | ✗BF021 | ✗BF021 | ✗BF021 | ✗BF021 | ✗BF021 | ✗BF021 | ✗BF021 | ✗BF021 |
rich-prop-client-read |
BF049 | BF049 | BF049 | BF049 | BF049 | BF049 | BF049 | BF049 | BF049 |
signal-object-spread-init |
✓ | ✓ | ✓ | ≠ | ✓ | ✓ | ✓ | ✓ | ✓ |
tag-cloud |
✓ | ✗BF021 | ✗BF021 | ✗BF021 | ✗BF021 | ✗BF021 | ✗BF021 | ✗BF021 | ✗BF021 |
✓ Works
✓— as written✓†— with a/* @client */comment (the details below say which diagnostic it silences)
☐ TODO — fix planned, issue linked
≠— compiles, but renders differently from the Hono referenceBF021— a bare diagnostic code: build error, no escape yet
✗ Doesn't work — by design, not a gap
✗BF021— a code led by✗: refused on purpose, no escape will ever be offered (why: in the details below)
The 14 fixtures that need a /* @client */ comment — which diagnostic each silences
every-typeof-predicate— 8 adapters, suppressingBF101fill-unsupported— 8 adapters, suppressingBF101filter-nested-callback-predicate— 6 adapters, suppressingBF101filter-nested-find-predicate— 8 adapters, suppressingBF101filter-typeof-predicate— 8 adapters, suppressingBF021find-typeof-predicate— 8 adapters, suppressingBF101flatmap-typeof-projection— 8 adapters, suppressingBF101map-array-builder-body— 8 adapters, suppressingBF021map-array-builder-escaping— 8 adapters, suppressingBF021reduce-right-typeof-body— 8 adapters, suppressingBF101reduce-typeof-body— 8 adapters, suppressingBF101some-typeof-predicate— 8 adapters, suppressingBF101static-array-from-props— 8 adapters, suppressingBF101static-array-from-props-with-component— 8 adapters, suppressingBF101
#Divergence details
Per adapter, for the 8 fixtures in the table above.
date-method-uncatalogued— Method call on a Date-typed prop refuses with BF021 (no catalogued lowering)blade,erb,go-template,hono,jinja,minijinja,mojolicious,twig,xslate— refused at build time withBF021
jsx-element-prop-array— Array literal wrapping JSX at a non-children prop position refuses with BF021blade,erb,go-template,hono,jinja,minijinja,mojolicious,twig,xslate— refused at build time withBF021
jsx-element-prop-fragment-conditional— Conditional inside a fragment-wrapped non-children prop (#2702)go-template— Go-specific half of the fragment-wrapped-conditional prop shape, tracked as https://github.com/piconic-ai/barefootjs/issues/2703 (distinct from #2702, the cross-adapter hydrate-time bug). #2702 is a hydrate-time-only bug (SSR is correct on every adapter, confirmed for Go by directadapter.generate()inspection of the raw template TEXT, which does contain{{if .Cond}}<a bf-c="^s0">x</a>{{else}}...{{end}}-shaped markup). But the REAL Go render of this exact fixture (go-template-adapter.test.ts, realgo run) produces an EMPTY<header>—<header bf="s1"><!--bf:s0--><!--/--></header>instead of the reference<a>x</a>— i.e. theheaderprop's jsx-children payload never reaches the child (Card) component's slot construction (.CardSlot1) at all for this NAMED-prop shape, even though the same conditional-in-fragment payload renders correctly when passed viachildreninstead (jsx-element-prop-children-escape). Root cause not yet isolated to a specific emitter function — only the reproducible symptom is pinned here. Every other adapter (Hono, ERB, Jinja, Mojolicious, MiniJinja, Twig, Xslate, Blade) renders this fixture correctly; verified by running each adapter's own JSX Conformance Tests for this fixture id.
jsx-element-prop-ternary— Ternary wrapping JSX at a non-children prop position refuses with BF021blade,erb,go-template,hono,jinja,minijinja,mojolicious,twig,xslate— refused at build time withBF021
preamble-cells— keyed .map() row with a preamble-built leaf child — patched in place on same-key update, not frozenblade,erb,go-template,jinja,minijinja,mojolicious,twig,xslate— refused at build time withBF021— no escape owed (✗): By design: this fixture is the SSR-adopting-hydration + patch-on-update regression pin for #2389. A '/* @client */' escape twin would SSR an empty region, eliminating the adopted DOM node the fixture exists to test patching — collapsing it to the already-covered client-only-loop case with zero new coverage. It is also deliberately kept out of integrations/shared/components (per this file's own docstring) because every DSL integration app's bf build compiles that directory wholesale, so no DSL escape variant is wanted there either.
rich-prop-client-read— A Map-typed prop used by client code (even via a method call, since BF021 never sees a handler…blade,erb,go-template,hono,jinja,minijinja,mojolicious,twig,xslate— refused at build time withBF049
signal-object-spread-init— Signal initializer spreads a prop-derived object directly, in value positiongo-template— PRE-EXISTING, unrelated to the #2696 Step 2 spread work this fixture pins: aderived-classified signal/memo whose value is an OBJECT literal has no live-template-expression lowering on Go — unlike the other six template-stash backends (e.g. minijinja emits{% set merged = dict(base, done=true) %}), Go always bakes an object-typed signal/memo field into Go SOURCE atNewXxxPropsconstructor time (convertInitialValue/parsedLiteralToGo), and that baker is STATIC-only (identifier/call/member operands defer,parsed-literal-to-go.ts's own docstring) — it cannot reference a live prop at all. Reproduced identically with the spread REMOVED (createSignal({ id: base.id, done: true })), confirming the gap predates and is independent of spread: the signal seedsniland every field read (.Merged.ID/.Merged.Done) reads the Go zero value regardless ofinitialTodos. Graduate by teaching the baker to emit prop-referencing Go expressions (https://github.com/piconic-ai/barefootjs/issues/2700).
tag-cloud— flatMap block-body tag list — hydration adoption, in-place leaf patch, add, and removeblade,erb,go-template,jinja,minijinja,mojolicious,twig,xslate— refused at build time withBF021— no escape owed (✗): By design: this fixture pins hydration ADOPTING pre-existing SSR flatMap leaves (interaction 1) plus in-place patch/add/remove on top of that adopted DOM. A '/* @client */' escape twin would SSR zero leaves, so there would be nothing to adopt — the adoption regression this fixture exists for would be untestable, collapsing it to the already-covered client-only-loop case. Also deliberately kept out of integrations/shared/components (per this file's own docstring) since every DSL integration app's bf build compiles that directory wholesale, so no DSL escape variant is wanted there either.
#Construct Support (kind × axis)
Construct-level support, derived from the same fixture-granular conformancePins/renderDivergences as the compat matrix. Pins are attributed to a FIXTURE, not to a construct — a gapped fixture is usually a complex component that incidentally exercises many common kinds/axes alongside the one shape it exists to pin down. Read each cell as a pass/total RATIO over the fixtures that exercise that construct, never as a binary verdict: a construct can show a healthy ratio (e.g. 250/253) with a handful of gapped fixtures — those are real, just narrow. Per-fixture detail for a gap lives in the gaps array (and in the full compat matrix, ui/compat.lock.json); the known-limitation label below is the per-issue source of truth for anything without a linked tracking issue.
Read this as a ratio, not a binary. conformancePins and renderDivergences are attributed to a whole fixture (a component), not to the individual construct — a gapped fixture is usually a complex component that happens to exercise many common kinds and axes alongside the one shape it exists to pin down. A construct showing 137/138 has exactly one gapped fixture contributing to it, not 137 broken cases. A construct with total: 0 (currently just regex) has no fixture exercising it yet.
Each construct name links to a conformance fixture exercising it — a small self-contained component showing the case in real code — and the Example column is that fixture's description. Constructs with no covering fixture yet link to where they're registered in the compiler instead.
#By kind
| Construct | Example | hono | blade | erb | go-template | jinja | minijinja | mojolicious | twig | xslate |
|---|---|---|---|---|---|---|---|---|---|---|
array-literal |
[]Client-only directive suppresses SSR for a bare loop, keeping boundary markers |
80/80 | 68/80 (#2320) | 69/80 (#2320) | 68/80 (#2320) | 68/80 (#2320) | 68/80 (#2320) | 69/80 (#2320) | 68/80 (#2320) | 68/80 (#2320) |
array-method |
items.at().at() with no argument returns the first element |
71/71 | 68/71 (known limitation) | 68/71 (known limitation) | 68/71 (known limitation) | 68/71 (known limitation) | 68/71 (known limitation) | 68/71 (known limitation) | 68/71 (known limitation) | 68/71 (known limitation) |
arrow |
(a, b) => a - b.sort((a,b) => a - b) sorts a primitive array by item value |
71/71 | 62/71 (#2320) | 63/71 (#2320) | 62/71 (#2320) | 62/71 (#2320) | 62/71 (#2320) | 63/71 (#2320) | 62/71 (#2320) | 62/71 (#2320) |
binary |
MODE === 'a'Nested ternary chain under a non-reactive (module-const) outer condition (#2470) |
94/94 | 85/94 (#2320, #2321) | 86/94 (#2320, #2321) | 85/94 (#2320, #2321) | 85/94 (#2320, #2321) | 85/94 (#2320, #2321) | 86/94 (#2320, #2321) | 85/94 (#2320, #2321) | 85/94 (#2320, #2321) |
call |
count()Aliased prop's caller-facing key collides with a same-named signal getter (#2525) |
228/229 (#2356) | 214/229 (#2320, #2321, #2356) | 215/229 (#2320, #2321, #2356) | 213/229 (#2320, #2321, #2356) | 214/229 (#2320, #2321, #2356) | 214/229 (#2320, #2321, #2356) | 215/229 (#2320, #2321, #2356) | 214/229 (#2320, #2321, #2356) | 214/229 (#2320, #2321, #2356) |
conditional |
active() ? 2 : 1Ternary expressions in title and data attribute positions |
27/27 | 25/27 (known limitation) | 25/27 (known limitation) | 25/27 (known limitation) | 25/27 (known limitation) | 25/27 (known limitation) | 25/27 (known limitation) | 25/27 (known limitation) | 25/27 (known limitation) |
identifier |
textAliased destructured prop ({ n: count }) keeps the rename (#2524) |
320/323 (#2356, #2667) | 304/323 (#2320, #2321, #2356, #2667) | 305/323 (#2320, #2321, #2356, #2667) | 302/323 (#2320, #2321, #2356, #2667) | 304/323 (#2320, #2321, #2356, #2667) | 304/323 (#2320, #2321, #2356, #2667) | 305/323 (#2320, #2321, #2356, #2667) | 304/323 (#2320, #2321, #2356, #2667) | 304/323 (#2320, #2321, #2356, #2667) |
index-access |
classes[variant]Object literal indexed by a prop, passed to a child component prop |
14/14 | 14/14 | 14/14 | 14/14 | 14/14 | 14/14 | 14/14 | 14/14 | 14/14 |
literal |
trueStatic boolean attributes: bare, ={true}, and ={false} omission |
259/260 (#2667) | 248/260 (#2321, #2667) | 248/260 (#2321, #2667) | 246/260 (#2321, #2667) | 248/260 (#2321, #2667) | 248/260 (#2321, #2667) | 248/260 (#2321, #2667) | 248/260 (#2321, #2667) | 248/260 (#2321, #2667) |
logical |
row.done || props.highlightLoop row preamble computes a conditional-condition value from item + prop |
48/48 | 46/48 (#2321) | 46/48 (#2321) | 46/48 (#2321) | 46/48 (#2321) | 46/48 (#2321) | 46/48 (#2321) | 46/48 (#2321) | 46/48 (#2321) |
member |
props.textThree-level composition threading a prop through each level |
183/186 (#2356, #2667) | 167/186 (#2320, #2321, #2356, #2667) | 168/186 (#2320, #2321, #2356, #2667) | 165/186 (#2320, #2321, #2356, #2667) | 167/186 (#2320, #2321, #2356, #2667) | 167/186 (#2320, #2321, #2356, #2667) | 168/186 (#2320, #2321, #2356, #2667) | 167/186 (#2320, #2321, #2356, #2667) | 167/186 (#2320, #2321, #2356, #2667) |
object-literal |
{ __html: html }dangerouslySetInnerHTML with a prop-derived value renders on Hono/CSR, refuses on template adapters |
63/63 | 60/63 (#2321) | 60/63 (#2321) | 59/63 (#2321) | 60/63 (#2321) | 60/63 (#2321) | 60/63 (#2321) | 60/63 (#2321) | 60/63 (#2321) |
regex |
— | 0/0 | 0/0 | 0/0 | 0/0 | 0/0 | 0/0 | 0/0 | 0/0 | 0/0 |
template-literal |
`${it.id}:${tag}`JSX-returning .flatMap() projection — neutral nested-loop IR on every adapter |
30/30 | 29/30 (known limitation) | 29/30 (known limitation) | 29/30 (known limitation) | 29/30 (known limitation) | 29/30 (known limitation) | 29/30 (known limitation) | 29/30 (known limitation) | 29/30 (known limitation) |
unary |
-1.at(-1) returns the last element |
24/24 | 22/24 (#2320) | 23/24 (known limitation) | 22/24 (#2320) | 22/24 (#2320) | 22/24 (#2320) | 23/24 (known limitation) | 22/24 (#2320) | 22/24 (#2320) |
unsupported |
BF021 escape: /* @client */ with an explicit new Date(...) revival wrapper, hydrate-safe unlike a bare @client call | 40/40 | 30/40 (#2321) | 30/40 (#2321) | 30/40 (#2321) | 30/40 (#2321) | 30/40 (#2321) | 30/40 (#2321) | 30/40 (#2321) | 30/40 (#2321) |
#By axis
| Construct | Example | hono | blade | erb | go-template | jinja | minijinja | mojolicious | twig | xslate |
|---|---|---|---|---|---|---|---|---|---|---|
array-method:at |
items.at().at() with no argument returns the first element |
2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 |
array-method:concat |
left.concat(right).concat(other) merges two arrays in order |
2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 |
array-method:endsWith |
value.endsWith(suffix).endsWith(suffix) renders the matching branch |
2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 |
array-method:flat |
rows.flat().flat() flattens one level |
4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 |
array-method:includes |
items.includes(target).includes(x) on an array prop renders the matching branch |
12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 |
array-method:indexOf |
items.indexOf(target).indexOf(x) returns the 0-based position |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
array-method:join |
items.join().join() with no argument joins with the default comma |
43/43 | 41/43 (known limitation) | 41/43 (known limitation) | 41/43 (known limitation) | 41/43 (known limitation) | 41/43 (known limitation) | 41/43 (known limitation) | 41/43 (known limitation) | 41/43 (known limitation) |
array-method:lastIndexOf |
items.lastIndexOf(target).lastIndexOf(x) returns the position of the last match |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
array-method:padEnd |
value.padEnd(5, '.').padEnd(target, pad) right-pads to the target width |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
array-method:padStart |
value.padStart(5, '0').padStart(target, pad) left-pads to the target width |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
array-method:repeat |
value.repeat(3).repeat(n) concatenates the string n times |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
array-method:replace |
value.replace('o', '0').replace(old, new) swaps the first occurrence |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
array-method:replaceAll |
path.replaceAll('/', ' > ').replaceAll replaces every occurrence, not just the first |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
array-method:reverse |
items.reverse().reverse() emits the array in reverse order |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
array-method:slice |
items.slice().slice() with no argument copies the whole array |
5/5 | 4/5 (known limitation) | 4/5 (known limitation) | 4/5 (known limitation) | 4/5 (known limitation) | 4/5 (known limitation) | 4/5 (known limitation) | 4/5 (known limitation) | 4/5 (known limitation) |
array-method:split |
value.split(',').split(sep) splits a string into an array of substrings |
2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 |
array-method:startsWith |
value.startsWith(prefix).startsWith(prefix) renders the matching branch |
3/3 | 3/3 | 3/3 | 3/3 | 3/3 | 3/3 | 3/3 | 3/3 | 3/3 |
array-method:toFixed |
price.toFixed(2).toFixed(2) formats a numeric prop with rounding and zero-padding |
2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 |
array-method:toLowerCase |
value.toLowerCase().toLowerCase() lowercases the receiver string |
4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 |
array-method:toReversed |
items.toReversed().toReversed() emits the array in reverse order |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
array-method:toUpperCase |
value.toUpperCase().toUpperCase() uppercases the receiver string |
3/3 | 2/3 (known limitation) | 2/3 (known limitation) | 2/3 (known limitation) | 2/3 (known limitation) | 2/3 (known limitation) | 2/3 (known limitation) | 2/3 (known limitation) | 2/3 (known limitation) |
array-method:trim |
value.trim().trim() strips leading and trailing whitespace |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
array-method:trimEnd |
value.trimEnd().trimStart() and .trimEnd() strip only their own side |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
array-method:trimStart |
value.trimStart().trimStart() and .trimEnd() strip only their own side |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
binary:!= |
Nullish coalescing with JSX fallback in JSX child position | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 |
binary:!== |
n() !== 0Comparison operators (>=, ===, !==) as ternary conditions |
9/9 | 9/9 | 9/9 | 9/9 | 9/9 | 9/9 | 9/9 | 9/9 | 9/9 |
binary:* |
count() * 2Derived value with createMemo |
12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 |
binary:+ |
'Hello, ' + nameString concatenation via the + operator |
21/21 | 20/21 (#2321) | 20/21 (#2321) | 20/21 (#2321) | 20/21 (#2321) | 20/21 (#2321) | 20/21 (#2321) | 20/21 (#2321) | 20/21 (#2321) |
binary:- |
a - b.sort((a,b) => a - b) sorts a primitive array by item value |
12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 |
binary:/ |
sortedData().length / pageSizesite/ui DataTable demo — keyed-loop reorder on sort: column-header clicks resort the memo and the runtime reorders rows in place |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
binary:< |
aStr < bStrsite/ui DataTable demo — keyed-loop reorder on sort: column-header clicks resort the memo and the runtime reorders rows in place |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
binary:=== |
MODE === 'a'Nested ternary chain under a non-reactive (module-const) outer condition (#2470) |
49/49 | 41/49 (#2320) | 42/49 (#2320) | 41/49 (#2320) | 41/49 (#2320) | 41/49 (#2320) | 42/49 (#2320) | 41/49 (#2320) | 41/49 (#2320) |
binary:> |
count() > 0Top-level ternary return preserves conditional (#968) |
11/11 | 11/11 | 11/11 | 11/11 | 11/11 | 11/11 | 11/11 | 11/11 | 11/11 |
binary:>= |
n() >= 10Comparison operators (>=, ===, !==) as ternary conditions |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
literal:boolean |
trueStatic boolean attributes: bare, ={true}, and ={false} omission |
55/56 (#2667) | 54/56 (#2667) | 54/56 (#2667) | 52/56 (#2667) | 54/56 (#2667) | 54/56 (#2667) | 54/56 (#2667) | 54/56 (#2667) | 54/56 (#2667) |
literal:null |
Falsy-zero {n && jsx} renders nothing (documented React divergence) |
23/23 | 23/23 | 23/23 | 23/23 | 23/23 | 23/23 | 23/23 | 23/23 | 23/23 |
literal:number |
2Inline SVG: viewBox casing and camelCase → kebab-case presentation attrs |
106/106 | 101/106 (known limitation) | 101/106 (known limitation) | 101/106 (known limitation) | 101/106 (known limitation) | 101/106 (known limitation) | 101/106 (known limitation) | 101/106 (known limitation) | 101/106 (known limitation) |
literal:string |
' 'Explicit {" "} joiners and multi-line text trimming |
183/183 | 175/183 (#2321) | 175/183 (#2321) | 174/183 (#2321) | 175/183 (#2321) | 175/183 (#2321) | 175/183 (#2321) | 175/183 (#2321) | 175/183 (#2321) |
logical:&& |
ready() && visible()Chained a() && b() && |
7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 |
logical:?? |
size ?? 1Nullish coalescing over destructured optional props seeds SSR correctly |
42/42 | 40/42 (#2321) | 40/42 (#2321) | 40/42 (#2321) | 40/42 (#2321) | 40/42 (#2321) | 40/42 (#2321) | 40/42 (#2321) | 40/42 (#2321) |
logical:|| |
row.done || props.highlightLoop row preamble computes a conditional-condition value from item + prop |
14/14 | 14/14 | 14/14 | 14/14 | 14/14 | 14/14 | 14/14 | 14/14 | 14/14 |
lowering:date |
Date-typed prop: toISOString() and getUTCFullYear() render via the catalogued date helper | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 |
lowering:formatDate |
formatDate(date, pattern, timeZone) renders via the catalogued format_date helper | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
lowering:queryHref |
queryHref(base, {…}) lowers to the query helper via the builtin plugin registry | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
lowering:toLocaleDateString |
toLocaleDateString dateStyle options lower to name-token patterns + shipped name tables | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 |
member:computed |
strokePaths['x']site/ui Pagination demo — one signal drives data-active across seven links; href="#" handlers must preventDefault |
8/8 | 8/8 | 8/8 | 8/8 | 8/8 | 8/8 | 8/8 | 8/8 | 8/8 |
member:optional |
user?.nameOptional chaining into an object prop with nullish fallback |
1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
sort-direction:asc |
.sort((a,b) => a - b) sorts a primitive array by item value | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 |
sort-direction:desc |
.sort((a,b) => b.f - a.f) sorts by field, descending | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
sort-key:auto |
.toSorted((a,b) => a.rank > b.rank ? 1 : -1) sorts ascending by rank | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
sort-key:numeric |
.sort((a,b) => a - b) sorts a primitive array by item value | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 |
sort-key:string |
.sort((a,b) => a.price - b.price || a.name.localeCompare(b.name)) sorts by price, then name | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
sort-target:field |
.sort((a,b) => a.f - b.f) sorts by field, ascending | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 |
sort-target:self |
.sort((a,b) => a - b) sorts a primitive array by item value | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
sort:multi-key |
.sort((a,b) => a.price - b.price || a.name.localeCompare(b.name)) sorts by price, then name | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
unary:! |
!accepted()Terms-acceptance form — reactive data-state / aria-checked / disabled from one signal |
16/16 | 14/16 (#2320) | 15/16 (known limitation) | 14/16 (#2320) | 14/16 (#2320) | 14/16 (#2320) | 15/16 (known limitation) | 14/16 (#2320) | 14/16 (#2320) |
unary:- |
-1.at(-1) returns the last element |
11/11 | 11/11 | 11/11 | 11/11 | 11/11 | 11/11 | 11/11 | 11/11 | 11/11 |
#Data Source
This table is generated from the committed ui/compat.lock.json, regenerated with bun run compat:lock and drift-checked in CI. Render-level divergences are declared per adapter in packages/adapter-*/src/render-divergences.ts (each adapter's conformance suite derives its skip list from the same declaration, so this page and the tests cannot drift apart). Tracked limitations carry the known-limitation label.
Component and fixture descriptions are computed into the same lock at regen time (component-docs.ts) — component copy is taken from the public component page's own description (falling back to the ui/registry.json catalogue one-liner, then the component's JSDoc tagline), and the component name links to that page when one exists; fixture descriptions come from the corpus itself. All of it is drift-checked with everything else and can't go stale independently.
The construct-support section above is generated from the committed ui/support-matrix.lock.json, regenerated with bun run support-matrix:lock and drift-checked in CI alongside the component matrix. Each construct's fixture link and example description are computed the same way — the exemplar is mechanically chosen as the most narrowly targeted covering fixture (construct-examples.ts), and definition-site fallback links come from a TS AST walk over the compiler's construct catalogues (construct-source-links.ts) — so they are regenerated and drift-checked on the same schedule: a link can't silently go stale, because moving the code it points at changes what the next regen commits, and CI fails if that regen was forgotten.
Every issue link on this page is freshness-checked on a schedule (bun run compat:issues, the compat-issue-freshness workflow): if a referenced issue is closed while its pin still points at it, an alert issue is opened automatically so the link is re-pointed or removed — the matrix cannot silently link a live limitation to a completed ticket.