A native macOS backend
On macOS createRoot() renders through Cocoa by default: real
NSWindows, Core Animation compositing, the system menu bar and native
control bezels, with no X server anywhere. The tree, components, styles and
hooks are the same; this page is what an app sees differently here, and what
it does not have yet. The design record — presenters, threads, the bridge
contract, measurements, the plan — is design/macos.md.
Choosing the backend
createRoot({ backend }):
'auto'(the default) — Cocoa on macOS where the bridge is installed, X11 through$DISPLAY(XQuartz) where it is not.'cocoa'— Cocoa, with the bridge required.'x11'— X11 through XQuartz, still the way to run X-only features (<foreign>,examples/wm.jsx) on a Mac.
REACT_X11_BACKEND=x11 or =cocoa overrides both. createRoot({ app })
ignores backend: a connection already is one.
The bridge is @windowkit/appkit,
an optionalDependency with N-API prebuilds for arm64 and x64, so it works
under Node and Bun. An install that skips optional dependencies has no
bridge, and 'auto' falls back to X11. The range react-x11 asks for has
everything on this page; where an older bridge lacks a verb, the feature is
detected and degrades as described below.
Root options and environment
createRoot({ cocoa: { … } }), ignored off macOS and when app is passed:
presenter—'surface'(the default: one bitmap per window) or'layers'(one CALayer per drawn node, opt-in while it is measured).REACT_X11_COCOA_PRESENTERsays the same.promote— layer promotion of animated boxes;falsekeeps every animation on the frame clock.REACT_X11_COCOA_PROMOTE=1/=0says the same.frameInterval— ms between scheduled frames. By default each window paces itself on its own display (8.3 ms at 120 Hz, 16.7 at 60); a number applies to every window.pumpInterval— ms between AppKit event pumps (8) when the app's JS runs on the main thread: the floor under input latency there. A worker has no pump.resizeWait— ms AppKit may hold a live-resize tick for the app's frame at the new size, on a worker (50;0lets the edge move without waiting).paneStretch— when a<Frame>pane's last frame is stretched while its next one comes:'behind'(the default: once it is later thanresizeWait),'never'or'always'.REACT_X11_COCOA_PANE_STRETCHsays the same.screenPoll— ms between re-reads of the screen layout while auseScreens()subscriber is mounted (500;0for never).appName— what the Dock, ⌘-Tab and the app menu print for an unbundled process; a bundle'sInfo.plistwins.activationPolicy—'regular'(the default: a Dock tile and a ⌘-Tab entry),'accessory'(a menu-bar app: windows, no tile) or'prohibited'. On a worker, set it at launch.exitOnQuit— end the process once a quit request (the Dock's Quit, ⌘Q, a logout) has closed the app (true);falseis for an embedder that owns the process's lifetime.
Other variables: REACT_X11_THREADED=0 (keep the JS on the main thread),
APPKIT_ACTIVATION_POLICY (the policy AppKit launches with),
REACT_X11_CALAYERS_PATH (where the bridge's addon is, for a
compiled binary), and
REACT_X11_NO_STROKE_CHUNKING=1 and
REACT_X11_NO_SHADOW_TILES=1.
The desktop option defaults to { a11y: false, globalMenu: false } here:
AT-SPI and the D-Bus menu export have nothing to register against on a Mac.
The menu bar is the system's own
regardless.
Windowing semantics: what maps, what bends, what breaks
macOS has no separate window manager, so what one does is where the backends part company. The per-prop list is elements.md; in short:
- Maps:
<window>is anNSWindowthat places itself (x/ywork), min/max sizes are its own, and its size is a whole number of points, rounded up.resizableanddecorationsare read once, at creation.fullscreen,maximized,hiddenanddemands_attentionare the full-screen Space, the zoom, the Dock and the Dock bounce. - Better:
transparentand a window'sborderRadiusalways work — every window composites, souseSupports('transparency')is always true. - Inert: the
sticky,below,skip_taskbarandshadedstates;appId,windowType,gravityand the increment and aspect hints (bundle identity lives inInfo.plist);transientFor, for now — a managed<Dialog>floats on its own window anyway.onClientMessagenever fires. <popup>is a borderless panel at the popup level, placed by the same anchor math.grabbends: there are no pointer grabs, so a press in another of the app's windows reaches the popup as a press outside it, which dismisses it, and a press in another application reaches nothing — the app deactivating does, asonDismisswithreason: 'focus', so a menu is not left over the app the user switched to.grabKeyboardmakes a key-capable window, asNSPopoverdoes (elements.md).<glarea>is a CGL context rendering into IOSurfaces on a sublayer (gl.md);<Frame>works through shared surfaces rather than reparenting (frame.md).- Screens (
useScreens) map, but macOS sends a client no event for a display change, so the layout is re-read on a clock while watched (system.md).app.scaleis the screen'sbackingScaleFactor, read once, at startup. - Not yet: nested
<window>elements throw. - Gone by design:
<foreign>and XEmbed, a window manager (examples/wm.jsx),ssh -X. A<foreign>reports oneonErrorand draws an empty box;useSupports('embedding')is false.
Inert means inert, not fatal, and silent: a prop with no meaning here is accepted and does nothing, so the same JSX runs on every backend and shared application code stays branch-free.
Desktop integrations: what an app does outside its own windows
Most of these are filedialog.md's ladder: the platform's mechanism, a shell-out beneath it, and a typed "cannot do this here" at the floor.
- File open/save:
NSOpenPanel/NSSavePanel, a sheet on the window that asked (filedialog.md). - Raise:
activateWindow()activates the app and orders the window front. - Deep links, documents, reopen: Apple Events into
useAppOpen/useAppActivate; Quit is the primary window's close request; single instance is the platform default (uri-schemes.md). - Drag and drop with other apps: AppKit's dragging behind the same
dropAccept/onDrag*/dragDataprops (drag-and-drop.md). - Clipboard:
NSPasteboard.generalasCLIPBOARD;watch()polls. NoPRIMARY: selecting copies nothing (clipboard.md). - Tray:
useTray()overNSStatusItem, icon names as SF Symbols (desktop.md). - Dock:
setBadge/useBadge, thedemands_attentionbounce,useLauncherMenu; the name and activation policy are thecocoaoptions (desktop.md). - Permissions:
usePermission()/requestPermission()over TCC, andopenPrivacySettings(permissions.md). - Notifications:
notify()overUNUserNotificationCenter, only in a code-signed app bundle;osascriptbanners otherwise (notifications.md). - Calendar:
useDesktopCalendarEvents()over EventKit (desktop-calendar.md). - Eyedropper:
pickScreenColor()overNSColorSampler; the system draws the loupe, so no Screen Recording grant is needed. A bridge older than 0.9 has no rung, anduseEyedropper().supportedis false (eyedropper.md). - Idle time:
useIdle(), from the bridge orioreg(system.md). - Appearance: dark or light, the accent, and the desktop's whole palette (appearance.md).
- Keep-awake: no macOS rung —
useKeepAwake()holds nothing here. - Accessibility: none yet (below).
Text
Text goes through CoreText behind the same API: the same <text> measures,
wraps, elides, selects and hit-tests on both backends, with the platform's
metrics, which differ from X11's. Layout can wrap differently across
backends; compare structure and behaviour across them, not pixels.
- Fonts are the system's. The CSS generic families map to system faces
(
sans-serifis San Francisco),opszis CoreText's own optical sizing, and-apple-systemandBlinkMacSystemFontread assystem-ui, so a font stack written for the web sets in SF. Labels and emoji measure as AppKit sets them. - OpenType features (
fontVariantNumeric,fontFeatureSettings) andletterSpacingwork;letterSpacingturns optional ligatures off, as on X11. A bridge too old for one draws the plain face and says so once in development. - Lines break at the same UAX#14 opportunities as on X11, so a
<text>in a flex item shrinks to the same min-content width; one that elides (textOverflow: 'ellipsis') floors at its…. An empty paragraph is one empty line, so an empty field's caret has a line's height. - Antialiasing is greyscale, as macOS has drawn it since Mojave.
- Glyph-level drawing (
drawGlyphs,positioned, a face'sglyphIdFor/advanceOf/shape/metrics,fallbackFor) answers ntk's contract, so a renderer written against ntk's context runs unchanged. Three differences:shape()substitutes fallback faces, so an emoji in Menlo comes out as the emoji where ntk shapes.notdef; every op draws asOver; and a transform scales glyphs as well as their origins. coverage()is the signed-area fill X11 uses, so both give the same bytes for the same outlines; colour emoji come out as silhouettes. A bridge without the verb answersnull.- No input method yet: dead keys, Press-and-Hold accents and CJK input
wait on
NSTextInputClient(events.md says what a dead key types meanwhile).
Input and the run loop
- Pointer: top-left coordinates,
clickCountasdetail, and a trackpad's precise deltas as the wheel'ssmoothflag. - Hover belongs to the active window: a background window gets no
:hover, enter, leave or motion until it is active, the platform's convention (events.md). A first click on a background window still acts, and the wheel still scrolls it. - Modifiers: ⌘ is
metaKey, ⌥ isaltKey. ⌘ is the app's primary modifier (app.primaryModifieris'Super'), so the text controls answer ⌘Z/⌘C/⌘V, their edit menu prints ⌘C, and a chord written withControlmeans ⌘ (events.md). - Keys arrive as the same
XK_*keysyms as on X11, and shortcuts keep their Latin letters under a non-Latin layout. The lock state is readable before the first key. - Frames are paced per window on its own display. A window that is
ordered out, miniaturized or entirely covered by another app's window
paints nothing — its frames and
requestAnimationFramecallbacks wait — and gets one catch-up frame when it is back. A window fed off a stream (a terminal undercat) paints every refresh unless it setsframeRate: 'adaptive'(elements.md).
JS on a worker: a UI thread of the bridge's own
AppKit owns the process main thread and runs its menus, drags, live resizes
and modal panels there. So on macOS react-x11 moves the app's JS onto a
worker_threads Worker by default, and the main thread sits in a real
[NSApp run]: the JS keeps running through every menu and resize, and input
reaches it as it happens rather than at the next pump. It needs a bridge with
runMain() (@windowkit/appkit 0.10).
node app.js and bun app.jsx move with nothing asked for, and so does
tsx app.jsx on Node 24 and later. The launcher moves the app before its
entry is loaded at all, which is cheaper:
node --import react-x11/cocoa-main app.js
bun --preload react-x11/cocoa-main app.jsx
The app stays on the main thread, with the pump, off macOS; with
REACT_X11_THREADED=0 or REACT_X11_BACKEND=x11; in a REPL or under -e;
in a single executable (a Node SEA, bun build --compile); in a <Frame>
pane; under a test runner; when react-x11 is imported after the app started
running (an import() the entry awaits, or one from a timer); and on Node 20
and 22 under an ES module loader (--import, --loader, tsx). There a menu,
a drag, a live resize or a modal panel stops the JS while it is open. The
startup cost of each route and the matrix by Node version are in
packaging.md.
A menu-bar app sets its policy at launch. On a worker AppKit is up before
the app's code runs, so cocoa: { activationPolicy } can only switch it
afterwards, when a Regular launch has already shown a Dock tile. Launch with
the variable, or put LSUIElement in the bundle's Info.plist; an app whose
option disagrees with its launch is told so in development.
APPKIT_ACTIVATION_POLICY=accessory node --import react-x11/cocoa-main app.js
What changes for an app
- About 16 MB of RSS for the worker's isolate, and 26–31 ms before the worker runs its first line.
- The idle app stops waking 125 times a second: the pump is an 8 ms interval whether anything happens or not.
- Inside a Worker,
process.chdir()throws,process.on('SIGINT')is never called (the launcher forwards signals: a signal is re-emitted on the worker'sprocess, and one nobody listens for exits with 128 + n),process.envis a copy unless it is shared, andprocess.stdinforwards through the main thread the way stdout does. - What the entry imports ahead of react-x11 runs twice: on the main
thread, then on the worker. A module that does work as it loads — opens a
port, writes a file, logs — does it twice. A call only a main thread may
make —
process.chdir(),process.umask(mask)— goes through the first time and throws the second, and the app stops with that error and a word on why. Make it on the main thread only (isMainThread, fromnode:worker_threads): what it changes is the process's, so the worker starts with it. One made after the import runs on the worker alone and throws there. Import react-x11 first in the entry, or setREACT_X11_THREADED=0to keep the whole app on the main thread. process.exit()and an uncaught error end the app as they would a process of its own: exit handlers run, and the error is printed with exit code 1.--inspectattaches to the main thread; the app is the worker target beside it.- A single executable does not move, and runs on the pump (packaging.md).
Not done yet on a worker:
- A dropped image, or any binary type, reads as nothing: the drop carries its text and URL forms.
- The layer presenter's colour transitions start from the declared value.
- A transparent window's shadow is recomputed a frame interval after a flip, not once the flip has committed.
- A worker that cannot load the entry stops the app: under tsx before 4.21.1
on Node 24 and later, or tsx on Node 20 and 22 under the launcher, it stops
as it starts on
ERR_UNKNOWN_FILE_EXTENSION. Update tsx, or setREACT_X11_THREADED=0. - Under Bun, react-x11 required from a callback before the entry has
finished — a
nextTickor microtask a CommonJS entry queues, a timer while an ES module entry awaits at top level — reads as the entry's first run, and that code runs again on the worker. - Not yet tried on the worker:
<glarea>'s GL context andreact-x11/refresh's module hooks.
Native controls
Button, Checkbox, Radio, Switch, Slider and Select wear
AppKit's own bezels here by default; interaction, focus, keyboard and
accessibility stay the shared implementation. A bezel is an image drawn
through the ordinary paint path, not an embedded NSView
(components.md).
ThemeProvider'scontrols:'auto'(the default) takes bezels wherever the backend has them — Cocoa, and win32 while the desktop is light — and draws elsewhere;'native'asks outright, and on a backend without them warns once and draws;'drawn'is this library's rendering everywhere.native={false}on one control is the escape hatch.useSupports('nativeControls')says whether the backend has them.- A native control is two boxes: the footprint
stylesizes, and the control at AppKit's metrics, centred in it. Role, handlers, focus ring and ref are on the control, so a press in the slack around it does nothing. - A
<Button>label wraps when its row is too narrow, and the control grows with it; past one line it is AppKit's flexible push button, whose default-button accent is flat. A bridge too old for that bezel keeps the push and says so once in development. - Text controls stay drawn: a native
NSTextFieldwould fork the shared editing model. - Drawn content sits beside the bezels through the desktop's palette (appearance.md).
Menus: the global menu, finally at home
The menu bar is the system's, always. A window's menus go to the macOS
menu bar as soon as they are installed, the drawn bar never renders, and
onGlobalMenuChange(true) reports it (globalmenu.md). Item
icon names are SF Symbols.
- An application menu goes in front of the app's, carrying Quit (⌘Q).
Quit is a quit request: the primary window's close request, which
onCloseRequestcan intercept, then, withexitOnQuit, the end of the process. Nothing else is synthesized — no About, Hide, Edit or Window menu — so an app that wants them names them inmenus. - Accelerators:
Controlin a chord is ⌘, so[['Control', 'S']]is ⌘S; there is no separatePrimarytoken. The menu bar answers the chords it can spell — one chord, one character key — and reserves them so no window binding fires them twice; the rest (F5,plus, a second alternative) are matched by the window (events.md). - Where native controls render,
<ContextMenu>, a<MenuBar>'s dropdowns and a<Select>'s drawn list are sized as NSMenu is: 22pt rows, 5pt padding, 11pt separators, the 13pt menu font. - A
<Select>'s list is the real NSMenu, dropped from the trigger as a browser drops a<select>'s;nativeMenuchooses (components.md). - The Dock menu and a tray item's menu take the same item vocabulary (desktop.md).
Custom drawing on a layer tree
<canvas onDraw>, <svg> and a registered element's paint(ctx) run
unchanged, through a CoreGraphics context that speaks the whole dialect:
canvas2d plus fillRects, drawGlyphs, positioned, layoutSubtree and
vw/vh/em.
Offscreen surfaces. react-x11/ntk's Surface is a CoreGraphics bitmap
here, in device pixels. Unlike ntk:
- A surface has one context for its life:
getContext('2d')answers the same object every time, itsdestroy()is a no-op, andrender()brackets its callback insave/restorefrom the identity. format: 'a8'throws: allocateargb32and tint throughfillStyle/globalAlpha.Picture,<image picture>and<image drawable>are X server resources: they draw nothing here and warn once in development. AnImageworks.
Context properties that differ or matter here:
globalCompositeOperationis canvas's whole set, including blend modes (multiplyand the rest) that X11 lacks. An op applies inside what the draw covers, as on X11 and unlike a browser'scopy. A bridge without the verb refuses all butsource-over: assign, then read it back. AdrawGlyphscall'sopis ignored.'copy'plusdrawImage(surface, x, y)at the surface's own size, a whole-pixel translate, no alpha, no shadow and at most a rectangular clip is a row memcpy, not a blend — the cheap way to present a surface every frame (a 125×45 terminal grid in 0.42 ms rather than 1.37). Anything else is drawn, with the same result.globalAlphabelow 1 on a surface draw is cheap wherectx.fadesSurfacesCheaplyis true.imageSmoothingEnabled/imageSmoothingQualityare honoured; a context starts at'medium', not canvas's'low'.'low'makes a surface drawn tile by tile through a matrix far cheaper (784 tiles: 200 ms → 27 ms).filterapplies the colour functions (grayscale(),sepia(),saturate(),hue-rotate(),invert(),brightness(),contrast()),opacity(), andblur()on drawn images only;drop-shadow()andurl()do not stick. Readfilterback to know.clip(rule)honours'evenodd';roundRecttakes canvas's radii,{ x, y }corners included;createPatternis not supported.
An element can hand parts of its drawing to the render server as sprites()
(extending.md).
One that composites its own surface over its box every frame — a terminal, a
chart on a socket — pays that composite and a copy behind it; a layer of the
element's own is designed, not built.
Stroking a path with many subpaths
CoreGraphics strokes in time quadratic in a path's subpaths, so the context
splits a large stroke at subpath boundaries: a path of 4,000 rings strokes
in 113 ms rather than 986. Left whole: a hairline (1 device pixel or less)
and anything that composites — a translucent strokeStyle, globalAlpha
below 1, a shadow. The one visible difference: where two subpaths' strokes
overlap, the antialiased fringe reads a little lighter.
ctx.strokeChunking = false turns it off for one context,
REACT_X11_NO_STROKE_CHUNKING=1 for the process. fill is never split.
A shadow from a tile
On macOS and Windows the context draws the shadow of a rect, a roundRect,
a fillRect or an inset frame of two from a tile made once and
stretched, shared by every card with the same shadow — a page header's inset
shadow went from 130–380 ms to 4 ms.
- A tiled shadow is the canvas spec's gaussian (σ half the blur, three σ wide), as Chrome and X11 draw it. Other shapes, rotations and composite ops get CoreGraphics' own, which is a little narrower.
<box boxShadow>draws on macOS and Windows.
REACT_X11_NO_SHADOW_TILES=1, or ctx._shadowTiles = false on one context,
puts every shadow back on CoreGraphics.
Animations and transforms
transition and animation styles work as everywhere. Where a layer can
express the property — a plain <box>'s backgroundColor, borderColor,
borderWidth, borderRadius and opacity — the render server runs the
animation: no JS frames, and it keeps moving while the JS thread is blocked
(examples/animation.jsx blocks it for two seconds). Everything else — text
colour, layout properties, a node that paints as a raster — runs on the frame
clock exactly as on X11.
- Timing matches the frame clock's easings.
- Interruption: a length retargets smoothly from where it is; a colour restarts from the value on screen.
- Not offloaded:
boxShadow.transformis not a style property on any backend yet.
On the default presenter the offload is
layer promotion.
cocoa: { presenter: 'layers' } gives every drawn node a CALayer instead; it
is opt-in while it is measured, and a pan there re-rasters the whole scene.
Layer promotion: the animated few on their own layers above the surface presenter
The default presenter keeps one bitmap per window and gives the few nodes that animate a CALayer of their own above it while they do. Hit testing never moves: promotion changes pixels and nothing else. With no prop to set, a node is promoted when it is
- a plain
<box>with a transition or a loop onbackgroundColor,borderColor,borderWidth,borderRadiusoropacity— for as long as it has one and a second after; or - a box out of the flow that no pointer lands on (
position: 'absolute',pointerEvents: 'none') with a loop on an inset, which moves it and nothing else —<ProgressBar indeterminate>'s block costs no frames; - and only where nothing paints over it: no later sibling's ink in its bounds, no ancestor's border or focus ring, no ancestor that fades. A clipping ancestor, round corners included, clips the layer. A node in a pane that scrolled in the last 250 ms waits until the pane stops.
A node that stops qualifying returns to the bitmap in the frame that finds it, and its animation carries on on the frame clock from where it was. A scrollbar thumb over promoted rows goes on a layer of its own.
It costs a frame to start, one to come back a second after the last
animation ends, and meanwhile a CALayer, plus for a node with children a
bitmap the size of their reach. cocoa: { promote: false } or
REACT_X11_COCOA_PROMOTE=0 keeps every animation on the frame clock; true
or =1 forces it on whatever the bridge (it is on by default where the
bridge reports sRGB colours, 0.5.1 and later).
On a wide-gamut display (a MacBook's built-in panel, a monitor with its
own profile) every window is colour-managed, as a browser manages a page, so
it looks less saturated than an unmanaged one. Opaque colours agree to the
pixel between bitmap and layer; translucent ones differ slightly, because
Core Animation blends in the display's space. window.snapshot() reads the
display's numbers and carries its profile, so a script comparing raw
snapshot pixels with CSS colours sees a shift.
Design notes and measurements: design/macos.md.
Video on a layer
A <video> goes on a layer of its own under the same
rule: nothing painted over it, no fade, no rounded clip that cannot hold it.
<video frames>: lifted, each pushed frame goes straight to a surface the render server shows, at no cost to the window; declined, the element draws the newest frame into the window. The colours areAVPlayerLayer's. Over a bridge without the video-surface verbs the frame is converted in JavaScript, andapp.videoFormats()listsBGRAfirst.<video src>plays throughAVPlayer: lifted, its player layer shows every frame at no cost to the window; covered, each new frame is copied into the window. Over a bridge without the player verbsuseSupports('mediaPlayback')is false andsrcrefuses, as on X11. In a<Frame>pane the host plays the video and sends the frames down.
Running as an app bundle
AppKit names, icons and identifies an app by the bundle its executable
lives in. Run from the shell, the menu bar says "node", the Dock shows the
generic icon, the defaults domain (window frames, the tab bar) is shared with
every script that runtime ever ran, and the notification centre refuses the
app. cocoa: { appName } fixes the name alone; a bundle fixes all of it.
examples/form/ is the example:
npm run examples:form:app # builds examples/form/build/Guestbook.app and opens it
make-app.sh makes the executable with
bun build --compile at Contents/MacOS/Guestbook: AppKit takes the main
bundle from the running executable's path, so it must be a real binary, not
a script.
- The bridge's addon cannot be compiled in. The bundle carries it as
Contents/Resources/calayers.node, and the entry,main.js, pointsREACT_X11_CALAYERS_PATHat it relative toprocess.execPathbefore importing the app, so the bundle can be moved. Info.plistcarries the name, the identifier, the icon,NSHighResolutionCapable(without it AppKit renders at 1×) andLSEnvironmentnaming the Cocoa backend.lsappinfothen reports the process under the app's name and identifier.
The tab bar under bun. Unbundled, a bun app's windows live in the
bun defaults domain, and once anything turns the tab bar on there (View →
Show Tab Bar, a stray shortcut) every window the bridge opens wears a 28pt
tab bar and every popup lands that much off. Clear it, or run as a bundle:
defaults delete bun "NSWindowTabbingShoudShowTabBarKey-NSWindow-CALBackendDelegate-(null)-VT-FS"
Distributing the bundle
make-app.sh builds a developer's bundle, which runs where it was built;
packaging.md has the recipes. What
differs on macOS:
- Signing. A downloaded app needs a Developer ID signature with the
hardened runtime, then notarization. Sign inside-out, not with
--deep:calayers.nodefirst, without entitlements, then the app with Bun's five entitlements (packaging.md). For a colleague, ad-hoc signing (--sign -) andxattr -dr com.apple.quarantineon their end are the short path. - The Mac App Store requires App Sandbox, under which a
bun build --compilebinary does not start. A Node single executable in the same bundle shape does, so that is the store build; Bun is fine for Developer ID. - Architecture. A compiled binary and the bridge's prebuild are each for
one CPU; a universal bundle is two builds and
lipo, for the executable andcalayers.nodealike. - Size. About 70 MB with bun, nearly all of it the runtime; 142 MB as a Node single executable.
Testing
react-x11/test drives the X11 backend on every platform, macOS
included; there is no 'cocoa' backend for renderX11(). Test the tree,
queries and behaviour there, which are shared. Pixels are per-backend
(CoreText is not ntk's rasterizer), so native bezels, the NSMenu and
pasteboard flavours are checked by running on a Mac
(testing.md). A window's snapshot() resolves to its
composited pixels, in the display's colour profile.
Not supported yet
- VoiceOver: no
NSAccessibilitybridge, and AT-SPI is off here, so a Cocoa app is not reachable by assistive technology yet. Write roles and labels anyway; the bridge will read them (accessibility.md). - Input methods: no
NSTextInputClient— no marked text, Press-and-Hold accents or CJK input (events.md). - Nested
<window>elements throw; a<glarea>is the only child surface. Surfaceformat: 'a8'throws — allocateargb32and tint throughfillStyle/globalAlpha.createPattern, and the X-onlyPicture,<image picture>and<image drawable>.- The eyedropper over a bridge older than 0.9:
useEyedropper().supportedis false — leave the eyedropper button undrawn. useKeepAwake(): no macOS rung.transformas a style property (on any backend).transientForand thesticky,below,skip_taskbarandshadedstates: inert.- A
'cocoa'backend forreact-x11/test. - Dropped images while the app runs on a worker.
- Never, by design:
<foreign>and XEmbed, a window manager (examples/wm.jsx), remoting overssh -X— run the X11 backend through XQuartz for those.