Your first app#

Scaffold a Lynx project, run it on a simulator and make it reactive — in about five minutes.

Prerequisites#

You'll need Node.js 22+ and the native toolchain from Installation. The scaffolded app can check it for you with npx sigx doctor.

Scaffold the app#

Using the CLI#

Terminal
pnpm create @sigx@latest hello-lynx -- --kind lynx --install
cd hello-lynx
pnpm dlx sigx doctor

This is the plain-styles template, which the samples below are based on. Add --styling daisyui for daisyUI components or --styling tailwind for Tailwind classes. With another package manager, add --pm pnpm (or yarn, bun) and use its spelling for the commands below. doctor prints a fix: line for anything that needs attention.

Run on a simulator or emulator#

Terminal
pnpm run run:android    # or: npm run run:ios (macOS only)

run:android builds the app, installs it and launches it. With nothing connected, it boots your most recently used Android emulator. It then keeps a dev server running, so edits to src/App.tsx hot-reload straight into the running app. The first Android build downloads Gradle and its dependencies and takes a few minutes; later builds are incremental.

Once the app is installed, npm run dev (sigx dev) is all you need day to day. It starts the dev server and a dashboard that lists every simulator, emulator and device it can see. Select one and press Enter to install the app if it's missing and launch it. You don't run run:ios or run:android alongside it. Use those for a one-off build on one platform, or for a --release build.

A physical iPhone or iPad also needs a signing team. Set your Apple Team ID for the command, for example SIGX_IOS_DEVELOPMENT_TEAM=AB12CD34EF npx sigx run:ios --device "My iPhone". The CLI usage guide covers devices and release builds.

Your first component#

Open src/App.tsx and replace it with:

TSX
import { component } from '@sigx/lynx';

const App = component(() => {
    return () => (
        <view style={{ padding: '24px' }}>
            <text style={{ fontSize: '24px', fontWeight: '600' }}>Hello, Lynx!</text>
        </view>
    );
});

export default App;

component, signal and defineApp come from @sigx/lynx, and component() works the same way it does on the web. Only the elements differ. Instead of HTML tags you use Lynx's lowercase native elements, such as <view>, <text>, <image> and <scroll-view>, and each one renders to a real native view. Inline style values are CSS strings with units, like '24px', as in the scaffold. A bare number is treated as px. See Native elements and Styling & layout.

src/main.tsx mounts App with defineApp(<App />).mount(null); you don't need to change it.

Adding reactivity#

TSX
import { signal, component } from '@sigx/lynx';

const App = component(() => {
    const count = signal(0);

    return () => (
        <view style={{ display: 'flex', flexDirection: 'column', padding: '24px', gap: '12px' }}>
            <text style={{ fontSize: '32px' }}>{count.value}</text>
            <view
                bindtap={() => count.value++}
                style={{ padding: '12px', backgroundColor: '#3b82f6', borderRadius: '12px' }}
            >
                <text style={{ color: '#fff' }}>Tap me</text>
            </view>
        </view>
    );
});

export default App;

bindtap is the native tap event. When count.value changes, only the <text> node that reads it updates: the op streams to the main thread and patches one native view. For press states, long-press and pan gestures, use Pressable and friends from @sigx/lynx-gestures.

Next steps#