window.NativeBridge есть на любой странице, открытой внутри Brouncher.
Каждый метод возвращает Promise. Если обращаетесь к нему сразу при загрузке —
дождитесь события nativebridgeready.
83 методов. Исходник: nativebridge.js.
The Taptic Engine. Feedback you feel rather than see, for the moment an action lands. Free of charge and easy to overdo: one tap per real event, never per frame.
A physical tap through the Taptic Engine. style is "light" | "medium" | "heavy" | "rigid" | "soft"; anything else is treated as "medium".
The heavier two-part pattern iOS uses to report an outcome. type is "success" | "warning" | "error"; anything else is treated as "success".
The faint tick for a value changing under the finger — a picker moving to the next item, a segmented control switching. Not for taps.
What this mini-app may ask the device for. The set is fixed when the mini-app is registered on the portal; nothing here can widen it at runtime, only prompt for what was already declared.
Asks the system for a capability, showing iOS's own prompt the first time. Rejects if the mini-app did not declare name when it was registered: a permission you did not ask for at registration cannot be requested at runtime. Resolves once the user has answered.
Whether a capability is available, without prompting. Resolves to { status } — "granted" | "denied" | "restricted" | "notDetermined" for camera and microphone, which the system tracks; "declared" or "not-declared" for the rest, which only depend on registration.
Face ID and Touch ID inside an open mini-app.
Face ID or Touch ID, inside an already-open mini-app. reason is shown in the system prompt. Resolves to { success }, and { success: false, error } when the user cancels or biometrics are unavailable — it does not reject, so a refusal is handled like any other answer.
This gates something within your page. To require Face ID before the mini-app opens at all, ask the portal to set lockOnOpen: that runs before your page exists, and there is no way to reach it from here. Requires the "faceid" permission.
Bluetooth LE: scan, connect, read, write. Peripherals are addressed by the id onDiscover reports, and everything a device sends back — whether from a read or a notification — arrives through onValueChange.
Starts discovering nearby Bluetooth LE peripherals; results arrive through onDiscover. Pass an array of service UUID strings to see only devices advertising those, or omit it for everything. Scanning is power-hungry — call stopScan as soon as you have what you need. Requires the "bluetooth" permission.
Stops discovery. Already-connected devices stay connected.
Connects to a peripheral by the id onDiscover reported. Progress and failure both arrive through onConnectionChange.
Drops the connection to one peripheral.
Writes to one characteristic. The value is base64 because the bridge carries JSON, which has no way to express raw bytes.
Reads one characteristic once. The value comes back through onValueChange, the same path notifications use, so both are handled in one place.
Called for each peripheral found while scanning.
Called when a peripheral connects or disconnects, including disconnections you did not ask for — a device going out of range or losing power.
Called with a characteristic's value, from either a read or a notification.
This mini-app's own icon on the iOS Home Screen.
iOS lets no app place an icon on the Home Screen — not this launcher, not Apple's own apps. The only route is Safari's "Add to Home Screen", so add sends the user to Safari on a page built to be clipped, and the icon it produces opens this mini-app directly.
Which means two things worth designing around. The user leaves your page to finish the job, so call this from a deliberate tap and not on load. And nobody — not the launcher, not Safari — can report back whether they went through with it, which is what check says.
Hands off to Safari with the "Add to Home Screen" instructions and this mini-app's icon ready to be clipped. Resolves as soon as the hand-off is made, not when the user is done: what happens in Safari is not observable from here. Ignored while the mini-app is not the page on screen.
Resolves to { status: "unknown" } — always, on every iOS version. There is no API that can be asked whether a web clip exists. It is here so the answer is an explicit "cannot be known" rather than a missing method, and so that a page written against it keeps working if that ever changes.
The system clipboard, shared with every other app on the device.
Puts text on the system clipboard, where every other app can read it. Requires the "clipboard" permission.
Reads the system clipboard. On iOS this may show the paste banner or prompt, which is the system's decision and not something a mini-app can suppress. Requires the "clipboard" permission.
One contact, chosen deliberately by the user in the system picker. There is no way to read the address book.
Opens the system contact picker and resolves to the one the user chose, or to a cancelled result if they close it. Nothing is read from the address book beyond that single deliberate choice. Requires the "contacts" permission.
One image, chosen by the user in the system picker.
Resolves to { base64, mimeType } for the picked image, or { cancelled: true }. No native Photos permission dialog — PHPickerViewController never gives this app access to the library itself, only to whatever single image the user themselves taps inside Apple's own picker UI.
On-device only — scheduled locally, delivered even if the mini-app (and this launcher) is fully closed by then, same as any installed app's own reminders. Not the same thing as a server waking a closed app on its own (real remote push) — that needs infrastructure this launcher doesn't have wired up.
A local notification from this mini-app, delaySeconds from now. Local only: the launcher has no server to push from, so nothing arrives unless the device scheduled it itself. Requires the "push" permission.
Cancels every notification this mini-app has scheduled and not yet delivered.
What this mini-app is running on, and what it was allowed to ask for.
Resolves to { appName, permissions, systemVersion, model, interfaceStyle }. permissions is what this mini-app declared at registration, which is the reliable way to find out what it is allowed to ask for.
Called when the system switches between light and dark. The current value is in device.info().interfaceStyle.
Being put away and brought back.
Closing a mini-app does not tear it down. The page stays loaded, with its scroll position, its open screen and anything typed into it intact, and reopening it hands the same page back rather than reloading — closer to switching away from an app on iOS than to quitting one.
Which means your page keeps running while nobody is looking at it, and that its load handlers do not run again when it comes back. Both are worth handling: stop timers, polling and animation on suspend, and refresh anything that goes stale on resume.
NativeBridge.lifecycle.onSuspend(function () { clearInterval(poll); });
NativeBridge.lifecycle.onResume(function () { poll = setInterval(tick, 5000); refresh(); });
Native UI you asked for — a toolbar, glass controls, edge bands — is restored for you, so there is no need to describe it again on resume.
A mini-app that requires Face ID to open is never suspended: handing back a screen that is already past its own lock would defeat the lock.
Called when the mini-app is closed but kept alive in the background.
Called when a suspended mini-app is reopened. Never fires on a fresh load — that case is your ordinary page load.
Fire-and-forget diagnostics forwarded to the same log stream native-side events land in. Not meant for mini-app authors to call directly — the launcher's own injected scripts use it to report console errors and rendering liveness.
Writes a line to the launcher's device log, alongside its own diagnostics. For working out what a page did on a real device, where there is no console to open.
The general-purpose layer.
Everything in ui below is a fixed component: the launcher decides what a badge is, where the seven anchors are, which single spring an update animates with. This is the other option — a real native view tree you describe yourself, with real Auto Layout, real animations on any property with your own timing, and real gestures. Nothing here is a preset; if you could build it in a native app, you can build it here.
glass.render(node) replaces the whole scene. Every node is:
{
id: "unique", // how you address it later
type: "view" | "glass" | "glassContainer" | "stack"
| "text" | "icon" | "image" | "input" | "spacer" | "edgeEffect",
layout: { ... }, // see below
material: { ... }, // "glass"/"glassContainer" only
gestures: ["tap","longPress","pan","pinch"],
opacity, hidden, zIndex, transform,
children: [ ...nodes ]
}
"glass" is a real UIGlassEffect surface. "glassContainer" is a real UIGlassContainerEffect: glass children inside it MERGE into one another as they come within spacing points — the liquid-metaball behaviour you see in the system tab bar. That is not expressible with ui at all.
Auto Layout, exposed directly. Any of:
top, bottom, leading, trailing, centerX, centerY, width, height, aspectRatio, hugging, compressionResistance
Each takes either:
42 — points, relative to the parent's same edge
(for width/height: a fixed size)
"50%" — a fraction of the parent
{ ref, anchor, offset, multiplier }
ref is another node's id, or one of:
"@parent" the containing node (the default) "@safeArea" the mini-app's safe area "@screen" the whole screen, ignoring safe area "@keyboard" a live guide whose top edge tracks the keyboard
Referencing another node is the important one. "Keep these buttons level with that field" is a constraint — { ref: "field", anchor: "centerY" } — resolved in one layout pass, not arithmetic your page performs against constants and then re-applies on a second timer. They cannot drift apart, because there is no second clock. The same goes for "@keyboard": pinning above the keyboard is declarative, and moves inside the keyboard's own animation.
An "edgeEffect" node is the blur-and-darken strip content runs off under, rather than being cut at a hard line. Its size and position are ordinary layout, so it can span the top of the screen, sit behind a composer, or line any edge of any container:
{ id: "topFade", type: "edgeEffect", edge: "top",
blur: true, dim: { color: "#000000", alpha: 0.55 },
layout: { top: 0, leading: 0, trailing: 0, height: 120 } }
edge: "top" | "bottom" — which side the effect is strongest on; it
fades inwards from there.
blur: true | "ultraThin" | "thin" | "regular" | "thick" | "light"
dim: "#rrggbbaa" or { color, alpha }
Both are independent: pass only blur for a frosted strip, only dim for a plain legibility scrim, or both for the full effect. An edgeEffect never takes touches, so it can sit over scrolling content freely.
material: {
style: "regular" | "clear",
tint: "#rrggbb" | "#rrggbbaa",
interactive: true, // the real touch-reactive material
corners: "capsule" | 18 | { topLeft, topRight, bottomLeft, bottomRight }
}
glass.animate(changes, animation) — one animation, any number of nodes, any animatable property, your timing:
glass.animate(
[ { id: "pill", layout: { height: 96 } },
{ id: "mic", opacity: 0, transform: { scale: 0.001 } },
{ id: "panel", material: { tint: "#2f9bffcc" } } ],
{ duration: 0.4, curve: [0.22, 1, 0.36, 1] }
);
curve is "easeInOut"/"easeIn"/"easeOut"/"linear" or four cubic-bezier control points — the same numbers as your CSS. Or pass a real spring:
{ duration: 0.5, spring: { mass: 1, stiffness: 460, damping: 22 } }
glass.set(changes) is the same thing with no animation.
glass.on("myButton", "tap", function (e) { ... });
glass.on("myPanel", "pan", function (e) {
e.phase; e.translationX; e.translationY; e.velocityX; e.velocityY;
});
An "input" node also emits "input" ({ text, height }) and "submit". Pass "*" as the id to hear that event from every node.
await glass.measure("id") -> { x, y, width, height, centerX, centerY }
await glass.getText("id") -> an input node's current textReplaces the whole scene with this tree. Pass null to tear it down.
Removes the scene entirely.
Applies changes instantly. changes is an array of { id, ...properties }; whatever a change does not mention is left alone. Same vocabulary as the node it addresses — text, layout, material, and so on.
The same changes, animated. animation is { duration, curve } or { duration, spring: { mass, stiffness, damping, initialVelocity } }.
For a movement that has to line up with something your page is animating, use timeline below instead: this animates the native side alone, and a CSS transition started separately is a second clock.
animation.from gives the node's starting state, so you decide how it arrives — there is no built-in insertion look to opt out of.
animation.to gives the state to animate to before it goes; without one it simply fades.
Resolves to { x, y, width, height, centerX, centerY } for one node, in screen points after layout has settled — so a page can place its own content around a control whose size the launcher decided. Resolves to null if there is no such node.
The current contents of an input node.
Focuses an input node and raises the keyboard.
Drops focus and dismisses the keyboard.
Subscribes to one node's events: "tap" on anything with gestures: ["tap"], "input" and "submit" on an input node. The handler receives the event payload — an input event carries the text and the field's height.
Every event from every node, if you'd rather route them yourself.
One animation, drawn by both renderers.
glass.animate and a CSS transition are two clocks: each is scheduled by a different process against a different origin, so however carefully they are started together they drift a few milliseconds — small, and exactly the drift that reads as the header not being part of the screen it belongs to.
This is the other option. You describe the whole movement once — the parts that happen in your page and the parts that happen in glass — and both are committed to the same start instant. They are not kept in step; they are in step, the way two layers of one native screen are, and there is nothing left to calibrate.
NativeBridge.glass.timeline({
duration: 0.42,
curve: [0.22, 1, 0.36, 1],
page: [{
selector: "#chatDetail",
keyframes: [{ transform: "translateX(100%)" }, { transform: "translateX(0)" }],
commitStyle: { transform: "translateX(0)" },
}],
glass: [
{ id: "back", from: { x: "100%" }, to: { x: 0 } },
{ id: "title", from: { x: "100%" }, to: { x: 0 } },
{ id: "avatar", from: { x: "100%" }, to: { x: 0 } },
],
});
page steps are Web Animations keyframes — anything you can animate in CSS. glass steps take x/y (points, or a string like "100%" of the screen so a control can say "off the edge" without knowing the device), opacity and scale; id is any glass control or scene node, or one of "@blurTop", "@blurBottom", "@toolbar", "@scene".
commitStyle is written onto the element when its animation finishes, so the end state becomes the element's own and the fill can be dropped without a flash. lead (default 0.05) is how far ahead the shared start is placed — it only has to cover one bridge hop, not be guessed accurately.
Native Liquid Glass UI (iOS 26+, falls back to a blur material on older systems). Every element below accepts a tintColor (any hex color) and an anchor — one of: "topLeading" | "topCenter" | "topTrailing" | "center" | "bottomLeading" | "bottomCenter" | "bottomTrailing" — placing it anywhere on screen.
idshowGlassButton / showGlassBadge / showGlassView / showGlassShape / showGlassCard / showGlassCluster each take an id, naming which instance you mean. Omit it and you get the implicit one — so a page showing a single button of a kind never has to think about ids at all. Pass different ids and you get genuinely separate elements, live at the same time:
NativeBridge.ui.showGlassView({ id: "avatar", shape: { kind: "circle" }, … });
NativeBridge.ui.showGlassView({ id: "unread", shape: { kind: "capsule" }, … });
This matters most for showGlassView, the general-purpose builder: it exists so a new design doesn't need an app update, and a one-per-screen limit meant a chat that spent it on an avatar had none left for anything else.
The matching hide takes an id too. Passing one hides exactly that element; omitting it hides every element of that kind — usually what you want when leaving a screen, and what makes pages written before ids existed keep behaving as they always did. updateGlassView addresses the element with viewId (its id field is reserved for the content node inside it).
offsetoffset: { x, y } shifts an element from its anchor in points — positive x toward the trailing edge, positive y downward. Seven anchors can't express "just above the composer", and picking a different anchor than the one you mean to dodge something is worse than saying so.
Tap events carry the id back, so one handler can serve every instance:
NativeBridge.ui.onGlassViewTap(function (e) { … e.id … });The launcher's own tab bar, at the bottom of the screen, in real glass. items is [{ id, title, systemName }] using SF Symbol names; selected is the id to start on. options: { tintColor }.
Taps do not change the selection on their own — onToolbarTap tells you, and your page decides. That is deliberate: a tab whose content fails to load should not leave the bar showing a tab the user is not on.
Takes the tab bar away — for a screen pushed on top of your tabs, say.
Called with the tapped item's id. Confirm it with setToolbarSelection once the tab has actually switched.
Tells the bar which tab the page is really on. The bar moves optimistically when tapped, so call this to confirm a tab — or to put it back when the tap did not navigate anywhere, instead of leaving the bar showing a tab you are not on.
style: "glass" | "prominentGlass" | "clearGlass" | "prominentClearGlass" options: { title, style, tintColor, anchor, size, slide } — icon and/or title may be given. size is the button's diameter (icon-only) / height (with title) in points, default 52.
slide slides the button in from the trailing edge instead of just fading in — for a control that appears alongside a screen you're sliding in yourself (a chat header's back button, say, next to the chat screen itself pushing in). Every mini-app animates its own screens differently — a fast snap, a slow ease, a custom brand curve — so this native slide is never one fixed timing; it takes whichever of:
- omitted / `false` — no slide, just the plain fade.
- `true` — slide with a reasonable native default timing.
- `{ duration, curve: [x1, y1, x2, y2] }` — your own duration (seconds)
and cubic-bezier control points, i.e. exactly the numbers already in
your page's own CSS `transition` on the screen this sits alongside,
so the two motions read as one instead of two separately-timed ones.
A slide is self-contained: it animates this one control and waits for nothing. If the control has to travel with something your page is animating — a screen sliding in, a panel dropping — do not try to match this to a CSS transition by giving both the same numbers. Two animations started by two processes are two clocks, and they drift. Describe the whole movement once with glass.timeline instead: it commits your page and the glass to one start instant, so they are not kept in step, they are in step.
The same slide shape works on hideGlassButton/showGlassBadge/ hideGlassBadge/showGlassView/hideGlassView/showGlassComposer/ hideGlassComposer below.
morph: true is a different kind of transition from slide — a same-spot crossfade-and-scale swap for a button whose icon is changing, not one appearing alongside something else moving. Matched against Telegram's own mic → send transition (their ChatTextInputPanelNode.updateActionButtons): showing a button with an id already on screen and morph: true fades and scales the old one out while the new one fades and scales in, in place, over 0.2s — instead of slide's off-to-the-side motion, or the plain instant swap you'd get with neither. Use this for a composer's mic circle becoming a send circle (or back) as typing starts and stops; use slide for a control appearing or leaving alongside a screen transition instead.
options: { slide } — carries the button off toward the trailing edge instead of just fading it out in place; see showGlassButton's own note on slide for the full shape. Use this when the button is part of a screen your own page is sliding away (a chat header's back button alongside the chat itself sliding off), so the native control reads as leaving with that screen instead of independently disappearing.
Called with { id } for any button shown by showGlassButton.
options: { tintColor, style } — style: "regular" (default) | "clear" (UIGlassEffect.Style — a more transparent variant for over rich content).
Dismisses the card from code, for when whatever it was asking about got settled somewhere else. The card's own close button does not need this. options: { id } — omit it to dismiss every card at once.
options: { tintColor, anchor, size, style } — size is each bubble's diameter, default 44; style: "regular" (default) | "clear".
options: { id } — omit it to remove every cluster at once.
Called with { id, index } — which cluster, and which item within it.
A real UIGlassEffect in a shape none of the other elements above can take — they all require an icon or title (showGlassButton) or are always the same rectangular card (showGlassCard). Three kinds, via options.shape: - "polygon" (default): pass points, an array of [x, y] pairs normalized
0..1 within options.width x options.height — a star, a hexagon, a speech-bubble notch, anything expressible as a closed polygon.
- "circle": a plain, contentless glass circle. points is ignored. - "roundedRect": a plain glass panel with options.cornerRadius (default 16). options: { shape, points, width, height, cornerRadius, tintColor, style, anchor }.
options: { id } — omit it to remove every shape at once.
Called with { id } for any shape shown with gestures.
A compact glass capsule holding a title + optional subtitle, no icon required — a bot/channel-style header badge ("Split | Купить Звёзды" + "14 995 пользователей"), which neither showGlassCard (always a big rectangular card) nor showGlassButton (one line of text at most) can produce. options: { subtitle, tintColor, style, anchor, height }. height fixes the badge's height (content centers inside it) instead of auto-sizing to its own text — set it to match neighboring fixed-size header elements (a back button, an avatar circle) so a header row lines up at one consistent height instead of each element being its own size. slide: true — see showGlassButton's own note; same meaning here.
options: { slide } — see showGlassButton's own note.
Called with { id } for any badge shown by showGlassBadge.
The general-purpose escape hatch: describe any glass element — shape + an arbitrary tree of text/icons/stacks — as plain data, instead of reaching for one of the fixed methods above. This is what makes a brand new design (any shape, any arrangement of content) possible without a launcher update: nothing here is a fixed native component, it's all built from this one call.
spec: {
shape: { kind: "capsule" | "roundedRect" | "circle" | "polygon",
cornerRadius, points }, // roundedRect/polygon only
width, height, // omit to auto-size from content (not for "polygon")
tintColor, style, interactive, anchor,
padding: number | { top, left, bottom, right },
content: node, // see below — omit for a plain, contentless glass shape
}
A node is one of:
{ type: "text", id, text, fontSize, fontWeight, color, textAlign }
{ type: "icon", id, systemName, size, color }
{ type: "spacer", length }
{ type: "vstack" | "hstack", spacing, alignment, children: [node, ...] }
id on a text/icon node is optional and only matters for updateGlassView afterward — give one to anything you'll want to change live (a counter, a status icon, a balance) without rebuilding the whole element.
Example — the exact title+subtitle badge showGlassBadge hardcodes, built from scratch instead, with its subtitle id'd for a live update later:
NativeBridge.ui.showGlassView({
shape: { kind: "capsule" },
content: { type: "vstack", spacing: 1, children: [
{ type: "text", text: "Полярный Банк", fontSize: 15, fontWeight: "semibold" },
{ type: "text", id: "memberCount", text: "128 442 клиента", fontSize: 12, color: "#ffffff99" },
]},
anchor: "topLeading", tintColor: "#2fb5ff",
});options: { slide } — see showGlassButton's own note.
Called with { id } for any view shown by showGlassView with interactive: true.
Updates the currently-shown showGlassView in place — a real animation on just what changed, not a teardown-and-rebuild of the whole element (which would restart every other part of it too, and read as a jump cut rather than a live update — the same distinction real Liquid Glass surfaces like the Dynamic Island draw between "this changed" and "this is a whole new thing"). Any combination of, in one call:
- one id'd child's content: `id` + whichever of
{ text, color, systemName, hidden } apply to it (a UILabel only reads
text/color; a UIImageView only reads systemName/color) — crossfaded.
- the whole surface's tint: `tintColor`, no `id` — crossfaded.
- its shape: `shape` (same `{ kind, cornerRadius, points }` showGlassView
takes) — a capsule can become a circle, a rounded rect a star —
crossfaded.
- its size and/or position: `width`/`height`/`anchor`/`offset` — a real
spring animation, since this one is actual motion, not a material
property.
options: { animated } — defaults to true.
The same live resize/reposition updateGlassView gives showGlassView, generalized to every other kind this bridge can show: a button, badge, shape, or cluster can be narrowed, widened, moved to a new anchor, or nudged with offset — animated with the same spring — once it's already on screen, instead of only ever being fixed at whatever size/position it was first shown with.
patch: { kind, id, width, height, anchor, offset, animated }. kind is one of "button" | "badge" | "shape" | "cluster" | "view" (though updateGlassView above is the richer call for "view", since it can also touch content/tint/shape, not just layout). Omitting id updates every instance of that kind at once. Whatever you don't mention is left as it was — updating only anchor doesn't reset the width back to auto, for instance.
Telegram's chat edge effect — content blurs and darkens into the launcher header at the top and into the bottom of the screen, instead of being cut at a hard line. The real thing, ramp and proportions included.
setSystemBlur(true) // both edges, default placement
setSystemBlur({ top: true }) // top only
setSystemBlur({ bottom: true }) // bottom only
setSystemBlur(false) // off
Default placement is the top band hanging directly under the header and the bottom one sitting at the very bottom of the screen. To put a band somewhere else, give it an offset — how far inwards from its own edge it moves, down from the header for the top, up from the bottom of the screen for the bottom:
setSystemBlur({ top: { offset: 44 }, bottom: { offset: 96 } })
offset and the default placement are mutually exclusive: passing one is choosing custom placement, so there is nothing to turn off first. Also per edge: height (how deep the band covers the page), blurHeight (how long the ramp takes to fall from full strength to nothing — this does not crop the effect, a shorter value makes the same band go sharp-to-blurred over less distance so the transition reads faster and harder, a longer one spreads it out and reads softer), blur: false (tint with no blur), color and alpha (for a mini-app whose background is not near-black), and aboveControls: true to draw the band over your glass elements instead of behind them.
Any of those keys placed at the top level rather than inside an edge applies to both edges, so "same falloff top and bottom" is one value in one place:
setSystemBlur({ top: true, bottom: true, blurHeight: 48 })
setSystemBlur({ blurHeight: 48, top: true, bottom: { blurHeight: 96 } })
An edge's own keys win over the shared ones.
Pinch and double-tap zoom, per screen. Off everywhere by default, because a mini-app should feel like an app rather than a web page — turn it on for the screens where zoom is the point (a photo, a map, a scan) and leave it off for the rest. Each pushed screen is its own web view with its own answer, so a gallery can allow it while the list it opened from does not, and going back restores the list's own setting without the page having to ask again.
setZoomEnabled(true) // up to 3x
setZoomEnabled(true, { maximumScale: 6 })
setZoomEnabled(false)Stacking order for anything on screen, addressed by the id you already use. Besides your own ids there are four names for the surfaces the launcher owns: "@blurTop" and "@blurBottom" (the edge bands), "@page" (your web content), "@toolbar" (the launcher's tab bar) and "@scene" (everything in your scene, as one).
setLayer("composer", { above: "@blurBottom" })
setLayer("@blurTop", { above: "header" }) // frost the header too
setLayer("watermark", { back: true })
setLayer({ reset: true }) // drop every ordering
Orderings are remembered and re-applied whenever anything new is mounted, so one does not quietly revert the next time you add a control. Two elements with different parents have no order between them — the exception is a scene node against something outside the scene, where the scene as a whole moves. The close button always stays on top.
Paints your top edge with an exact color — both the native header region above your page (where the close button sits) and the WKWebView's own rubber-band overscroll gutter just below it (what you see if the user pulls down past the top of the page). Automatic detection for both only ever sees your flat background-color, which for a gradient or image background usually isn't the color actually touching the top edge; pass the color your background touches there (e.g. a gradient's first stop) and native chrome, overscroll, and content all line up. Call again whenever that top color changes (e.g. switching screens); call with no argument to go back to automatic detection.
Closes the mini-app, the same as the user tapping the native close button themselves. Useful for a "Done"/"Log out" action inside your own page — if this mini-app also has Face ID required on open (a launcher-level setting, not something the page controls), the next open will ask again.
A real UITextView in a real UIGlassEffect capsule, floating over your page — meant to fully replace your own message/comment input, the same way showGlassToolbar replaces a bottom nav. Matched against Telegram's own iOS composer: 42pt tall at rest, 17pt text, and it genuinely grows as the user types a second and third line — up to five lines, then scrolls internally — with its corner radius recomputed at every height so it stays a true capsule throughout, not a fixed shape the text scrolls sideways inside. Return inserts a newline, the same as Telegram's own field; there's no way to send except tapping the button.
There's no way to read what's typed as it's typed; onComposerSend(fn) fires once with the full text when the user taps send, and the field clears itself. The send button only appears once there's at least one character typed — baked into the composer itself, not something you configure. options: { tintColor, liftContent, fullWidth, insets }. A real DOM <input>/contenteditable in your own page gets its surrounding content resized around the keyboard automatically, for free — this composer is a native control the page never actually sees, so that doesn't happen unless you opt in with liftContent: true, which raises your whole page alongside the composer instead of only the composer itself. fullWidth: true runs the bar edge-to-edge — still a rounded capsule (real Liquid Glass surfaces stay rounded regardless of width; a flat, square-cornered bar isn't a look this material uses) — instead of the default floating pill with side margins.
insets: { left, right } (16pt each by default — the same margin the header's own elements use) is how you narrow the pill to make room for your own controls beside it, a paperclip or a mic circle say, instead of those overlapping a bar that's otherwise always edge-to-edge-minus-16. updateGlassComposer below changes these live; onComposerHeightChange tells you the pill's current height (it grows for multi-line text) so anything you've placed alongside it can track that growth via updateGlass's own offset. slide — see showGlassButton's own note; same meaning here.
options: { slide } — see showGlassButton's own note.
Narrows, widens, or re-centers a composer already on screen. options: { insets: { left, right }, animated }. Only the sides you mention change — passing just { left: 68 } leaves the right inset wherever it already was.
Called with { text } when the composer's send is triggered — by its own return key, or by sendComposer.
There's no button inside the composer itself to tap — matched against Telegram's own layout, where sending lives entirely in a separate circle beside the pill (see onComposerHasTextChange's own note). Call this from that circle's own tap handler once it's showing a send icon; it fires onComposerSend with whatever text the field currently holds, exactly as if the field had a send button of its own.
Makes the whole page ride up and down with the keyboard, on the keyboard's own curve and duration — not an approximation of them: the page is moved inside the very same native animation, so there is nothing to match.
Off by default, and you usually don't want it. A page whose input is a real DOM element already gets this from WebKit for free. Turn it on when the keyboard is being held open by something WebKit cannot see — a native composer, or an "input" node in a glass scene — because as far as the page is concerned nothing is focused and nothing moves. Turning it on as well as WebKit's own handling would move everything twice.
Fires with the composer's current height (points) whenever it changes — typing past one line, deleting back down, or once at rest right after showGlassComposer. Use this to keep anything positioned relative to the composer (updateGlass's offset) in step as it grows.
Fires with { hasText: true|false } the moment the field crosses between empty and non-empty — not on every keystroke, only the transition. Checked against Telegram's own ChatTextInputPanelNode: there is no button inside their pill at all, and the pill's width never changes between the two states — sending lives entirely in a separate circle beside it, which crossfades+scales between a mic icon and a send icon in place (their own values: 0.2s, ease-in-out, scaling toward/from ~0). Reproduce that by re-showing the same button id with the new icon and morph: true on this event, and calling sendComposer() from that circle's own tap handler while it's showing the send icon:
NativeBridge.ui.onComposerHasTextChange(function (e) {
NativeBridge.ui.showGlassButton(e.hasText ? "arrow.up" : "mic.fill", {
id: "mic", anchor: "bottomTrailing", size: 44, morph: true,
style: e.hasText ? "prominentGlass" : "glass",
});
});A real UIAlertController, not an in-page modal — blocks like a native alert and reads exactly like one. alert() resolves once dismissed; confirm() resolves { confirmed: true|false }.
The system confirmation dialog. options: { confirmText, cancelText, destructive }. Resolves to { confirmed }.
A real UIAlertController, not an HTML dialog: it looks and behaves like every other confirmation on the device, and cannot be styled away.
An unread-count badge on this mini-app's own Home Screen icon. 0 clears it.
Native push/pop navigation: pushScreen loads url (relative to your own mini-app's origin) into a brand-new native screen, slid in with the standard iOS transition and edge-swipe-to-go-back gesture already wired up. popScreen goes back one screen, the same as the user swiping or tapping "Назад".
Goes back one screen, the same as the user swiping from the edge or tapping the launcher's own back button.
ничего не найдено