Skip to main content

Components

The components are the React Native ones, with the same names and, where the engine implements a prop, the same meaning. This page lists what each takes. Props shared by every component come first.

Shared props​

Every host component accepts these, on top of its own.

PropTypeNotes
stylestyle object or arraySee Styles. Arrays and falsy entries are flattened, as in React Native
visiblebooleanfalse prunes the subtree from layout, rendering and hit-testing without unmounting it; the nodes and their React state stay. A display in style wins over it
refRef<NodeHandle>Receives the engine node handle, for updateVector/updateText
onLayout(e) => voide.nativeEvent.layout (also e.layout) is {x, y, width, height} relative to the parent, before any scroll offset or transform, after the commit that laid it out
onTouchStart, onTouchMove, onTouchEnd, onTouchCancel(e) => voidRaw touches. They bubble from the node that was hit up through its ancestors, whatever the responder system decides. onTouchCancel fires when a sequence is abandoned rather than finished

A touch event carries {type, x, y, dx, dy, vx, vy}: the point in screen pixels, the distance traveled since touch-down, and the velocity in px/ms over the most recent move. That makes a flick onTouchEnd={e => e.vx > 0.4 && next()}, with no gesture recogniser needed.

Responder props. Every component also accepts the gesture responder handlers (onStartShouldSetResponder, onMoveShouldSetResponder, their Capture variants, onResponderGrant/Move/Release/Terminate/Reject and onResponderTerminationRequest). A should-set predicate returning true claims the gesture through real negotiation in the engine: the capture phase asks root to leaf, then bubble asks leaf to root, and a granted responder owns the touch stream, which stops a ScrollView ancestor auto-scrolling under it. Most apps use PanResponder rather than wiring these directly.

View​

The building block: a box with flexbox layout, background, borders, shadow, and transform.

<View
style={{flex: 1, padding: 16, backgroundColor: '#0d2035', borderRadius: 12}}>
{children}
</View>

View has no props beyond the shared ones. Everything it draws comes from style.

Text​

<Text style={{color: '#e6edf5', fontSize: 20}} numberOfLines={1}>
Hello, {name}
</Text>
PropTypeNotes
numberOfLinesnumberTruncate past this many lines; 0 (default) does not truncate
ellipsizeMode'head' | 'middle' | 'tail' | 'clip'How truncated text is cut. tail and clip are implemented; head and middle are deferred

Children may be a string, an interpolation (Hi {name}), or nested <Text> runs with their own style, which become inline spans on one engine node (up to 4 spans). Text draws with the built-in font unless a baked fontFamily is set; see Assets for sizes and glyphs.

Flow B: one level of nested spans; a span cannot carry a state-driven style; a <Text> compiles to a single formatted string, so a ternary in text cannot mix a string branch with a numeric one. numberOfLines and ellipsizeMode must be constants, and ellipsizeMode is tail or clip.

Image​

import logo from './assets/logo.png';
<Image source={logo} style={{width: 64, height: 64}} resizeMode="contain" />;
PropTypeNotes
sourceasset name or {uri}The name an asset import resolves to. React Native's numeric require() id is not accepted
resizeMode'cover' | 'contain' | 'stretch' | 'repeat' | 'center'Default 'cover'
tintColorcolourRecolours the image, keeping its alpha; for monochrome icons
imageNamestringThe engine asset name, when set directly instead of through source

An image must be baked into the app; see Assets. An opaque image, or one baked as RGB565, is drawn through the engine's copy fast path with no per-pixel blending.

borderRadius and the per-corner radii clip the bitmap to the rounded edge a background with those radii fills, anti-aliased the same way. The clip is the node's border box, so padding moves the picture inward without moving the corners.

Pressable​

<Pressable onPress={() => setCount(c => c + 1)} style={styles.button}>
<Text>count is {count}</Text>
</Pressable>
PropTypeNotes
onPress, onPressIn, onPressOut, onLongPress(e) => voidThe press family. A press looks for the nearest ancestor carrying one of these, so wrapping a subtree in a single Pressable is enough
delayLongPressnumberMilliseconds before onLongPress fires
disabledboolean

