Every prop table below is read from Schema definitions in
packages/core/src/index.ts where a full
table is maintained. The closed tag set and catalog version are
conformance-checked by
scripts/check-catalog-reference.ts
(pnpm run check:catalog-reference) so this page cannot silently drop a shipped
component.
Current catalog marker: CatalogVersion = "effect-native/v43" (v43, issue
#96: cut-corner-surface no longer overlays a full-width top stroke across its
clipped corners). v41, issue #95, made header-line signal and structural
strokes share one continuous baseline. v40, issue #91, made the existing
Frame gain optional bounded Khala static decoration with
a stable caller-owned id and complete DOM, React DOM, and React Native
support/degradation receipts).
v39 added harmonization P1.6, issue #79: matrix axes on the remaining PARTIAL
components from the harmonization audit. Badge/Chip gain variant
(solid/soft/outline) + lattice size; omitting both keeps the exact
pre-v39 tone-colored-text-only look (resolveBadgeAppearance's isLegacy
flag gates every new visual). TextField gains variant
(outline/soft), lattice size, an independent gutterSize override,
invalid, and (on PlainTextFieldView) autoResize for a DOM textarea
that grows to its content (React Native already grows a multiline field
with no fixed height, so autoResize there is a declared, accurate no-op).
Select gains SelectControl-style trigger conventions — variant
(soft/outline/ghost), lattice size, pill, dropdownIcon — plus
additive multi-select (multiple + values, onChange fires the next
selected-values array); React Native's rows-list Select has no trigger to
attach dropdownIcon to, a declared fidelity gap. A brand-new Alert
component (icon + title + body on the full tone x variant matrix) joins the
catalog rather than reshaping StatusBanner in place — see the AlertView
doc comment in packages/core/src/index.ts and the ### Alert section
below for the Alert-vs-StatusBanner decision.
v38 added Spinner + LoadingDots + ShimmerText — indeterminate loading
indicators for Desktop transcript streaming states, tool-card wait states,
and pending text (issue #83, harmonization P2.10). Spinner/LoadingDots
size off the control-lattice icon sub-token and the closed Tone set;
ShimmerText sweeps either real pending text or a skeleton placeholder
width. Determinate circular progress stays a Meter variant — this bump
does not duplicate it. All three honor reduced motion: an explicit
reduceMotion wins, otherwise the renderer bakes in the resolved
prefers-reduced-motion signal via the new
MotionPreferenceService/ViewResolution.reducedMotion, so no component
checks a media query itself.
v37 gave Button the full tone x variant x
size matrix — tone (the 6 matrix tones, default "accent"), variant
(solid/soft/outline/ghost, default "solid"), size (the control
lattice, default "md"), plus pill, loading, block, and selected —
issue #78, harmonization P1.5. Pre-v37 trees using the old
variant: "primary"|"secondary"|"ghost" still decode:
resolveButtonAppearance normalizes them onto their exact tone+variant
equivalents ("primary" -> accent/solid, "secondary" -> secondary/solid,
"ghost" -> accent/ghost). v36 added SegmentedControl — a single-choice
INPUT control distinct from Tabs (no panel association): typed options
(id/label/icon?/disabled?), a typed value + onChange, lattice size,
gutterSize, and pill. DOM renders an animated selection thumb measured
via ResizeObserver against the selected segment's live bounds, sliding
with the #76 named move easing token, plus WAI-ARIA radiogroup/radio
semantics with roving tabindex and arrow/Home/End keyboard nav; React Native
renders pressable segments with a static (non-animated) selection highlight
— issue #81, harmonization P2.8. v35 added CopyButton — the typed
copy-to-clipboard control with the injected Clipboard service, copied-state
feedback, and the typed onCopy / onCopiedReset intents — issue #84,
harmonization P2.11. v34 added Avatar + AvatarGroup — typed identity
marks with the image -> initials -> icon fallback chain, control-lattice
sizes, Tone soft/solid variants, and cutout-overlap groups with a
max/overflow count — issue #80, harmonization P2.7).
Closed component tags (componentTags, 79 total):
Stack, Text, Button, Image, TextField, List,
SectionList, Card, Spacer, Link, Modal, Sheet,
Host, Icon, Divider, Badge, Chip, Meter,
StatTile, Table, SplitPane, NavRail, Workbench, Popover,
DropdownMenu, ContextMenu, Tooltip, Combobox, CommandPalette, Tabs,
Composer, Toggle, Select, Checkbox, RadioGroup, Slider,
NumberField, FieldRow, Toast, ToastRegion, StatusBanner, RecoveryOverlay,
Markdown, Transcript, CodeBlock, DiffView, GraphFigure, Timeline,
Section, Hero, AnnouncementBadge, CtaSection, Footer, NavBar,
Accordion, PricingColumn, PricingTable, LogoRow, StatsBand, Glow,
MockupFrame, Pager, SwipeableListItem, BackgroundGradient, Wallpaper, Spotlight, Frame, BlurredPopup,
IconButton, Toolbar, EmptyMessage, Avatar, AvatarGroup, CopyButton,
SegmentedControl, Spinner, LoadingDots, ShimmerText, Alert.
There is no escape hatch to add an ad hoc component — growing the
catalog is a deliberate, tracked process; see
../../GAPS.md. Foreign imperative surfaces use the reviewed
Host node (also listed above), not an open plugin API.
The tone x variant x size matrix (v37, issue #78): tone is one of the 6
matrix tones (accent, secondary, danger, success, warning, info,
default "accent"); variant is solid/soft/outline/ghost (default
"solid"); size is a control-lattice step (2xs/xs/sm/md/lg/xl,
default "md") that coherently sizes height, horizontal gutter, corner
radius, label font size, and — while loading — the spinner glyph. pill
forces the fully-rounded radius token; block stretches to full width;
loading disables press, marks aria-busy/accessibilityState.busy, and
(DOM only) draws a lattice-icon-sized spinner in place of the label; selected
renders the matrix's selected state and sets aria-pressed/
accessibilityState.selected.
Back-compat: the pre-v37 variant: "primary"|"secondary"|"ghost" tokens are
still accepted and normalize onto their exact tone+variant equivalents via
resolveButtonAppearance (exported from @effect-native/core): "primary"
-> { tone: "accent", variant: "solid" }, "secondary" ->
{ tone: "secondary", variant: "solid" }, "ghost" ->
{ tone: "accent", variant: "ghost" } (already a matrix token, unchanged).
Every renderer calls this one resolver instead of branching on the legacy
strings itself.
Submit lifecycle (v29, #72): disabled fields accept no input and dispatch no
change/submit intents; clearOnSubmit makes the renderer empty the field
locally after dispatching onSubmit (the app's controlled reset to ""
agrees with it). Focused fields still receive app-driven controlled value
changes.
Matrix axes (v38, #79): variant (outline/soft) and lattice size opt a
field into renderer-drawn box chrome (border for outline, tinted fill for
soft, sized from the control lattice); omitting both keeps the exact pre-v38
look — no border, no background, fully style-driven, since that is what
every existing TextField call site already relies on
(resolveTextFieldAppearance's isLegacy flag gates the chrome). gutterSize
independently overrides the horizontal inline padding regardless of variant.
invalid is a wholly new axis: it always reflects aria-invalid and always
draws a danger-tone cue (a border on variant-opted fields, a bottom border
otherwise). PlainTextFieldView (non-secure) additionally accepts
autoResize: with multiline: true, the DOM renderer grows the <textarea>
to its scrollHeight on every input (Textarea parity); React Native's
multiline TextInput already grows with its content whenever nothing
constrains its height, so autoResize there is a declared, accurate no-op
rather than new imperative sizing logic.
Virtualized collection. Optional pull-to-refresh via typed refreshing state and
onRefresh intent (#61).
Grouped virtualized collection. Same optional refreshing / onRefresh as List.
Optional presentationDetents ("half"/"full", GL-1) is a semantic hint for
hosts with native sheet presentation; the required size-token detents still
drive the owned DOM/RN panel lowering.
Matrix axes (v38, #79): variant (solid/soft/outline) and lattice
size opt a badge into the tone x variant color-matrix fill (the closed
Tone set maps onto its matrix-tone equivalent: neutral -> secondary,
info/success/danger unchanged, warn -> warning); omitting both
keeps the exact pre-v38 look (tone-colored text only, no fill/border/sizing)
since that is what every existing Badge call site already renders
(resolveBadgeAppearance's isLegacy flag gates the chrome).
Same matrix axes as Badge (variant, lattice size, same back-compat
isLegacy gate and Tone mapping) applied to the chip's label/value pill.
Submit lifecycle (v29, #72): disabled, submitting (typing stays live for
follow-up drafting, onSubmit dispatch is suppressed and the surface is marked
busy — the typed "submit" key command still fires so apps can queue), and
clearOnSubmit (the renderer empties the editor after dispatching onSubmit).
SelectControl trigger conventions (v38, #79): variant
(soft/outline/ghost — no solid; a trigger is never a
call-to-action), lattice size, and pill opt the trigger into the
tone-neutral (fixed "secondary" tone) matrix box chrome; dropdownIcon
picks the trigger's indicator glyph from the closed IconName set (defaults
to "ChevronDown" once variant opts in). Omitting variant/size keeps
the pre-v38 platform-default <select> look on DOM and the unstyled rows
list on React Native (resolveSelectAppearance's isLegacy flag gates the
chrome). The DOM renderer draws the dropdown-indicator glyph as a
background-image data URI rather than a wrapper element (so the
<select> stays the keyed root and existing element.value = call sites
keep working); that glyph paints a fixed neutral tone rather than the
resolved matrix text color, a declared simplification (CSS custom
properties and currentColor do not resolve inside an externally
referenced SVG image). React Native's rows-list Select has no trigger to
attach dropdownIcon to at all — an existing, now-declared fidelity gap
alongside SegmentedControl's RN thumb-animation gap.
Multi-select (v38, #79) is additive: multiple + values (the selected
list) sit alongside the pre-v38 single-select value/onChange, which keep
their exact prior meaning when multiple is omitted or false. When
multiple is true, onChange fires with the next selected-values array
instead of a single string.
A single-choice INPUT control (v36, #81) — distinct from Tabs: there is no
associated panel/content, only a typed value + onChange. options is a
bounded list (minimum two) of { id, label, icon?, disabled? }; size rides
the shared control lattice (#76) so height/gutter/radius/font/icon size
coherently from one step; gutterSize is the token gap between segments;
pill renders full radius instead of the lattice step's radius. DOM renders
an animated sliding thumb measured via ResizeObserver against the selected
segment's live bounds, transitioning with the named move easing token, plus
WAI-ARIA radiogroup/radio semantics with roving tabindex and arrow/Home/End
keyboard nav. React Native renders pressable segments with a static
(non-animated) selection highlight applied directly to the selected
segment — a real fidelity gap versus DOM's shared sliding thumb, declared
rather than faked.
A persistent single-line app-chrome status row (a connectivity/health bar):
typed tone, message, optional onRetry/onDismiss, bound to
aria-live/role by tone. See ### Alert below for why the harmonization
#79 rich-callout shape landed as a new component instead of reshaping this
one.
New component (v38, harmonization P1.6, issue #79) — an icon + title + body
callout on the full tone x variant matrix (tone, one of the 6 matrix tones,
default "info"; variant, solid/soft/outline/ghost, default
"soft"), typically embedded inline in page/form content (validation
summaries, settings-panel warnings) rather than mounted as persistent app
chrome. icon defaults to a tone-appropriate glyph
(danger -> AlertCircle, warning -> AlertTriangle, success ->
CheckCircle, info/accent/secondary -> InfoCircle) when omitted;
title is optional, message is required; onDismiss is optional.
Alert vs. StatusBanner, decided in-issue: apps-sdk-ui's Alert is a rich
inline callout; our StatusBanner is a narrower persistent single-line
status row with only a message field. Reshaping StatusBanner in place to
carry icon/title/body would change the required shape of every existing
StatusBanner call site and blur its persistent-banner role. Alert is a
distinct new catalog entry instead, so StatusBanner's contract and
rendering are completely unchanged — zero back-compat risk — while the
richer inline-callout shape gets its own typed home, per the GAPS growth
rule (a new named component for a new named use, not a breaking reshape).
Typed, pre-parsed block+inline model (no parser, no raw HTML). Link href
(v28, #71) accepts exactly: http(s)://… absolute URLs, same-origin rooted
paths (/path, optional ?query then #fragment; a leading // is
rejected), and in-page #fragment refs. All other schemes — javascript:,
data:, mailto:, custom schemes — are typed decode/construction failures
(MarkdownLinkHrefSchema).
Message chrome (v29, #72): TranscriptMessage carries optional senderLabel
and timestamp display strings. Renderers draw them in a meta row separated
from the body — never concatenated into body text — with role-differentiated
row treatment: user rows end-aligned bounded bubbles, assistant rows
start-aligned prose, system/tool rows muted.
Typed arbiter-graph model: nodes (id/label/closed kind+status sets,
optional precomputed x/y), edges (endpoints, closed kind set, edge
status extends node statuses with evidence_backed — provenance-backed
links draw in the accent color), typed layout policy and pan/zoom camera
state. Provenance vocabulary (v31): each node may carry a domain-neutral
badge (label + tone — app semantics as data, never new node kinds) and
typed chips (provenance/evidence/datum + opaque ref) whose
activation dispatches onChipSelect with { nodeId, chipId, ref? };
nodeEntry (none/fade/pop) is the typed entry treatment for keyed
nodes newly observed after the first commit (DOM applies it; RN/canvas carry
it as a declared no-op). Intents: onNodeSelect, onNodeHover,
onChipSelect, onCameraChange.
Marketing layout band — width (full/contained), vertical padding, background token, child slot.
Display-scale headline (optional gradient tone), subhead, CTA action row, optional media/mockup slot, start/center align.
Outlined pill above a hero — label, optional action label, optional onPress intent.
Mid-page conversion band — headline, body, tone, action row.
Brand slot, typed columns of links, legal/meta row.
Marketing top navigation — brand, link list with intents, trailing actions, sticky/collapsed state, menu toggle intent.
Disclosure list (FAQ composition) — items with header + content, mode (single/multi), expanded ids, onToggle intent.
Single plan card — name, price, period, features, highlight flag, CTA intent.
Side-by-side PricingColumn children for plan comparison.
Trusted-by logo strip — source/alt items with optional press intents.
Metric band — bound value + label items with optional tone.
Bounded radial accent glow behind a child slot (sm/md/lg intensity).
Device/browser frame around children with optional perspective tilt (none/left/right).
Linear onboarding stepper — ordered steps, activeStepId, progress (dots/bar/none),
back/advance/complete intents, panel content by step id (lazy vs keepMounted).
Swipe-action list row — one child, typed leading/trailing actions (id/label/icon/tone/destructive),
onAction intent, optional full-swipe action id. Compose as a List item.
Token gradient backdrop (vertical/horizontal/radial).
Bounded wallpaper variant (plain/city/mesh) behind children.
Focus glow intensity (sm/md/lg) around a child slot.
Arcade bordered frame (square/rounded/arcade). Optional khala data adds
one of the twelve static Khala motifs through a stable caller-owned id, bounded
logical width/height, optional zoom, density, and forced-color input. The
semantic children remain ordinary View data and the decoration owns no
intent or state.
Blur-backed popup with typed open + onDismiss (overlay presence).
Circular icon-only pressable (44pt hit target) over the closed IconName set.
accessibilityLabel is required — an icon-only button with no accessible name
is not constructible. Optional surface: "glass" for the translucent material.
Floating action strip (glass set, GL-1). Children rendered in a row; semantic
placement (bottom-floating default, or top); optional surface: "glass".
Centered empty-state block for empty panes (issue #82). Optional icon badge
over the closed IconName set with its own bounded tone
(secondary/danger/warning, default secondary) and size (sm/md,
default md); required title; optional muted description; optional
action slot typed as a Button view specifically (an arbitrary view there is
a decode failure). Layout is a centered column on spacing tokens. No
illustrations/images and no loading state (Spinner/LoadingDots/
ShimmerText, issue #83, below).
Identity mark (v34, #80) with the typed fallback chain image (app-supplied
src — the catalog does no remote fetching or identicon generation) ->
initials (bounded to 3 characters) -> icon (closed IconName set). At
least one source is required — an empty avatar is not constructible. size
is a ControlToken on the shared control lattice; tone is the closed Tone
set with a variant of soft (tinted translucent fill, default) or solid
(tone fill + inverse text). A label present means meaningful (aria-label /
role img); absent means decorative. Renderers layer the image absolutely over
the fallback so a failed load reveals it without renderer state.
Overlapping keyed avatars (cutout ring against the background color, first
on top) with an optional positive-integer max; the remainder collapses into
a +N overflow count in the same treatment. Group-level size/tone/
variant are defaults applied to children without their own value and to the
overflow count.
Typed copy-to-clipboard control (v35, #84) for transcript message actions,
diagnostics panels, and code surfaces beyond CodeBlock's built-in copy intent.
content is the string to copy; the write goes through the injected
Clipboard service/driver (renderer option or Layer — never a bare
navigator.clipboard call in the component contract), then the typed onCopy
intent fires with the content as component value. label absent is the
IconButton-shaped icon-only default; present is a Button-shaped icon+label
control. size rides the shared control lattice (sm/md/lg/xl) and
variant reuses the Button vocabulary (primary/secondary/ghost, ghost
default). Copied feedback (Check icon swap + copiedLabel tooltip/live
announcement): the DOM renderer owns uncontrolled per-node feedback and
reverts after resetMillis (default 2000ms); controlled copied data plus
onCopiedReset schedules the typed reset intent on every renderer (React
Native's parity path — RN declares uncontrolled self-feedback unsupported and,
without an injected clipboard, fires onCopy so the app performs the write).
The headless renderer records every write (clipboardWrites, simulateCopy).
Compact indeterminate in-flight ring (v38, #83). size is a ControlToken
sized off the shared control lattice's icon sub-token; tone is the closed
Tone set. Determinate circular progress stays a Meter variant (its
indeterminate flag already covers unknown-duration bars) — this does not
duplicate it. A label present means meaningful (role="status" +
aria-live="polite"); absent means decorative (aria-hidden). reduceMotion
is an explicit override; when unset, renderers bake in the resolved
prefers-reduced-motion signal instead — see "Reduced motion" below.
3-dot pulse loading indicator (v38, #83) with the same size/tone/label/
reduceMotion vocabulary as Spinner.
Shimmer/skeleton sweep (v38, #83) over either real pending text (text) or a
skeleton placeholder bar (width, a Dimension) when no content has arrived
yet — at least one of text/width is required, mirroring the Avatar
fallback-chain discipline (an empty ShimmerText is not constructible).
Optional typeScale sizes the skeleton bar height and the text font/line
height (default body). Not a full skeleton-screen layout system — this is
the text-shimmer primitive only. label/reduceMotion follow the same
convention as Spinner/LoadingDots.
Spinner, LoadingDots, and ShimmerText are the catalog's first
continuously-animating components. Each honors prefers-reduced-motion the
same typed way: an explicit reduceMotion on the view always wins; when
unset, resolveView's ViewResolution.reducedMotion input — populated by
the new MotionPreferenceService (mirrors ViewportService: a live
SubscriptionRef-backed signal) — is baked in as the default before the
tree reaches a renderer. The DOM renderer detects the OS-level media query
exactly once, at mount, and keeps it live via the query's own change event;
no component ever checks @media (prefers-reduced-motion) itself. When
reduced, DOM renders the identical static markup with no @keyframes
animation attached (gated by a data-en-motion="auto"|"reduced" attribute).
React Native renders all three as an honest static affordance
unconditionally today (no Animated dependency exists yet in this
dependency-free catalog), which trivially satisfies "falls back to a static
affordance" for that renderer since there is no motion to fall back from; a
live native animation loop is an additive, demand-gated follow-up tracked in
GAPS.md, not a gap in this contract.
Every component accepts these two, inherited from NodeBase:
| Field | Type | Notes |
|---|---|---|
key |
string (optional) |
Required (enforced by the schema, not just convention) on any view placed inside a List/SectionList/Link children array. |
catalogVersion |
"effect-native/v43" |
Set automatically by every constructor function — you never pass this yourself. |
Styles never take raw numbers or hex strings (except a few explicitly
numeric fields like borderWidth, flex, and pixel Dimensions) — they
take a token from one of these closed vocabularies
(packages/tokens/src/index.ts):
| Token kind | Values |
|---|---|
SpacingToken |
0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64 |
ColorToken |
background, surface, textPrimary, textMuted, accent, danger, border, focus |
RadiusToken |
none, sm, md, lg, xl, full |
TypeScaleToken |
caption, body, label, title, heading |
BreakpointToken |
sm, md, lg, xl |
DimensionToken |
xs, sm, md, lg, xl, full |
ControlToken |
2xs, xs, sm, md, lg, xl — the shared control size lattice (paired height, gutter, radius, font size, icon size per step, #76); sizes Avatar/AvatarGroup/SegmentedControl |
SurfaceMaterial |
glass — style-level surface token on box-derived styles (GL-1): translucent blurred material on DOM, translucent theme surface + hairline border on RN core |
A Dimension is a DimensionToken or a non-negative pixel number — used
for width/height/estimatedItemSize and similar layout fields.
Every component that renders a box or text accepts an optional style whose
shape is a subset of the full StyleProperties — a Card, for instance,
cannot set fontWeight (it has no text), and a Spacer cannot set
backgroundColor (it has no paint). The subsets, from the source:
| Component style | Keys |
|---|---|
StackStyle, ListStyle, CardStyle |
margin/padding family, gap*, size/flex family, backgroundColor, borderColor, borderRadius, borderWidth (*Stack only) |
TextStyle |
margin family, size/flex family, color, typeScale, fontWeight, textAlign |
ButtonStyle, LinkStyle, TextFieldStyle |
the box keys above plus color, typeScale, fontWeight, textAlign |
ImageStyle |
margin family, size/flex family, borderRadius |
SpacerStyle |
margin family, size/flex family only — no paint, no color |
Any style value can additionally carry variants: { state?, platform?, breakpoint? }, resolved by the runtime (not a CSS cascade) — state covers
pressed/focused/disabled, platform covers web/ios/android, and
breakpoint covers the four breakpoint tokens above. Stack.direction,
Stack.gap, Stack.padding, and Image.width/height additionally accept
a ResponsiveValue<T> directly ({ base: T, sm?: T, md?: T, lg?: T, xl?: T }) without going through style.variants.
Bound<T>(contentonText,title/openonModal,openonSheet) isT | Binding— either a literal value orBinding(path), which reads live state at that path when the runtime resolves the view.IntentRef(onPress,onChange,onSubmit,onEndReached,onDismiss) is a name plus an optional payload template — see the intents chapter.NavigationDestination(Link.destination) is one of{ kind: "url", href, target? },{ kind: "path", path, replace? }, or{ kind: "anchor", id }.
export interface StackProps {
readonly key?: string
readonly direction: ResponsiveValue<"row" | "column">
readonly gap?: ResponsiveValue<SpacingToken>
readonly align?: "start" | "center" | "end" | "stretch"
readonly justify?: "start" | "center" | "end" | "between" | "around"
readonly padding?: ResponsiveValue<SpacingToken>
readonly style?: StackStyle
}
// Stack(props: StackProps, children: ReadonlyArray<View> = []): StackViewimport { Stack, Text } from "@effect-native/core"
const row = Stack({ key: "row", direction: "row", gap: "2", align: "center", justify: "between" }, [
Text({ key: "label", content: "Left", variant: "body" }),
Text({ key: "value", content: "Right", variant: "body" })
])export interface TextProps {
readonly key?: string
readonly content: string | Binding
readonly variant: "caption" | "body" | "label" | "title" | "heading"
readonly color?: ColorToken
readonly weight?: "regular" | "medium" | "semibold" | "bold"
readonly style?: TextStyle
}
// Text(props: TextProps): TextViewimport { Binding, Text } from "@effect-native/core"
const bound = Text({
key: "message",
content: Binding(["message"]),
variant: "body",
color: "accent",
weight: "semibold"
})export interface ButtonProps {
readonly key?: string
readonly label: string
readonly variant: "primary" | "secondary" | "ghost"
readonly disabled?: boolean
readonly onPress: IntentRef
readonly style?: ButtonStyle
}
// Button(props: ButtonProps): ButtonViewimport { Button, IntentRef, StaticPayload } from "@effect-native/core"
const confirm = Button({
key: "confirm",
label: "Confirm",
variant: "secondary",
disabled: false,
onPress: IntentRef("Confirm", StaticPayload({ ok: true }))
})export interface ImageProps {
readonly key?: string
readonly source: string // must start with a URI scheme, e.g. "https:" or "data:"
readonly alt: string
readonly width?: ResponsiveValue<Dimension>
readonly height?: ResponsiveValue<Dimension>
readonly fit?: "contain" | "cover" | "fill"
readonly style?: ImageStyle
}
// Image(props: ImageProps): ImageViewimport { Image } from "@effect-native/core"
const avatar = Image({
key: "avatar",
source: "https://example.com/avatar.png",
alt: "User avatar",
width: 48,
height: 48,
fit: "contain"
})TextField is two schema variants that TypeScript keeps mutually exclusive:
a secure: true field cannot also be multiline, and only a non-secure
field may be multiline.
export interface BaseTextFieldProps {
readonly key?: string
readonly value: string
readonly placeholder?: string
readonly label?: string
readonly field?: { readonly form: string; readonly field: string }
readonly focused?: boolean
readonly onChange?: IntentRef
readonly onSubmit?: IntentRef
readonly style?: TextFieldStyle
}
export type TextFieldProps =
| (BaseTextFieldProps & { readonly secure: true; readonly multiline?: false })
| (BaseTextFieldProps & { readonly secure?: false; readonly multiline?: boolean })
// TextField(props: TextFieldProps): TextFieldViewimport { ComponentValueBinding, FieldBinding, IntentRef, TextField } from "@effect-native/core"
const search = TextField({
key: "search",
value: "",
placeholder: "Search notes",
onChange: IntentRef("SearchChanged", ComponentValueBinding()),
multiline: false
})
const password = TextField({
key: "password",
value: "",
label: "Password",
field: FieldBinding("login", "password"),
secure: true
})field: FieldBinding(form, field) is the hook into the Schema-backed form
layer (defineFormSpec, setFormFieldValue, submitForm, and friends) —
out of scope for this guide; see packages/core/src/index.ts and
../proof.md.
export interface ListProps {
readonly key?: string
readonly style?: ListStyle
readonly virtualize?: boolean // defaults to false via the constructor
readonly estimatedItemSize?: Dimension // required by the schema when virtualize is true
readonly onEndReached?: IntentRef
readonly endReachedThreshold?: number
}
// List(props: ListProps, items: ReadonlyArray<View & { key: string }>): ListViewimport { List, Text, type View } from "@effect-native/core"
const keyed = <V extends View>(view: V): V & { readonly key: string } => view as V & { readonly key: string }
const items = List({ key: "items", virtualize: true, estimatedItemSize: 48, endReachedThreshold: 0.5 }, [
keyed(Text({ key: "row-1", content: "First", variant: "body" })),
keyed(Text({ key: "row-2", content: "Second", variant: "body" }))
])export interface SectionListProps {
readonly key?: string
readonly style?: ListStyle
readonly virtualize?: boolean
readonly estimatedItemSize?: Dimension // required by the schema when virtualize is true
readonly onEndReached?: IntentRef
readonly endReachedThreshold?: number
readonly stickyHeaders?: boolean
}
export interface SectionListSection {
readonly key: string
readonly header: View
readonly items: ReadonlyArray<View & { key: string }>
}
// SectionList(props: SectionListProps, sections: ReadonlyArray<SectionListSection>): SectionListViewimport { SectionList, Text, type View } from "@effect-native/core"
const keyed = <V extends View>(view: V): V & { readonly key: string } => view as V & { readonly key: string }
const grouped = SectionList({ key: "grouped", stickyHeaders: true }, [
{
key: "today",
header: Text({ key: "today-header", content: "Today", variant: "label" }),
items: [keyed(Text({ key: "today-1", content: "Ship the guide", variant: "body" }))]
}
])export interface CardProps {
readonly key?: string
readonly padding?: SpacingToken
readonly radius?: RadiusToken
readonly style?: CardStyle
}
// Card(props: CardProps, children: ReadonlyArray<View> = []): CardViewimport { Card, Text } from "@effect-native/core"
const card = Card({ key: "card", padding: "4", radius: "lg" }, [
Text({ key: "card-text", content: "Card body", variant: "body" })
])Same either/or shape as TextField: a fixed size or flex: true,
never both.
export type SpacerProps =
| { readonly key?: string; readonly size: SpacingToken; readonly flex?: false; readonly style?: SpacerStyle }
| { readonly key?: string; readonly flex: true; readonly style?: SpacerStyle }
// Spacer(props: SpacerProps): SpacerViewimport { Spacer } from "@effect-native/core"
const fixedGap = Spacer({ key: "gap", size: "4" })
const flexiblePush = Spacer({ key: "push", flex: true })Link.children accepts only Text, Image, or Spacer nodes, and must
have at least one — the schema rejects both an empty children array and any
disallowed child type.
export interface LinkProps {
readonly key?: string
readonly destination:
| { readonly kind: "url"; readonly href: string; readonly target?: "self" | "blank" }
| { readonly kind: "path"; readonly path: string; readonly replace?: boolean }
| { readonly kind: "anchor"; readonly id: string }
readonly style?: LinkStyle
}
// Link(props: LinkProps, children: ReadonlyArray<TextView | ImageView | SpacerView>): LinkViewimport { Link, Text } from "@effect-native/core"
const docsLink = Link(
{ key: "docs-link", destination: { kind: "url", href: "https://effect-native.dev", target: "blank" } },
[Text({ key: "docs-link-label", content: "Read the docs", variant: "body" })]
)export interface ModalProps {
readonly key?: string
readonly title: string | Binding
readonly open: boolean | Binding
readonly dismissable: boolean
readonly size: "xs" | "sm" | "md" | "lg" | "xl" | "full"
readonly onDismiss: IntentRef
// no `style` field — Modal has no style contract in the current catalog
}
// Modal(props: ModalProps, children: ReadonlyArray<View> = []): ModalViewimport { Binding, IntentRef, Modal, Text } from "@effect-native/core"
const confirmModal = Modal(
{
key: "confirm-modal",
title: "Delete note?",
open: Binding(["modals", "confirmDelete"]),
dismissable: true,
size: "sm",
onDismiss: IntentRef("DismissConfirmModal")
},
[Text({ key: "confirm-modal-body", content: "This can't be undone.", variant: "body" })]
)export interface SheetProps {
readonly key?: string
readonly open: boolean | Binding
readonly dismissable: boolean
readonly edge: "bottom" | "side"
readonly detents: ReadonlyArray<"xs" | "sm" | "md" | "lg" | "xl" | "full"> // 1 to 3 entries
readonly onDismiss: IntentRef
}
// Sheet(props: SheetProps, children: ReadonlyArray<View> = []): SheetViewimport { IntentRef, Sheet, Text } from "@effect-native/core"
const shareSheet = Sheet(
{
key: "share-sheet",
open: true,
dismissable: true,
edge: "bottom",
detents: ["sm", "lg"],
onDismiss: IntentRef("DismissShareSheet")
},
[Text({ key: "share-sheet-body", content: "Share this note", variant: "body" })]
)Modal and Sheet share one rule, enforced by a schema-level check on the
full view tree (OverlayStackFilter), not by any individual component: at
most one Modal and one Sheet may appear in a tree, and neither may nest
inside the other. A tree with two modals, or a sheet inside a modal, fails
to decode.
This page covers every top-level prop on every catalog component. It does
not re-derive the full Style variant grammar, the responsive-breakpoint
struct shape, or the Schema-backed form layer (FormSpec, FormState,
submitForm, and friends) in the same exhaustive way — those are documented
in prose in earlier chapters and are otherwise best read directly from
packages/core/src/index.ts, which is
the single source of truth this page is generated against.