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"
| Quill | Compiles 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
| Prop | Description |
|---|---|
showsVerticalScrollIndicator | Show/hide the vertical scroll bar |
contentContainerStyle | Style applied to the scroll content container |
ListHeaderComponent | Component rendered at the top of the list |
ScrollView
| Prop | Description |
|---|---|
keyboardShouldPersistTaps | Controls keyboard dismiss behavior on tap |
contentContainerStyle | Style applied to the scroll content container |
TextInput
| Prop | Description |
|---|---|
keyboardType | Keyboard type (e.g., "numeric", "email-address") |
placeholderTextColor | Color of the placeholder text |
value | Current text value (controlled input) |
onChangeText | Callback 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
| Quill | React Native |
|---|---|
font size | fontSize |
font weight | fontWeight |
background color | backgroundColor |
border radius | borderRadius |
margin bottom | marginBottom |
align items | alignItems |
justify content | justifyContent |
padding horizontal | paddingHorizontal |
Navigation
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:
| Module | Quill Import |
|---|---|
| Location | use "expo-location" as Location |
| Notifications | use "expo-notifications" as Notifications |
| Camera | use "expo-camera" as Camera |
| Image Picker | use "expo-image-picker" as ImagePicker |
| File System | use "expo-file-system" as FileSystem |
| Haptics | use "expo-haptics" as Haptics |
Element Reference
Quill maps its element names to React Native components:
| Quill Element | React Native | Use For |
|---|---|---|
view | View | Container / layout |
text | Text | Display text |
scroll | ScrollView | Scrollable container |
image | Image | Display images |
input | TextInput | Text input field |
button | TouchableOpacity | Tappable button |
pressable | Pressable | Advanced pressable |
flatlist | FlatList | Efficient long lists |
safearea | SafeAreaView | Safe area wrapper |
modal | Modal | Modal overlay |
switch | Switch | Toggle switch |
activity | ActivityIndicator | Loading 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"