TouchableOpacity​

A Pressable that dims while held. Every Pressable prop works unchanged.

PropTypeNotes
activeOpacitynumberOpacity while held, 0 to 1. Default 0.2

The fade runs on the native driver, so it costs no re-render. It owns the node's opacity: animated opacity in style is ignored here (and rejected by the AOT); animate opacity yourself with a Pressable instead. Any other property, transform included, is fine. A dimmed node composites its whole subtree offscreen, so dim the box that reads as the button rather than the surrounding screen.

Flow B: activeOpacity and disabled must be literals, not state.

ScrollView​

<ScrollView style={{flex: 1}} onScroll={e => setY(e.scrollY)}>
{rows.map(r => (
<Row key={r.id} {...r} />
))}
</ScrollView>
PropTypeNotes
onScroll(e) => voide.scrollX, e.scrollY in pixels

It scrolls whichever axis overflows: lay the content out with flexDirection: 'row' for a horizontal scroller. There is no horizontal prop. Scrolling has momentum, and a granted gesture responder inside it (a PanResponder, a Dial adjustable) takes the touch away from the scroller.

FlatList​

A thin wrapper over ScrollView, not a virtualized list: every row mounts as a real engine node and stays mounted.

PropTypeNotes
datareadonly T[]
renderItem({item, index}) => ReactNode
keyExtractor(item, index) => string | number
stylestyle

Both flows perform the same rewrite:

<FlatList data={items} keyExtractor={it => it.id} renderItem={({item}) => <Row item={item} />} />
// renders the same tree as
<ScrollView>
{items.map(item => <Row key={item.id} item={item} />)}
</ScrollView>

Write renderItem as ({item, index}) => …; the AOT reads that destructuring literally. Those four are the only props either flow honours: horizontal, numColumns, onEndReached, ListHeaderComponent, ItemSeparatorComponent and the other virtualization knobs have no meaning here, so the AOT rejects them, Flow A warns once and ignores them, and the types reject them. For headers, footers, or separators use a ScrollView with .map directly.

Every row mounts and stays mounted, and each row's nodes come out of a fixed pool (ERUI_MAX_NODES) that is small on the boards that need lists most:

ConfigERUI_MAX_NODES
Engine default512
ESP32-S3 (800×480)512
ESP32 CYD44
RP2040-Touch-LCD 1.69"48
Simulator4096

Overflow is silent: rows past the cap never appear. A 40-row list of 3-node rows needs 120 slots before the rest of the UI, more than the two MCU boards above have in total. For a long list, paginate it, render a fixed window against a scroll offset you own, or split it across screens. Virtualization is a deliberate non-goal: it would cost a React commit on every scroll frame, and these panels show a few dozen rows, not thousands.

SectionList​

Also, a thin ScrollView wrapper: no virtualization, no sticky headers. Headers, rows, and footers are flat siblings, as in React Native, so the FlatList node budget applies plus a header and a footer per section.

PropTypeNotes
sectionsArray<{data, key?, renderItem?, keyExtractor?, ...}>A section carries its rows plus any fields of your own; per-section renderItem and keyExtractor override the list-wide ones
renderItem({item, index, section}) => ReactNodeindex is within the section
renderSectionHeader, renderSectionFooter({section}) => ReactNode
keyExtractor(item, index) => string | number
stylestyle

Flow A only for now; the AOT has no SectionList lowering yet.

Button​

React Native's pre-styled button: a Pressable around one centred Text, two nodes. It takes no style, as upstream; build the Pressable and Text yourself when you need one.

PropTypeNotes
titlestring
onPress(e) => void
colorcolourFill colour, replacing the default blue
disabledbooleanGreys it out and detaches onPress

Accessibility, TV-focus, and testID props are accepted and ignored. Flow A only for now.

Button, ImageBackground and SectionList are JavaScript over the primitives, as in React Native, so they add no engine node type. They exist, so a React Native source pastes in and runs; reach for the primitives directly when you want control. The AOT has no lowering for them yet and says which tree to write instead.

