veneer 0.14.0 copy "veneer: ^0.14.0" to clipboard
veneer: ^0.14.0 copied to clipboard

Real native iOS 26 UI for Flutter: Liquid Glass, UIKit tab and navigation bars, composers and sheets driven by Flutter layout, with matching Flutter UI on Android.

Veneer #

pub package CI License: MIT

Real native iOS 26 UI for Flutter apps — Liquid Glass, UIKit bars, native composers and sheets — driven by your Flutter widget tree, with matching Flutter UI on Android and older iOS.

Demo: native tab bar and composer, glass tabs, and an assistant in a native UIKit sheet
The example app on the iOS 27 simulator, real time. Full-quality video.

Native tab bar, navigation bar and composer over a Flutter chat Assistant in a native UIKit sheet with a native prompt composer The sheet at its medium detent, in Liquid Glass Voice mode: glass buttons split off the composer

Veneer puts one native overlay above the Flutter surface and keeps it in sync with Flutter layout every frame. The tab bar is a real UITabBarController, the navigation bar is a real UINavigationBar, sheets are real UISheetPresentationController sheets, and glass is real UIGlassEffect refracting your Flutter pixels, with UIKit's own animations, gestures and accessibility. There are no platform views per widget, so separate glass widgets can merge and morph into each other the way native glass does.

Everything is a regular Flutter widget. On iOS 26 and later it renders natively; on Android and iOS 15–25 the same widget tree renders Flutter replicas with the same layout, sizes and behaviour, so one codebase serves every platform.

Status: pre-release (0.1). The API may still change. It has been verified on the iOS 26/27 simulator and the Android emulator; real-device performance measurements are still to come.

Contents #

Features #

Tab bar A real UITabBarController: the floating Liquid Glass bar, selection morph, badges and the split layout's detached trailing button. Slides away when another screen covers it.
Navigation bar A real UINavigationBar with glass bar buttons, a title capsule, grouped trailing buttons, native menus and iOS 26's scroll edge effect over Flutter content.
Composer A messaging composer: a glass capsule that morphs into an expanded card above the keyboard, inside the keyboard's own animation, with interactive keyboard dismissal, mentions and Slack-style voice recording.
Prompt composer An assistant-style composer: grows from a capsule into a card for long prompts and attachments, turns its voice button into send as you type, and splits glass buttons off for a voice session.
Sheets Real UIKit sheets hosting Flutter content: detents, grabber, Liquid Glass at partial heights, the page behind receding — with Veneer's bars and composers inside.
Menus Native UIMenus from bar buttons, composer buttons and glass shapes, and UIKit context menus on long-pressed Flutter widgets.
Glass shapes UIGlassEffect shapes laid out by Flutter, merging within groups, clipped by Flutter clips, interactive.
Icons SF Symbols, any IconData (Material, Cupertino, custom and variable icon fonts), SVG and images — all rendered natively.

Platform support #

Platform What renders
iOS 26+ The native layer: UIKit bars, sheets and composers, Liquid Glass, scroll edge effects
iOS 15–25, Android Flutter replicas of the same UI, matching layout, sizes, positions, colours and corner radii, with solid surfaces instead of glass
Web, desktop Not supported (the bridge uses dart:ffi)

Veneer.isSupported tells you which path is active. You rarely need it: every widget picks its own path, so app code needs no Platform.isIOS checks.

Installation #

flutter pub add veneer

Or track the repository directly:

dependencies:
  veneer:
    git:
      url: https://github.com/troyvnit/veneer.git
      ref: main

Requirements:

  • Flutter 3.47 or later (Dart 3.13).
  • iOS deployment target 15.0 or later. The native layer turns on at runtime on iOS 26+. Build with Xcode 26 or later so the iOS 26 SDK is available.
  • Works with CocoaPods and Swift Package Manager. No Android setup.

Quick start #

Wrap your pages in a NativeChromeScope for the tab bar, and register the navigator observer so native chrome steps aside for Flutter dialogs and bottom sheets:

