Mobile Apps with Quill + Expo

Quill compiles to React Native JSX, so you can build real mobile apps using Quill's readable syntax and test them instantly with Expo Go on your phone.

Getting Started

Scaffold a new Expo project:

quill expo my-app
cd my-app
npm install

This creates a complete Expo project with Quill source files, navigation, and example screens.

Project Structure

my-app/
  App.quill              -- Navigation setup
  screens/
    Home.quill           -- Home screen component
    Details.quill        -- Details screen component
  package.json           -- Expo dependencies

Build and Run

-- Compile all Quill files to JSX
quill build --expo App.quill
quill build --expo screens/Home.quill
quill build --expo screens/Details.quill

-- Start Expo
npx expo start

Scan the QR code with Expo Go on your phone to see the app.

Components

Quill components map directly to React functional components. Each component can have state, methods, effects, and a render body.

component HomeScreen:
  state count is 0

  to increment:
    count is count + 1

  to render:
    view style container:
      text style title: "Hello, Quill!"
      text: "Count: {count}"
      button onPress increment:
        text: "Tap me"

This compiles to a React functional component with useState hooks and JSX.

Props

Use with to receive props from navigation or parent components:

-- Receive navigation and route props
component ProfileScreen with navigation route:
  to render:
    view:
      text: "User: {route.params.userId}"
      button onPress goBack:
        text: "Go Back"

  to goBack:
    navigate to "Home"

State & Hooks

State declarations become useState hooks. When you reassign a state variable inside a method, Quill automatically uses the setter function.

component Counter:
  state count is 0
  state name is "World"

  to increment:
    count is count + 1      -- becomes setCount(count + 1)

  to updateName:
    name is "Quill"          -- becomes setName("Quill")

  to render:
    view:
      text: "Hello, {name}! Count: {count}"

Effects

