# Native UI and styling

Controlled inputs, native selectors, macOS composition, settings windows, and Uniwind.

Source: https://legend.so/spark/ui/

Legend Spark: Experimental — not for production.

These guides cover source SDK APIs; the published preview can differ. [Check release status.](https://legend.so/spark/limitations/)

## [Common controls](https://legend.so/spark/ui/#common-controls)

For Markdown and HTML readers, inputs, and editable previews, see [Rich text](https://legend.so/spark/rich-text/).

Import `Button`, `TextInput`, `Select`, and `SegmentedControl` from `@legendapp/spark/ui`:

```tsx
import { useState } from 'react';
import { View } from 'react-native';
import { Button, TextInput, Select } from '@legendapp/spark/ui';

export function Preferences() {
  const [name, setName] = useState('');
  const [theme, setTheme] = useState('system');
  return (
    <View>
      <TextInput value={name} onChangeText={setName} accessibilityLabel="Name" />
      <Select
        options={[
          { label: 'System', value: 'system' },
          { label: 'Light', value: 'light' },
          { label: 'Dark', value: 'dark' },
        ]}
        value={theme}
        onValueChange={setTheme}
        accessibilityLabel="Appearance"
      />
      <Button onPress={() => console.log({ name, theme })}>Save</Button>
    </View>
  );
}
```

Controls share `disabled`, `accessibilityLabel`, `testID`, layout `style`, `onError`, and a `ControlRef` supporting `measureInWindow`. Shared refs do not expose backend objects or a universal focus/text mutation API.

TextInput is single-line and supports either controlled `value` or uncontrolled `defaultValue`. Those modes are mutually exclusive; remount to switch modes/reset an uncontrolled field. Defaults initialize once. Controlled desktop edits acknowledge native event counts; mobile controlled editing uses React Native TextInput.

Select and SegmentedControl share unique string-valued options and controlled `value`/`onValueChange`. Labels may repeat; a missing selected value does not implicitly select the first option. A parent that declines a choice retains its current selection.

| Control | macOS | Windows | iOS | Android | Web |
| --- | --- | --- | --- | --- | --- |
| Button | AppKit | WinUI | Expo SwiftUI | Expo Compose | HTML button |
| TextInput | AppKit | WinUI | RN TextInput | RN TextInput | HTML input |
| Select | Popup | ComboBox | Menu picker | Inline choices | HTML select |
| SegmentedControl | Segmented control | Unsupported | Segmented picker | Inline choices | Button group |

`getControlAvailability` is synchronous. Missing/unsupported native controls report through `onError` (default console logging) and render a disabled fallback with current label/text. Invalid props throw during render. A fallback is not a functioning native control.

Mobile Button/Select use optional `@expo/ui@0.2.0-beta.9` with Expo 54; Compose requires a development build. Windows native controls still need validation.

## [Specialized macOS views](https://legend.so/spark/ui/#specialized-macos-views)

| Import | Contract |
| --- | --- |
| `/ui/search` | `TextInputSearch`, controlled/default input, explicit appearance, focus/blur/measurement ref |
| `/ui/sidebar` | Controlled nullable `selectedId`, required `onSelectionChange`, item-array or `SidebarItem` composition, typed context-menu coordinates |
| `/ui/split-view` | `SidebarSplitView` with named `sidebar`/`content` panes, an optional `list` column, grouped title-bar options and provisional/ready layout events |
| `/ui/swipe` | `SwipeActions` with `leadingActions`/`trailingActions`, required `onAction`, and `getSwipeActionsAvailability`; `SwipeAction`/`SwipeActionsProps` types |
| `/ui/glass` | `GlassView`, regular/clear styles and RN color tint; native effect requires macOS 26 |
| `/ui/symbol` | `SFSymbol`, Apple-specific symbol names, layout/accessibility props and missing-symbol errors |

These views have safe synchronous availability queries and `onError`. Unsupported hosts preserve ordinary layout/content and report the limitation. Split layout readiness comes from native events; initial metrics are hints. Zero pane dimensions remain zero. Older macOS retains GlassView children without the effect.

```tsx
import { SidebarSplitView } from '@legendapp/spark/ui/split-view';
import { View, Text } from 'react-native';

<SidebarSplitView
  sidebar={<View><Text>Navigation</Text></View>}
  list={<View><Text>Documents</Text></View>}
  content={<View><Text>Editor</Text></View>}
  titleBar={{ content: { height: 52, material: 'glass' } }}
  onResize={event => console.log(event.phase, event.contentX)}
/>
```

`SidebarSplitView.list` adds a content-list column between the sidebar and detail content. Omit it for a two-pane layout.

`SwipeActions` wraps a row with trackpad actions. Each `SwipeAction` has a unique `id`, `title`, SF Symbol `symbol`, and `color` (`#RRGGBB` or `#RRGGBBAA`). Swipe right to reveal `leadingActions` or left for `trailingActions`; a full swipe runs the first action. `onAction` receives its ID. In recycling lists, key the view by the logical row ID. Optional `dismisses` slides the row away; remove or remount it to recover after failure or undo.

```tsx
import { SwipeActions } from '@legendapp/spark/ui/swipe';
import { Text } from 'react-native';

<SwipeActions
  trailingActions={[{ id: 'archive', title: 'Archive', symbol: 'archivebox', color: '#007AFF' }]}
  onAction={id => console.log(id)}
>
  <Text>Project notes</Text>
</SwipeActions>
```

## [Settings windows](https://legend.so/spark/ui/#settings-windows)

`/settings/window` exports `SettingsWindow`, `VirtualizedSettingsWindow`, named props/pages, and `createSettingsWindowOptions`. Both use `{ id, title, render }` pages and an explicit `windowId`. Use controlled `selectedPageId`/`onSelectionChange` or mount-time `defaultPageId`, with one selection owner.

This composition requires `@legendapp/list` and macOS split-view support. It shows a hidden window only after layout/initial-scroll readiness. When native split view is unavailable, the fallback has no readiness event and the settings window stays hidden. Handle `onError`; check availability before selecting this composition for a target.

## [Styling](https://legend.so/spark/ui/#styling)

`/ui/uniwind` exports the same four common controls with optional `className` integration. Classes shape layout around native controls; arbitrary OS chrome styling is not promised. Explicit style takes precedence. `/ui/classnames` is a library-specific `clsx`/`tailwind-merge` convenience.

Use ordinary React Native views/text for surrounding screens. Uniwind supports application theme choices `system`, `light`, and `dark`; theme persistence belongs to the app. Windows Appearance overrides update mounted WinUI controls through the native theme module.

See [UI contracts](https://github.com/LegendApp/legend-spark/blob/main/docs/ui.md) and [styling setup](https://github.com/LegendApp/legend-spark/blob/main/docs/styling.md). Router integration and a broad cross-platform component catalog remain outside the supported scope.

[View Markdown](https://legend.so/spark/ui.md)[Report a documentation correction](https://github.com/LegendApp/legend-docs/issues/new?title=Docs%3A+Native+UI+and+styling&body=Page%3A+https%3A%2F%2Flegend.so%2Fspark%2Fui%0A%0AWhat+is+incorrect+or+missing%3F%0A%0AExpected+behavior+or+supporting+source%3A%0A)