import 'package:veneer/veneer.dart';

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    navigatorObservers: [VeneerNavigatorObserver()],
    home: const Home(),
  );
}

class Home extends StatefulWidget {
  const Home({super.key});

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int _tab = 0;

  @override
  Widget build(BuildContext context) => NativeChromeScope(
    tabs: const [
      NativeTabItem(title: 'Home', icon: NativeIcon.symbol('house'), selectedIcon: NativeIcon.symbol('house.fill')),
      NativeTabItem(title: 'Inbox', icon: NativeIcon.symbol('tray'), badge: '3'),
    ],
    selectedIndex: _tab,
    onTabSelected: (i) => setState(() => _tab = i),
    child: IndexedStack(index: _tab, children: const [HomePage(), InboxPage()]),
  );
}

Pages add a navigation bar by wrapping their content. Content gets MediaQuery padding for the bars, so it starts below them and scrolls underneath:

class InboxPage extends StatelessWidget {
  const InboxPage({super.key});

  @override
  Widget build(BuildContext context) => Scaffold(
    body: NativeNavigationBar(
      title: const NativeBarTitle(title: 'Inbox'),
      trailing: [NativeBarButton(icon: const NativeIcon.symbol('square.and.pencil'), title: 'New', onPressed: () {})],
      child: Builder(
        builder: (context) => ListView(padding: MediaQuery.paddingOf(context), children: const [/* … */]),
      ),
    ),
  );
}

Components #

Tab bar #