ImageBackground​

A View with an Image stretched behind its children, for the case Image cannot cover because it takes no children.

PropTypeNotes
stylestyleThe container: the box the picture fills (its content box, so padding insets the picture)
imageStylestyleMerged over the fill: borderRadius, opacity, tintColor
imageRefRef<NodeHandle>The Image node; the outer ref is the View
every Image propsource, resizeMode, tintColor go to the image

Flow A only for now.

TextInput​

<TextInput
style={styles.field}
value={ssid}
placeholder="Network name"
onChangeText={setSsid}
onSubmitEditing={connect}
/>
PropTypeNotes
value, placeholder, placeholderTextColor
editableboolean
secureTextEntrybooleanDraws a dot per character
onChangeText(text) => void
onSubmitEditing, onFocus, onBlur(e) => void

Focus brings up the engine's on-screen keyboard, in builds that include it (ERUI_ONSCREEN_KEYBOARD). Its done key calls onSubmitEditing, then takes the focus away. Its layout and colours are configurable with setKeyboardConfig, below. The style may set cursorColor.

Switch​

PropTypeNotes
valueboolean
onValueChange(value) => void
trackColor{false?, true?}
thumbColorcolour

Flow B: a Switch with onValueChange needs a value prop, and disabled is not supported.

ActivityIndicator​

PropTypeNotes
animatingbooleanDefault true
style.colorcolourThe spinner's tint, as in React Native

The spinner animates in the engine.

