I've been doing native mobile development for a while and one thing that always bugged me was the tradeoff between cross-platform tools and actual native UI. Flutter and React Native solve the "write once" problem but you're not really getting native components. And writing everything twice in Swift and Kotlin is just exhausting.
So I built Melody. You define your UI in YAML, write your logic in Lua, and it renders real SwiftUI on Apple platforms and Jetpack Compose on Android. No web views, no bridge, no JS runtime.
Here's what a full screen looks like:
screens:
- id: home
path: /
title: Home
state:
count: 0
body:
- component: text
text: "{{ 'Tapped ' .. state.count .. ' times' }}"
style:
fontSize: 24
fontWeight: bold
- component: button
label: Tap me
onTap: |
state.count = state.count + 1
That's it. Change the YAML, hot reload picks it up over WebSocket, and the app updates instantly.
Why YAML and Lua?
YAML handles the layout. It's declarative, easy to read, and easy to diff in git. Lua handles the logic — it's tiny, fast, and embeddable. No npm, no bundler, no node_modules. The whole dev experience is just editing files and saving them.
What's in the box
There are 23+ built-in components that map directly to native views — text, buttons, stacks, lists, grids, forms, charts, toggles, pickers, sliders, inputs, and more. Styling is inline in a style block with support for padding, colors, shadows, border radius, animations, etc. You reference theme colors with "theme.colorName" and they resolve automatically including dark mode.
State is reactive. You assign to state.key in Lua and only the components that reference that key re-render. No diffing, no virtual DOM.
Networking is built in with melody.fetch() which is non-blocking under the hood using coroutines, so your Lua reads linearly but doesn't freeze the UI. There's also WebSocket support, persistence, cross-screen events, timers, and clipboard access.
Navigation
Navigation is path-based with dynamic route params:
- id: profile
path: /profile/:id
onMount: |
local res = melody.fetch("https://api.example.com/user/" .. params.id)
if res.ok then state.user = res.data end
Tab bars, sheets, alerts, and full navigation stacks are all supported. Tabs even adapt between sidebar on iPad/Mac and tab bar on iPhone with a single config.
Custom components
Custom components work like you'd expect — define once in YAML with props, use them anywhere:
components:
UserCard:
props:
name: ""
avatar: ""
body:
- component: stack
direction: horizontal
style: { spacing: 12, padding: 16 }
children:
- component: image
src: "{{ props.avatar }}"
style: { width: 48, height: 48, borderRadius: 24 }
- component: text
text: "{{ props.name }}"
Plugins
Plugins let you extend Melody with native code. A plugin is a git repo with Swift and Kotlin implementations that register functions into Lua under their own namespace. You declare them in app.yaml and run melody plugins install. Useful for things like keychain access, analytics, or anything that needs platform APIs.
Getting started
Install the CLI with Homebrew, scaffold a project, and run it:
brew tap josejuanqm/tap
brew install melody
melody create MyApp
cd MyApp
melody dev
It generates the Xcode project and Android boilerplate for you. The dev server handles hot reload.
I'm currently using Melody to build a real app so this isn't just a toy — it's been tested against real use cases with networking, auth flows, dynamic lists, and complex navigation.
It supports iOS, iPadOS, macOS, tvOS, visionOS, and Android.
Web, Windows, and Linux support is planned and I'd love to work on it if theres traction.
The repo is here:
josejuanqm
/
melody
A declarative UI framework that interprets YAML configuration and Lua scripting into fully native SwiftUI and Jetpack Compose — no web views, no hybrid layers.
Melody
Build native apps with YAML and Lua. One codebase, every Apple platform + Android.
No JSX, no bridge, no bundler — describe your UI in YAML, write your logic in Lua, and Melody renders it as real native UI. SwiftUI on Apple platforms, Jetpack Compose on Android.
app:
name: MyApp
theme:
primary: "#6366f1"
screens:
- id: home
path: /
title: Home
state:
count: 0
body:
- component: text
text: "{{ 'Tapped ' .. state.count .. ' times' }}"
style:
fontSize: 24
fontWeight: bold
- component: button
label: Tap me
onTap: |
state.count = state.count + 1
melody.log("count is now " .. state.count)
That's a full screen. Change the YAML, hot reload, see it instantly.
Why
I wanted to build apps fast without fighting tooling. React Native…
Would love to hear any feedback or questions.
Top comments (0)