NativeChromeScope(
  tabs: const [
    NativeTabItem(title: 'Chat', icon: NativeIcon.symbol('bubble.left'), selectedIcon: NativeIcon.symbol('bubble.left.fill')),
    NativeTabItem(title: 'Library', icon: NativeIcon.icon(Icons.photo_library_outlined), badge: '3'),
  ],
  trailingAction: NativeTabAction(icon: NativeIcon.symbol('sparkles'), title: 'Assistant', onPressed: openAssistant),
  scrollEdgeEffect: NativeScrollEdgeEffect.soft,
  selectedIndex: index,
  onTabSelected: (i) => setState(() => index = i),
  child: pages,
)
  • A real UITabBarController, so the glass bar, selection animation, badges, VoiceOver and the large content viewer are UIKit's. Icons, titles and badges update in place.
  • Split layout: trailingAction becomes the detached glass button at the trailing edge. It acts as a button by default; with selectable: true it's a tab reported as index tabs.length.
  • Up to 5 items including the trailing button (UIKit's limit on iPhone before a "More" tab).
  • Slides away while another route covers its page, and while a Flutter popup shows (with VeneerNavigatorObserver).
NativeNavigationBar(
  leading: NativeBarButton(icon: const NativeIcon.symbol('chevron.left'), title: 'Back', onPressed: pop),
  title: NativeBarTitle(title: 'launch-crew', subtitle: '6 members', icon: const NativeIcon.symbol('lock.fill'), capsule: true),
  trailing: [
    NativeBarButton(icon: const NativeIcon.symbol('headphones'), title: 'Huddle', onPressed: huddle),
    NativeBarButton(icon: const NativeIcon.symbol('ellipsis'), title: 'More', menu: moreItems),
  ],
  child: page,
)
  • A real UINavigationBar: 44 pt glass buttons, and adjacent trailing buttons sharing one glass capsule. When its buttons change, UIKit morphs the glass between the old and new sets.
  • NativeBarTitle(capsule: true) shows a tappable glass capsule with an icon, title, subtitle and optional accessory (such as a chevron), like a channel header; otherwise a plain title, centred or leading (alignment: NativeBarTitleAlignment.leading). With fill: true the capsule spans the space between the buttons (a chat header), and iconSize fits an avatar-sized icon such as a group avatar. Pass style and subtitleStyle to use your app's fonts and colours; the native bar loads the font family from your Flutter assets.
  • Bar buttons can show a badge (a count, text, or '' for a dot), use prominent tinted glass, show text instead of an icon, and split into separate capsules with group.
  • Scroll edge effect: content dissolves into iOS 26's own progressive blur under the bar (scrollEdgeEffect, soft by default).
  • Shown while its page is visible; the most recently shown bar wins.

Composer #

A messaging composer for chat screens:

NativeComposer(
  controller: composer,
  placeholder: 'Message launch-crew',
  leading: NativeComposerButton(icon: const NativeIcon.symbol('plus'), title: 'Attach', onPressed: attach),
  idleAction: NativeComposerButton(icon: const NativeIcon.symbol('mic'), title: 'Voice message'),
  toolbar: [
    NativeComposerButton(icon: const NativeIcon.symbol('textformat'), title: 'Formatting'),
    NativeComposerButton(icon: const NativeIcon.symbol('face.smiling'), title: 'Emoji'),
  ],
  onSend: send,
  child: ListView(reverse: true, children: messages),
)
  • Idle, a 44 pt glass capsule above the tab bar. Focused, it morphs into a card above the keyboard with a toolbar row and send button, inside the keyboard's own animation.

  • The text view grows to maxLines, then scrolls. NativeComposerController reads and sets the text and focus.

  • Interactive keyboard dismissal: dragging the content down pulls the keyboard with the finger, using UIKit's own mechanism, while Flutter keeps scrolling.

  • Use it in a Scaffold(resizeToAvoidBottomInset: false): it handles the keyboard and pads child for the space it takes.

  • Mentions: controller.selection follows the caret, controller.replaceRange(start, end, '@Name ') completes the mention being typed, and highlights: ['@Name'] draws mentions in the tint colour.

  • Voice recording (both composers), as in Slack:

    NativePromptComposer(
      controller: composer,
      actions: [
        NativeComposerButton(
          icon: const NativeIcon.symbol('mic'),
          title: 'Record',
          onPressed: () => composer.startVoiceRecording(maxDuration: const Duration(minutes: 5)),
        ),
      ],
      onVoiceRecorded: (clip) => attach(clip.path, clip.duration),
      onVoiceRecordingFailed: (reason) => recordWithFlutter(),
      recordingCancelLabel: 'Cancel',
      recordingDoneLabel: 'Done',
      child: conversation,
    )
    

    The composer becomes a recording bar with cancel, a live waveform, the elapsed time and done. Recording is native: an AAC .m4a file in the temporary directory, which your app owns from then on. Your app needs an NSMicrophoneUsageDescription. The Flutter fallback reports NativeVoiceRecordingFailure.unavailable.

Prompt composer #

An assistant-style composer:

Multi-line prompt with a photo attachment Native menu from the composer's + button

NativePromptComposer(
  controller: composer,
  placeholder: 'Ask anything',
  leading: NativeComposerButton(icon: const NativeIcon.symbol('plus'), title: 'Add', menu: [
    NativeMenuItem(title: 'Photos', icon: const NativeIcon.symbol('photo.on.rectangle'), onSelected: pickPhoto),
    NativeMenuItem(title: 'Files', icon: const NativeIcon.symbol('doc'), onSelected: pickFile),
  ]),
  actions: [NativeComposerButton(icon: const NativeIcon.symbol('mic'), title: 'Dictate', onPressed: dictate)],
  primaryAction: NativeComposerButton(
    icon: const NativeIcon.symbol('waveform'),
    title: 'Voice mode',
    onPressed: () => setState(() => voice = true),
  ),
  sideActions: [
    NativeComposerButton(icon: const NativeIcon.symbol('mic.slash'), title: 'Mute', onPressed: toggleMute),
    NativeComposerButton(
      icon: const NativeIcon.symbol('xmark'),
      title: 'End',
      prominent: true,
      onPressed: () => setState(() => voice = false),
    ),
  ],
  showSideActions: voice,
  attachments: [
    NativeComposerAttachment(id: 'p1', thumbnail: NativeIcon.imageFile(photoPath), onRemove: () => remove('p1')),
  ],
  onSend: send,
  child: conversation,
)
  • One line (48 pt): leading button, text, inline actions and a filled circle that shows primaryAction while empty and becomes send once there's text or an attachment.
  • Multi-line: when the text wraps, has a line break or there are attachments, the capsule grows into a card with attachments on top and the buttons on a bottom row.
  • Side actions: glass circles that split off the capsule like liquid while showSideActions is true (for example a voice session's mute and end buttons).
  • Attachments: image tiles (no title) or file chips, with remove buttons; loading dims a tile and shows a spinner while it uploads, and onTap makes the tile itself tappable (to preview or play it).
  • States: stopAction replaces the circle while a reply is generating, sendEnabled and sendBusy hold or spin the send button, and editable: false shows text (such as a live transcript) without taking the keyboard.
  • Idle, it sits concentric with the display's corners; focused, it rides the keyboard.

Sheets #

Real UIKit sheets for Flutter content:

Sheet at the large detent Sheet at the medium detent

// main.dart: the sheet's Flutter app
@pragma('vm:entry-point')
void assistantSheet() => runNativeSheet(
  MaterialApp(theme: appTheme, home: const AssistantPage()),
);

// Warm it up early so the sheet opens with content:
NativeSheet.prewarm('assistantSheet');

// Present it:
showNativeSheet<void>(
  context: context,
  entrypoint: 'assistantSheet',
  detents: const [NativeSheetDetent.medium, NativeSheetDetent.large],
  initialDetent: NativeSheetDetent.large,
  builder: (context) => const AssistantPage(), // Android / iOS 15–25
);

// Inside the sheet:
NativeSheet.close(context, result);
  • On iOS 26 this presents a FlutterViewController as a .pageSheet, so detents, the grabber, drag physics, dimming, the corner radius, Liquid Glass at partial heights and the page behind receding are all UIKit's.
  • A Flutter engine renders into one view at a time, so the sheet runs its own engine, started at entrypoint (a top-level function in main.dart, or in libraryUri) and spawned from a shared FlutterEngineGroup, which shares compiled code and the GPU context. The next engine is warmed automatically once a sheet has slid up, and after it closes.
  • Each sheet engine registers all of your app's plugins unless you choose them: set VeneerPlugin.sheetPluginRegistrant in your app delegate to register only what sheets use, so plugins that observe the app (calls, push, analytics) don't run twice.
  • The sheet's isolate doesn't share state with your app: pass a payload in (read it with NativeSheet.payload(); pre-warmed engines receive it when presented) and return a result with NativeSheet.close. arguments reach the entrypoint instead, but bypass pre-warming.
  • While it's open, the sheet can ask the app for more with NativeSheet.request(name, arguments) — a fresh credential, or data only the app holds. The app answers with the handler it set through NativeSheet.setRequestHandler((name, arguments) async => …); its value (or error, as a PlatformException) comes back to the sheet. In the Flutter fallback the handler is called directly, so the same code runs everywhere. A sheet presented from inside a sheet asks that sheet's engine, which can forward the request with a handler of its own.
  • The app can talk to an open sheet too: give showNativeSheet a NativeSheetController, then controller.send(message) reaches the sheet's NativeSheet.messages(context) stream, and controller.close(result) dismisses it. Together with requests, a sheet can stay a view of state the app owns: the payload is its first state, requests carry the user's actions to the app, and the app's answers and messages bring fresher state back.
  • Sheet engines never restyle the app's status bar, and each engine is torn down when its sheet is dismissed — unless the sheet was shown with keepAlive: true: then closing only hides it. Its app keeps running (uploads, polling, audio; no frames while hidden), the next showNativeSheet at that entrypoint presents the same app again, and inside it NativeSheet.shown and NativeSheet.presented report each hide and reopening (with its payload). NativeSheet.release(entrypoint) frees it, for example on sign-out.
  • A sheet's engine can call showNativeSheet itself: the new sheet is presented natively over it, and goes away with it.
  • Plugin UI (contact pickers, calendar editors, share sheets) appears over an open sheet even when the plugin presents from the window's root view controller: Veneer sends a presentation UIKit would refuse there to the sheet on top. VeneerPlugin.forwardsPresentationsOverSheets turns it off.
  • For a NativeSheetDetent.content() detent, wrap the sheet's content in NativeSheetContent: UIKit sizes the sheet to it.
  • Inside, a NativeNavigationBar is a real UINavigationBar placed per Apple's sheet templates (16 pt from the sheet's edges), and composers ride the sheet natively. Pulling down drags the sheet when the content under the finger is at its top edge; otherwise the content scrolls, as in UIKit.
  • Give the sheet page's Scaffold a transparent background so the sheet's material shows.
  • Without an entrypoint, or off iOS 26, builder runs in a Flutter sheet with the same detents, grabber, iOS 26 look and drag physics. NativeSheetRoute pushes that Flutter sheet directly, and NativeSheetDetent.content() sizes it to its content (native sheets use large).
  • The sheet's content gets the safe area a UIKit sheet gives it: the top clears the grabber, the bottom clears the home indicator, and the keyboard moves the sheet to its largest detent.
NativeBarButton(
  icon: const NativeIcon.symbol('ellipsis'),
  title: 'More',
  menu: [
    NativeMenuItem(title: 'Share', icon: const NativeIcon.symbol('square.and.arrow.up'), onSelected: share),
    NativeMenuItem(title: 'Delete', icon: const NativeIcon.symbol('trash'), destructive: true, onSelected: delete),
  ],
)

menu on NativeBarButton, NativeComposerButton and GlassShape opens a native UIMenu that grows out of the button. NativeMenuItem.divider() separates sections. An item can carry a subtitle, a checkmark (checked, the current choice in a picker) or be dimmed (enabled: false). Off iOS 26 a Flutter menu with iOS metrics opens instead.

Menus from a Flutter-drawn trigger — a text row with a chevron, a filled pill:

NativeMenuAnchor(
  items: [
    for (final status in statuses)
      NativeMenuItem(title: status.label, checked: status == current, onSelected: () => pick(status)),
  ],
  child: statusRow,
)

A tap on the child opens the native UIMenu growing out of it. An invisible native button (GlassShape(style: GlassStyle.none)) sits over the child and takes the tap, so the child draws the trigger but gets no touches of its own. Off iOS 26 a tap opens the Flutter menu next to it.

Context menus on any Flutter widget:

NativeContextMenu(
  borderRadius: BorderRadius.circular(18),
  items: [
    NativeMenuItem(title: 'Reply', icon: const NativeIcon.symbol('arrowshape.turn.up.left'), onSelected: reply),
    NativeMenuItem(title: 'Delete', icon: const NativeIcon.symbol('trash'), destructive: true, onSelected: delete),
  ],
  child: bubble,
)

A long press opens UIKit's context menu. The widget lifts over a blurred backdrop, with the press shrink, haptics and the settle back. UIKit recognizes the press on the Flutter view against where Flutter laid the widget out that frame, so it follows scrolling, and a press that moves still scrolls. The preview is a snapshot of the widget clipped to borderRadius. Off iOS 26 a long press opens the Flutter menu next to the widget.

Glass shapes #

Stack(children: [
  ListView(children: [for (final item in items) GlassShape(height: 56, label: item.name, icon: item.icon)]),
  GlassGroup(
    zIndex: 1,
    child: Row(children: [
      GlassShape(width: 56, height: 56, icon: const NativeIcon.symbol('plus'), onTap: add),
      GlassShape(width: 56, height: 56, icon: const NativeIcon.symbol('ellipsis'), menu: moreItems),
    ]),
  ),
])
  • GlassShape is native UIGlassEffect positioned by Flutter layout every frame; its content (icon, label) is native too. With onTap it uses UIKit's interactive glass response.
  • Shapes in the same GlassGroup merge within the group's spacing and morph out of their neighbours. Groups stack by zIndex; shapes outside a group join their route's group.
  • Glass honours Flutter clips (ClipRect, ClipRRect, ClipOval, scroll viewports).
  • Glass always draws above Flutter content: Flutter widgets placed under a shape are refracted by it.

Glass behind Flutter content — for surfaces whose content has to stay Flutter:

GlassBackground(
  cornerRadius: 28,
  child: Padding(padding: const EdgeInsets.all(12), child: dockContent),
)

GlassBackground is a platform view (UIVisualEffectView + UIGlassEffect) that Flutter composites under its child, so the child can be any widget. It costs more than a shape; use GlassShape when the content can be native. With onTap the glass is interactive and takes the tap itself (give the child IgnorePointer). Off iOS 26 it is the replica surface.

Search field #

final search = GlobalKey<NativeSearchFieldState>();

NativeSearchField(
  key: search,
  placeholder: 'Search all emoji',
  controller: queryController,
  onChanged: (query) => setState(() => this.query = query),
  onSubmitted: (_) => search.currentState?.unfocus(),
)
  • A real UISearchTextField inside a glass capsule, positioned by Flutter layout like a GlassShape: UIKit draws the magnifier, placeholder, clear button and caret, and owns the keyboard.
  • Text flows both ways through the controller: typing updates it, and setting controller.text (e.g. clearing it) updates the native field. onChanged, onSubmitted and onFocusChanged report the user's edits; NativeSearchFieldState.focus() / unfocus() move the keyboard.
  • Off iOS 26 it renders a Flutter field of the same height on the fallback surface.

Segment bar #

GlassSegmentBar(
  segments: const [
    GlassSegment(icon: NativeIcon.symbol('clock'), label: 'Recent'),
    GlassSegment(icon: NativeIcon.symbol('face.smiling'), label: 'Smileys'),
    GlassSegment(icon: NativeIcon.symbol('leaf'), label: 'Nature'),
  ],
  selectedIndex: section,
  onSelected: jumpToSection,
)
  • One glass capsule holding a row of native buttons, with a highlight capsule that springs to the selected segment — like an emoji keyboard's category bar.
  • Segments show an icon, or a short label (e.g. an emoji) when there is no icon; with an icon the label is its accessibility label. foreground, selectedForeground and selectionColor default to the system's secondary label, label and an adaptive light capsule.
  • The bar is segments.length * segmentWidth wide plus its padding. Off iOS 26 it renders the same capsule in Flutter.

Controls and alerts #

NativeSwitch(value: notify, onChanged: (v) => setState(() => notify = v), activeColor: brand);
NativeSlider(value: speed, min: 0, max: 4, onChanged: setSpeed);
NativeSegmentedControl(labels: const ['Member', 'Admin'], selectedIndex: role, onValueChanged: setRole);

final picked = await showNativeAlert(
  context: context,
  title: 'Delete this item?',
  tint: brand,
  actions: const [
    NativeAlertAction(title: 'Delete', style: NativeAlertActionStyle.destructive, preferred: true),
    NativeAlertAction(title: 'Cancel', style: NativeAlertActionStyle.cancel),
  ],
);

The controls are UIKit's own, as platform views Flutter lays out (so they work in Flutter-drawn sheets). They're controlled: the value shown is the one you pass. showNativeAlert presents a UIAlertController and answers with the index of the action picked. Off iOS 26 they're Flutter's Cupertino controls and CupertinoAlertDialog.

Icons #

Every icon parameter takes a NativeIcon, rendered natively:

NativeIcon.symbol('heart.fill') SF Symbol
NativeIcon.icon(Icons.add) Any IconData: Material, Cupertino, package or custom icon fonts, and variable fonts (fill, weight, grade, opticalSize)
NativeIcon.svgAsset('assets/x.svg'), .svgFile(path), .svg(markup) SVG, drawn by Veneer's native renderer (CSS, gradients, clip paths, masks, use, text…)
NativeIcon.image('assets/photo.jpg'), .imageFile(path) Raster images in their own colours

Glyphs and SVGs are template images that tint like SF Symbols; pass tinted: false to keep an SVG's own colours. Release builds tree-shake icon fonts down to the const IconDatas you use, as with Icon. Off iOS 26, SF Symbols map to close Material icons (override with NativeIcon.symbol(name, fallback: …)).

Android and older iOS #

Where the native layer isn't available, every widget renders a Flutter replica with the native layout, sizes, positions and corner radii, iOS system colours, and solid surfaces instead of glass. It follows light and dark mode.

Flutter replicas of the tab bar, navigation bar and composer Flutter sheet with replica controls

To preview the replicas on an iOS 26 device, set Veneer.debugForceFallback = true before runApp. Wrap any subtree in VeneerFallbackScope to force its replicas.

To match your design system, add a VeneerFallbackStyle to your theme's extensions (one per brightness); the replicas read their surfaces, labels, accent, badge colours and shadows from it:

ThemeData(extensions: [
  VeneerFallbackStyle.iosLight.copyWith(accent: brand, surface: card, shadows: brandShadow),
])

A Flutter sheet given a backgroundColor stays solid at every detent; pass a CupertinoDynamicColor to have it follow light and dark mode while it's open.

Limitations #

  • Z-order: native glass and chrome always draw above Flutter content, so their content has to be native. Flutter popups are handled (chrome steps aside); arbitrary Overlays aren't.
  • Clips: one rounded clip per shape is exact; ClipPath counts as its bounding box. Shapes under different clips don't merge. Opaque Flutter widgets painted over glass don't hide it.
  • Route transitions: the tab bar slides away under pushed routes, but the navigation bar and composer switch at the start of a push instead of sliding with the page.
  • Sheets: the sheet's Flutter content runs in its own isolate (see Sheets).
  • Gestures: a touch that starts on an interactive glass shape belongs to UIKit and can't start a Flutter scroll.
  • Context menus: the lifted preview is a still snapshot, so animated content freezes while the menu is up.
  • Accessibility: not yet audited with VoiceOver.
  • Not yet native: toolbars.

Example app #

The example is a small messaging app that exercises every component:

cd example
flutter run
  • Chat: a channel with the native navigation bar, scroll edge effect and composer.
  • AI button (tab bar): an assistant in a native sheet with the prompt composer, attachments, voice mode and native menus.
  • Morph, Clip: merging glass groups and glass inside Flutter clips.
  • Lab: icon types, the sync-lag test and native stats.

Launch with -VENEER_FALLBACK 1 to see the Android / iOS 15–25 UI on an iOS 26 simulator. The example's icons are Phosphor Icons (MIT), compiled into an icon font by example/tool/icon_font/build_icon_font.py.

How it works #

Veneer adds one transparent native view above the FlutterView. Flutter widgets register anchors; once per frame, after Flutter lays out and paints, Veneer collects every anchor's rect, visibility and clip and hands them to UIKit in a single synchronous dart:ffi call, so native views move in the same frame as the Flutter pixels under them. Bars, composers and sheets are UIKit components configured from Dart. Touches pass through to Flutter except where they land on native controls.

See doc/architecture.md for the design, the per-frame path, measured results and the rules learned along the way.

Contributing #

Issues and pull requests are welcome. CI runs format, analysis and tests on every pull request. Before sending a change:

flutter analyze && flutter test
cd example && flutter analyze
cd ios && xcodebuild test -workspace Runner.xcworkspace -scheme Runner \
  -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:RunnerTests

License #

MIT — see LICENSE.

1
likes
160
points
1.94k
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Real native iOS 26 UI for Flutter: Liquid Glass, UIKit tab and navigation bars, composers and sheets driven by Flutter layout, with matching Flutter UI on Android.

Repository (GitHub)
View/report issues

Topics

#ios #liquid-glass #uikit #native #ui

License

MIT (license)

Dependencies

flutter, flutter_svg

More

Packages that depend on veneer

Packages that implement veneer