<Modal visible={open} backdropColor="#00000099" style={styles.sheet}>
{content}
</Modal>
PropTypeNotes
visiblebooleanShows and hides the modal (this is the modal's own switch, not the shared visible)
backdropColorcolourThe scrim painted behind it; also settable in style

Give a modal an explicit full-screen style ({position: 'absolute', left: 0, top: 0, width: screen.width, height: screen.height, alignItems: 'center', justifyContent: 'center'}): the engine lays a modal out as an ordinary flex child, so without one the sheet is flex-sized rather than centred on screen. The AOT honours the same style. Flow B: visible is required, and animationType can only be 'none'.

Dial​

The engine's native arc widget: dials, gauges, and progress rings as one node, drawn analytically, animated natively, with built-in drag-to-set.

<Dial
style={{width: 200, height: 200}}
min={50}
max={90}
value={target}
thickness={14}
cap="round"
trackColor="#1f3550"
indicatorColor="#38bdf8"
knob="circle"
knobSize={22}
adjustable
onChange={setTarget}
/>

Angles are degrees clockwise from 3 o'clock; the default is a 270° sweep starting at 135°.

PropTypeNotes
valuenumber | AnimatedValueThe current value in [min, max]; animatable on the native driver
min, maxnumber
startAngle, sweepAnglenumberDegrees
stepnumberDrag quantisation, default 1
thicknessnumberTrack and indicator thickness in px; default a tenth of the smaller side
bandThickness, bandColorAn optional wider backing band behind the track
trackColor, indicatorColorcolour
indicatorGradient{type: 'conic' | 'radial', stops: [{color, offset?}]}Replaces indicatorColor. Conic sweeps the stops along the angle, radial across the thickness
cap'butt' | 'round'
segments, gapAnglenumberSplit the arc into N segments separated by gapAngle degrees (default 2)
knob'none' | 'circle' | 'image' | 'child''child' positions the first child on the value point, for custom or multiple knobs
knobSize, knobColor, knobBorderColor, knobBorderWidth, knobImage
adjustablebooleanThe knob follows the finger in the engine; onChange gets the quantised value. It claims the gesture ahead of any ScrollView
rangebooleanDual-setpoint mode: the indicator spans [valueStart, value] with a knob at each end. A drag latches the nearer end, and the ends clamp against each other
valueStartnumber | AnimatedValueRange mode: the low end
minSpannumberRange mode: the minimum separation. Above 0, a drag that would close the gap pushes the other end along
onChange(value, valueStart) => void

A value change repaints only the swept sliver and the knob, not the whole dial. Hit-testing is ring-only, so the hole and the unswept gap fall through to what is behind.

Flow B: indicatorGradient must be a static object (2 to 4 stops); onChange an inline function or a useCallback; knobImage a static asset.

Svg​

<Svg> is one engine node, a vector shape tape. The shape elements inside it are descriptive children flattened into that tape, like text inside <Text>: they take no style, no events and no ref, and a component that returns them is never mounted (inline the shapes, or call it as a plain function). Arrays and fragments of shapes are unwrapped, so a mapped list works.

import gauge from './gauge.svg';

<Svg source={gauge} width={120} height={120} /> // a baked file

<Svg width={200} height={200}> // shapes described in JSX
<Circle cx={100} cy={100} r={80} fill="none" stroke="#1f3550" strokeWidth={12} />
<Arc cx={100} cy={100} r={80} startAngle={-135} endAngle={angle}
fill="none" stroke="#38bdf8" strokeWidth={12} strokeLinecap="round" />
</Svg>
PropTypeNotes
sourceimported .svgBaked at build time to a vector tape (or a raster, if the file uses text, masks or filters). Scaled to the render box; shape children are ignored
width, heightnumber or 'N%'The render box, react-native-svg style; a style width/height wins. A plain numeric string is ignored; Flow B refuses it, and takes 'N%' only without viewBox/source
viewBox'minX minY w h'Coordinates are baked into the tape against width/height, so give those as numbers. Flow B works width={screen.width} out at boot and scales the drawing then
paint propsApply to every shape, like a <G> around them all; a <G> or shape sets its own over them. Ignored with source

Paint props, on <Svg>, <G> and every shape: fill, stroke, strokeWidth, strokeLinecap (butt/round/square), strokeLinejoin (miter/round/bevel), strokeMiterlimit, fillRule (nonzero/evenodd), and fillGradient/strokeGradient, a gradient {type: 1 linear | 2 radial | 3 conic, stops: [{color, offset}], ax, ay, bx, by, r} with up to 8 stops. Colors accept #rgb, #rgba, #rrggbb, #rrggbbaa, rgb()/rgba() and the named colours. An unfilled shape defaults to black: set fill="none" on anything meant to be stroke-only.

ShapeProps
<Path>d (M/L/H/V/C/S/Q/T/A/Z, absolute and relative). Parsing d costs more than the primitives; prefer them for anything rebuilt per frame
<Circle>cx, cy, r
<Ellipse>cx, cy, rx, ry
<Rect>x, y, width, height, rx, ry (a negative radius is auto, as in SVG; each is clamped to half its side)
<Line>x1, y1, x2, y2
<Arc>cx, cy, r, startAngle, endAngle: degrees clockwise from 12 o'clock. Not a standard SVG element; it emits a native arc op with no bezier conversion, cheap enough to rebuild every drag frame
<G>Groups shapes under one paint and an optional x/y/translateX/translateY/scale, composed with enclosing groups

Limits: 16 shapes per <Svg> and 8 vector nodes at once, by default (ERUI_VECTOR_PAINTS_MAX, ERUI_MAX_VECTOR_NODES). Combine many tick marks into one <Path> with several subpaths.

Flow B: a state-driven <Svg> uses absolute pixels (no viewBox, no <G>) and literal shape children; a state-driven <Path d> is not supported, and visible or an event on an <Svg> is rejected (wrap it in a View or Pressable).

Intentionally absent React Native APIs​

A microcontroller has no operating system: no app switcher, no system chrome, no browser, no settings service, no other app to hand anything to. The React Native modules that wrap those services have nothing to wrap here, so Embedded React does not ship them. Their absence is a decision, not missing work, and it will not change. Reaching for one fails at build time (No matching export … for import "StatusBar" in Flow A, AOT: unknown element <StatusBar> in Flow B) rather than silently doing nothing on the device.

React Native APIWhy it does not apply on an MCUInstead
StatusBarThere is no OS status bar; your app owns every pixel from bootDraw your own header <View>
SafeAreaView, safe-area insetsNo notch, no home indicator, no system overlay: the whole framebuffer is the safe area<View style={{flex: 1}}>, plus padding for a physical bezel
AppStateNothing to background to; the app is the firmware and runs from boot to power-offSleep, wake and dim are firmware decisions in your host loop
Appearance, useColorSchemeNo OS light/dark setting to readOwn the theme in app state and pass it down
LinkingNo URL handler, no browser, no second app to deep-link into
AccessibilityInfo, accessibility* propsNo assistive-technology service on the device
AlertNo OS dialog service<Modal>, a real engine node you style yourself
Dimensions, useWindowDimensionsThe panel cannot resize or rotate; its size is fixed and known at build timeThe screen global
Vibration, Share, Clipboard, PermissionsAndroid, BackHandlerPhone and OS services with no embedded counterpartDrive the hardware (a haptic motor, buttons) from firmware
NativeModules, Turbo ModulesNo OS module registry; the firmware already is the native sideFlow B: useHostValue. Flow A: ErRuntimeConfig.install_host_globals
fetch, XMLHttpRequest, WebSocketQuickJS ships no networking, and there is no socket stack underneath itDo the I/O in firmware (Wi-Fi, BLE) and feed values into the UI

Two related notes:

  • Platform.OS is 'embedded'. There is no 'ios' or 'android' branch to take; write Platform.select({embedded: …, default: …}).
  • Absent-but-planned is a different list. Gaps that will close (backends, AOT subset limits, open API decisions) are in the roadmap; the permanent scope calls live in its non-goals section.

PanResponder​

Recognizes drags, swipes, and flings through the engine's responder system. Create it once per component and keep it in a ref; re-creating it per render loses the drag.

const pan = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (e, g) => Math.abs(g.dx) > 8,
onPanResponderMove: (e, g) => setOffset(g.dx),
onPanResponderRelease: (e, g) => settle(g.vx),
}),
).current;