Use use effect for side effects (like React's useEffect):

component DataScreen:
  state data is nothing
  state loading is yes

  -- Run once on mount (empty deps)
  use effect:
    say "Component mounted"

  -- Run when 'data' changes
  use effect when [data]:
    say "Data changed: {data}"

  to render:
    view:
      if loading:
        text: "Loading..."
      otherwise:
        text: "Data: {data}"

Context (Global State)

Use React Context to share state across components without prop drilling. Define a context at the top level, then consume it inside any component.

Defining a Context

-- Top-level context declaration
context ThemeContext is "light"
context UserContext is nothing

This compiles to React.createContext().

Consuming a Context

component ThemedScreen:
  use context ThemeContext as theme

  to render:
    view:
      text: "Current theme: {theme}"

The use context declaration compiles to useContext(ThemeContext). The as keyword lets you name the local variable.

Memo & Callback

Optimize performance with memoized values and callbacks — the Quill equivalents of useMemo and useCallback.

Memoized Values

component ExpensiveScreen:
  state items are []

  memo total is computeTotal(items) when [items]
  memo filtered is items.filter(isActive) when [items]

  to render:
    view:
      text: "Total: {total}"

The memo keyword creates a useMemo hook. The when [deps] clause specifies the dependency array — the value only recomputes when those variables change.

Memoized Callbacks

component ListScreen:
  state items are []

  callback handlePress is with item:
    say item.name
  when [items]

  to render:
    view:
      text: "List"

The callback keyword creates a useCallback hook. Use with to define parameters, then write the body indented below. The when [deps] clause controls when the callback is recreated.

Alerts

Show native mobile alerts with the alert() function. In Expo mode, it compiles to Alert.alert().

component AlertExample:
  to showError:
    alert("Error", "Something went wrong")

  to confirmDelete:
    alert("Delete?", "This cannot be undone")

  to render:
    view:
      button onPress showError:
        text: "Show Error"

The Alert import is added automatically when alert() is used.

Alerts with Buttons

Pass a third argument to alert() with an array of button objects to show custom buttons with handlers:

component DeleteScreen:
  to handleDelete:
    say "Item deleted"

  to confirmDelete:
    alert("Delete?", "Are you sure?", [{ text: "Cancel" }, { text: "Delete", onPress: handleDelete }])

  to render:
    view:
      button onPress confirmDelete:
        text: "Delete Item"

This compiles to Alert.alert("Delete?", "Are you sure?", [{ text: "Cancel" }, { text: "Delete", onPress: handleDelete }]). Each button object can have text, onPress, and style properties.

AsyncStorage

Persist data on the device with simple store and load functions. These compile to AsyncStorage calls.

component SettingsScreen:
  state theme is "light"

  to saveTheme:
    store("userTheme", theme)

  to loadTheme:
    result is load("userTheme")
    theme is result

  to clearTheme:
    removeStore("userTheme")

  to render:
    view:
      text: "Theme: {theme}"
      button onPress saveTheme:
        text: "Save"
QuillCompiles To
store(key, value)await AsyncStorage.setItem(key, JSON.stringify(value))
load(key)JSON.parse(await AsyncStorage.getItem(key))
removeStore(key)await AsyncStorage.removeItem(key)

The @react-native-async-storage/async-storage import is added automatically. Install it with:

npx expo install @react-native-async-storage/async-storage

FlatList

Render long scrollable lists efficiently with flatlist. Quill generates proper renderItem and keyExtractor props.

component UserList:
  state users are []

  to render:
    view:
      flatlist data users key id:
        view style row:
          text: "Name"

The data prop specifies the array, and key specifies which field to use as the key extractor. Children inside the flatlist become the renderItem template. If no key prop is given, a default index-based key extractor is used.

Context Provider Pattern

Wrap parts of your app in a Context Provider to pass dynamic values down to all child components. Use provide with a context name and a with clause for the value:

provide ThemeContext with theme:
  view:
    text: "Hello"

This compiles to <ThemeContext.Provider value={theme}>. Any child component that uses use context ThemeContext as theme will receive the current value. This is the standard React pattern for making global state (themes, user info, settings) available without prop drilling.

Conditional Rendering

Use if / otherwise to conditionally render different content. This works inside the to render body of any component.

If / Otherwise

if loading:
  text: "Loading..."
otherwise:
  text: "Done!"

This compiles to a ternary expression: {loading ? (<Text>Loading...</Text>) : (<Text>Done!</Text>)}.

If Only (no otherwise)

if showWarning:
  text: "Warning!"

Without an otherwise block, this compiles to the short-circuit pattern: {showWarning && (<Text>Warning!</Text>)}. The content is rendered only when the condition is truthy.

Reusable Components with Children

Create wrapper components that accept child elements using the children keyword. Declare children in the with clause and use it in the render body:

component Card with children:
  to render:
    view style card:
      children

When children appears in the render body, it compiles to {children} in JSX. This lets you create reusable layout wrappers:

-- Usage:
Card:
  text: "This appears inside the card"
  text: "So does this"

Dynamic Styles

Style values can include ternary expressions, variable references, and arithmetic for dynamic styling based on component state or props:

Ternary in Style Values

style native:
  indicator:
    color is score greater than 70 ? "#22c55e" : "#ef4444"
    font size is 16

The ternary expression is preserved in the compiled StyleSheet.create() output, allowing styles to change based on runtime values.

Variable References and Arithmetic

style native:
  box:
    width is screenWidth * 0.8
    padding is baseSize + 4
    background color is themeColor

Any valid JavaScript expression can be used as a style value, including variables, arithmetic, and function calls.

Multiple Styles

Apply multiple style names to a single element using array syntax:

view style [container, highlighted]:
  text: "Multi-styled"

This compiles to style={[styles.container, styles.highlighted]}. React Native merges the style objects from left to right, so later styles override earlier ones for conflicting properties. This is useful for combining a base style with conditional modifiers.

Advanced Element Props

Quill elements accept all the props supported by their React Native counterparts. Here are commonly used advanced props:

FlatList

PropDescription
showsVerticalScrollIndicatorShow/hide the vertical scroll bar
contentContainerStyleStyle applied to the scroll content container
ListHeaderComponentComponent rendered at the top of the list

ScrollView

PropDescription
keyboardShouldPersistTapsControls keyboard dismiss behavior on tap
contentContainerStyleStyle applied to the scroll content container

TextInput

PropDescription
keyboardTypeKeyboard type (e.g., "numeric", "email-address")
placeholderTextColorColor of the placeholder text
valueCurrent text value (controlled input)
onChangeTextCallback when text changes

Pressable

pressable onPress handleTap onLongPress handleHold:
  text: "Press or hold me"

Accessibility

Add accessibility props to any element:

button onPress handleTap accessibilityRole "button" accessibilityLabel "Submit form":
  text: "Submit"

The accessibilityRole and accessibilityLabel props work on any Quill element and compile directly to their React Native equivalents.

Math, Date & JS Built-ins

Since Quill compiles to JavaScript, all standard JS built-in objects and methods work in Expo mode. You can use them directly in expressions, methods, and render bodies.

Math

result is Math.max(a, b)
rounded is Math.round(score * 100) / 100
display is value.toFixed(2)
area is Math.PI * radius * radius
clamped is Math.min(Math.max(value, 0), 100)

Date

now is new Date()
iso is now.toISOString()
formatted is now.toLocaleDateString()
day is now.getDate()

Array & Object Methods

doubled is items.map(with x: x * 2)
active is items.filter(with x: x.active)
total is items.reduce(with acc x: acc + x.value, 0)
first3 is items.slice(0, 3)
sorted is items.sort(with a b: a.name.localeCompare(b.name))
entries is Object.entries(config)

These all work because Quill compiles to JavaScript. Any valid JS expression or method call can be used anywhere you would use a value in Quill.

Styling

Use style native: blocks inside components. Properties use Quill's natural is syntax and multi-word properties become camelCase automatically.

component StyledScreen:
  to render:
    view style container:
      text style heading: "Styled App"
      text style body: "React Native styles, written naturally."

  style native:
    container:
      flex is 1
      align items is "center"
      justify content is "center"
      background color is "#f0f0f0"
    heading:
      font size is 28
      font weight is "bold"
      color is "#333"
      margin bottom is 12
    body:
      font size is 16
      color is "#666"

This compiles to StyleSheet.create() with proper camelCase property names:

-- Generated:
const styles = StyleSheet.create({
  container: { flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: "#f0f0f0" },
  heading: { fontSize: 28, fontWeight: "bold", color: "#333", marginBottom: 12 },
  ...
});

Style Property Mapping

QuillReact Native
font sizefontSize
font weightfontWeight
background colorbackgroundColor
border radiusborderRadius
margin bottommarginBottom
align itemsalignItems
justify contentjustifyContent
padding horizontalpaddingHorizontal

Set up React Navigation with the app navigation: block:

-- App.quill
app navigation:
  stack:
    screen "Home" component HomeScreen
    screen "Profile" component ProfileScreen
    screen "Settings" component SettingsScreen

Navigate between screens inside components:

-- Navigate to a screen
navigate to "Profile"

-- Navigate with parameters
navigate to "Profile" with { userId: 42, name: "Arhan" }

Access route params via props:

component ProfileScreen with route:
  to render:
    view:
      text: "User ID: {route.params.userId}"
      text: "Name: {route.params.name}"

Tab Navigation

app navigation:
  tab:
    screen "Home" component HomeScreen
    screen "Search" component SearchScreen
    screen "Profile" component ProfileScreen

Expo SDK Imports

Import Expo SDK modules using Quill's use keyword. In Expo mode, imports compile to ESM import statements instead of CommonJS require().

-- Import an entire module
use "expo-location" as Location

-- Import specific items from a module
from "expo-notifications" use scheduleNotification sendNotification

-- Import Expo Camera
use "expo-camera" as Camera

This compiles to:

import * as Location from 'expo-location';
import { scheduleNotification, sendNotification } from 'expo-notifications';
import * as Camera from 'expo-camera';

Common Expo SDK modules you can import this way:

ModuleQuill Import
Locationuse "expo-location" as Location
Notificationsuse "expo-notifications" as Notifications
Camerause "expo-camera" as Camera
Image Pickeruse "expo-image-picker" as ImagePicker
File Systemuse "expo-file-system" as FileSystem
Hapticsuse "expo-haptics" as Haptics

Element Reference

Quill maps its element names to React Native components:

Quill ElementReact NativeUse For
viewViewContainer / layout
textTextDisplay text
scrollScrollViewScrollable container
imageImageDisplay images
inputTextInputText input field
buttonTouchableOpacityTappable button
pressablePressableAdvanced pressable
flatlistFlatListEfficient long lists
safeareaSafeAreaViewSafe area wrapper
modalModalModal overlay
switchSwitchToggle switch
activityActivityIndicatorLoading spinner

Event Handlers

Attach handlers with onPress, onChangeText, etc.:

button onPress handleTap:
  text: "Tap me"

input onChangeText updateName placeholder "Enter name"

Building & Running

Compile a single file

quill build --expo screens/Home.quill
-- Output: screens/Home.jsx

Compile all files

-- Build all screens
for f in screens/*.quill; do quill build --expo "$f"; done

-- Build the app entry point
quill build --expo App.quill

Run with Expo

npx expo start

Scan the QR code with Expo Go to see your app running on your phone.

Full Example: Todo App

component TodoApp:
  state todos are []
  state newTodo is ""

  to addTodo:
    if newTodo is not equal to "":
      todos is [...todos, { text: newTodo, done: no }]
      newTodo is ""

  to render:
    view style container:
      text style title: "My Todos"
      view style inputRow:
        input onChangeText setNewTodo value newTodo placeholder "Add a todo..." style input
        button onPress addTodo style addButton:
          text style addText: "+"
      scroll:
        for each todo in todos:
          view style todoItem:
            text: todo.text

  style native:
    container:
      flex is 1
      padding is 20
      background color is "#fff"
    title:
      font size is 28
      font weight is "bold"
      margin bottom is 20
    inputRow:
      flex direction is "row"
      margin bottom is 16
    input:
      flex is 1
      border width is 1
      border color is "#ddd"
      border radius is 8
      padding is 12
      font size is 16
    addButton:
      background color is "#6C5CE7"
      width is 48
      height is 48
      border radius is 24
      align items is "center"
      justify content is "center"
      margin left is 8
    addText:
      color is "#fff"
      font size is 24
    todoItem:
      padding is 16
      border bottom width is 1
      border bottom color is "#eee"