<View {...pan.panHandlers} style={styles.track} />;

The config is React Native's: the four should-set predicates, onPanResponderGrant/Move/ Release/Terminate/Reject, onPanResponderTerminationRequest, and onPanResponderStart/End for extra fingers. Only the Android-specific onShouldBlockNativeResponder is absent. Every callback gets (event, gestureState), where gestureState is one mutable object reused for the responder's lifetime with stateID, moveX/moveY, x0/y0, dx/dy, vx/vy and numberActiveTouches; read what you need during the callback rather than keeping it.

Flow B: compiles the same config onto the engine's C negotiation, so the gesture needs no JavaScript on the device. onPanResponderStart/End are the exception (Flow A only, rejected by name).

Imperative updates​

For continuous gestures where a React render per pointer move is too slow: take the node handle from a ref, push updates directly, and commit the result back to React state when the gesture ends.

const needle = useRef(null);
// on each move:
updateVector(
needle.current,
[
{
line: [cx, cy, cx + r * Math.sin(a), cy - r * Math.cos(a)],
stroke: '#38bdf8',
strokeWidth: 3,
},
],
[cx - r, cy - r, 2 * r, 2 * r],
);
  • updateVector(handle, shapes, dirtyRect?) sets an <Svg> node's geometry from primitive descriptors, skipping React and the d parser. Each shape has exactly one geometry key (arc, circle, line, rect, path) plus paint in the short spellings (fill, stroke, strokeWidth, cap, join, miter, fillRule, fillGradient, strokeGradient). The optional dirtyRect [x, y, w, h], in node-local pixels, limits the repaint to that region.
  • updateText(handle, text) sets a <Text> node's content without touching its style. A later React render reverts it cleanly.
  • setKeyboardConfig(config?) replaces the on-screen keyboard's layout and colours (panelColor, keyColor, keyActiveColor, labelColor, fontSize, rowHeight, keyGap, keyRadius, gridColumns, and layers as rows of keys with char, label, layer, backspace, done, span, highlight). Pass nothing to restore the built-in QWERTY. A no-op in builds without the keyboard.

Measured on the ESP32-S3, an imperative updateVector cut the JavaScript cost of a dial drag by about 98% compared with a render per move. See Performance.