npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-swiftui-dsl

v0.1.1

Published

A SwiftUI-inspired declarative DSL for building React Native UIs with chainable modifiers, theme system, and two-way bindings.

Readme

Chainable modifiers • Theme-aware colors • Two-way bindings • Zero JSX • Config-free defaults


What if building React Native screens felt as natural as SwiftUI? This framework replaces verbose JSX and scattered StyleSheet objects with chainable, type-safe function calls that read like a blueprint of your UI.

VStack(
  Text('Welcome Back').font('title').bold(),
  Text('Track your practice sessions').secondary(),
  Button('Get Started', () => navigate('home'), { style: 'filled' }),
  Spacer(),
)
.padding('lg')
.background('card')
.cornerRadius('md')
.shadow()

No angle brackets. No style objects. Just clean, readable, composable code.


Table of Contents


Why?

Building UIs in React Native often means juggling three separate concerns that should be one:

| Pain Point | Traditional RN | With this DSL | | -------------------- | -------------------------------------------------- | ----------------------------------------------------------- | | Verbose markup | Deeply nested <View> / <Text> JSX | Flat function calls: VStack(Text(...)) | | Scattered styles | StyleSheet.create at the bottom of every file | Inline chainable modifiers: .padding('lg').bold() | | Manual theming | useColorScheme() + conditional colors everywhere | Token-based: .background('card') auto-resolves light/dark | | Form boilerplate | value + onChangeText on every input | Two-way bindings: TextInput($form.email) | | Config overhead | Theme providers required before anything works | Built-in defaults: works out of the box with zero config |

The result? Less code, fewer bugs, and UIs you can actually read.


DSL vs JSX

Here's the same profile card built both ways:

Traditional React Native:

import { View, Text, StyleSheet, Image, Pressable } from 'react-native';

function ProfileCard({ user }: { user: User }) {
  const colorScheme = useColorScheme();
  const isDark = colorScheme === 'dark';

  return (
    <View style={[styles.card, { backgroundColor: isDark ? '#1E293B' : '#FFF' }]}>
      <Image
        source={{ uri: user.avatar }}
        style={styles.avatar}
        resizeMode="cover"
      />
      <Text style={[styles.name, { color: isDark ? '#F9FAFB' : '#111827' }]}>
        {user.name}
      </Text>
      <Text style={[styles.bio, { color: isDark ? '#94A3B8' : '#6B7280' }]}>
        {user.bio}
      </Text>
      <Pressable
        style={styles.button}
        onPress={() => navigateToEdit()}
      >
        <Text style={styles.buttonText}>Edit Profile</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  card: { padding: 24, borderRadius: 12, alignItems: 'center', gap: 12 },
  avatar: { width: 80, height: 80, borderRadius: 20 },
  name: { fontSize: 22, fontWeight: '700' },
  bio: { fontSize: 14, textAlign: 'center' },
  button: { backgroundColor: '#10B981', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 12 },
  buttonText: { color: '#FFF', fontWeight: '600' },
});

With react-native-swiftui-dsl:

import { VStack, Text, Image, Button, DSLRenderer } from 'react-native-swiftui-dsl';

function buildProfileCard(user: User) {
  return VStack(
    Image({ uri: user.avatar }, { resizeMode: 'cover' })
      .frame({ width: 80, height: 80 })
      .cornerRadius('lg'),
    Text(user.name).font('title').bold(),
    Text(user.bio).font('footnote').secondary().textAlign('center'),
    Button('Edit Profile', () => navigateToEdit(), { style: 'filled' }),
  )
  .padding('lg')
  .background('card')
  .cornerRadius('md')
  .alignment('center')
  .spacing(12);
}

Same result. 70% less code. Colors auto-resolve for light and dark mode. No StyleSheet needed.


Features

Layout & Primitives

  • 20 built-in primitives -- VStack, HStack, ZStack, Text, Image, Button, Toggle, TextInput, ScrollStack, LazyList, SectionedList, SafeArea, Spacer, Divider, Icon, Spinner, Link, Raw, Modal, ProgressBar

Styling

  • 60+ chainable modifiers -- padding, font, background, cornerRadius, shadow, border, opacity, position, zIndex, frame, hidden, fontFamily, italic, underline, strikethrough, and many more
  • Token-based theme -- Define color, spacing, font, and border-radius tokens once. They auto-resolve for light/dark mode

Data Flow

  • Two-way bindings -- SwiftUI-style createBinding and bindForm for effortless form handling
  • Declarative control flow -- If(), ForEach(), Group() instead of ternaries and .map()

Developer Experience

  • Full TypeScript support -- IntelliSense for every modifier, every token, every primitive
  • Zero JSX -- Build entire screens with pure function calls
  • Config-free defaults -- Works out of the box without wrapping your app in a theme provider
  • iOS + Android -- Works with React Native and Expo out of the box

Installation

npm install react-native-swiftui-dsl

Peer dependencies

# Required
npm install react react-native react-native-safe-area-context

# Optional - only needed for specific features
npm install @expo/vector-icons   # For Icon primitive (FontAwesome icons)
npm install expo-router           # For ScreenConfigRenderer navigation

| Dependency | Version | Required | Notes | | -------------------------------- | --------- | -------- | ------------------------------------------------------------------------------- | | react | >= 18.0.0 | Yes | | | react-native | >= 0.72.0 | Yes | | | react-native-safe-area-context | >= 4.0.0 | Yes | For SafeArea primitive | | @expo/vector-icons | >= 14.0.0 | No | For Icon primitive. Without it, icons render as text fallback. | | expo-router | any | No | For ScreenConfigRenderer. Without it, screen navigation modifiers are no-ops. |


Quick Start

Option A: Config-Free (zero setup)

The framework ships with built-in defaults based on the iOS Human Interface Guidelines. No provider needed:

import { VStack, Text, Button, DSLRenderer } from 'react-native-swiftui-dsl';

function buildWelcomeScreen() {
  return VStack(
    Text('Hello, World!').font('header').bold(),
    Text('Your first DSL screen').secondary(),
    Button('Continue', () => console.log('Tapped!'), { style: 'filled' }),
  )
  .padding('lg')
  .spacing(16)
  .alignment('center');
}

export default function WelcomeScreen() {
  return <DSLRenderer builder={buildWelcomeScreen()} />;
}

That's it. No DSLThemeProvider, no StyleSheet, no color conditionals, no boilerplate.

When no DSLThemeProvider is present, useDSLTheme() returns the built-in defaultThemeConfig with colorScheme: 'light'. All tokens, colors, and modifiers work exactly the same.

Option B: Custom Theme

Wrap your app root with DSLThemeProvider to supply your own design tokens:

import { DSLThemeProvider } from 'react-native-swiftui-dsl';

export default function App() {
  return (
    <DSLThemeProvider
      config={{
        colors: {
          light: { text: '#111827', background: '#F9FAFB', tint: '#10B981', card: '#FFF', secondaryText: '#6B7280' },
          dark:  { text: '#F9FAFB', background: '#0F172A', tint: '#34D399', card: '#1E293B', secondaryText: '#94A3B8' },
        },
        fonts: {
          size: { micro: 10, small: 11, caption: 12, footnote: 13, body: 17, subtitle: 20, title2: 22, title: 28, header: 34, hero: 40 },
          weight: { regular: '400', medium: '500', semibold: '600', bold: '700' },
          lineHeight: { tight: 16, normal: 22, relaxed: 28, loose: 34 },
        },
        layout: {
          spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
          borderRadius: { sm: 4, md: 8, lg: 16 },
        },
      }}
      colorScheme="light"
    >
      {/* Your app content */}
    </DSLThemeProvider>
  );
}

Config-Free Usage

DSLThemeProvider is fully optional. The framework includes a complete defaultThemeConfig that provides sensible defaults for every token:

// These tokens resolve automatically -- no provider needed:
Text('Hello').font('title').bold()          // fontSize: 28, fontWeight: '700'
VStack(...).padding('lg')                   // padding: 24
VStack(...).background('card')              // backgroundColor: '#F2F2F7' (light)
VStack(...).cornerRadius('md')              // borderRadius: 8
Button('Go', handler, { style: 'filled' }) // backgroundColor: '#007AFF'

The built-in defaults use iOS system colors and standard spacing scales. You can override any or all of them by adding a DSLThemeProvider whenever you're ready.

Single Color Scheme Support

The DSLColorConfig type is a union that accepts either a dual-scheme object or a single flat palette:

import { DSLThemeConfig } from 'react-native-swiftui-dsl';

// Dual scheme (light + dark):
const dualConfig: DSLThemeConfig = {
  colors: {
    light: { text: '#000', background: '#FFF', tint: '#007AFF', /* ... */ },
    dark:  { text: '#FFF', background: '#000', tint: '#0A84FF', /* ... */ },
  },
  // ...
};

// Single scheme (same colors for both modes):
const singleConfig: DSLThemeConfig = {
  colors: {
    text: '#000',
    background: '#FFF',
    tint: '#007AFF',
    // ...
  },
  // ...
};

When a single DSLColors object is provided (no light/dark keys), normalizeColors() automatically mirrors it to both schemes.


Examples

Simple Stat Card

A styled card showing a single metric -- great for dashboards:

import { VStack, HStack, Text, Icon, Spacer } from 'react-native-swiftui-dsl';

function buildStatCard(title: string, value: string, icon: string) {
  return VStack(
    HStack(
      Icon(icon, { size: 20, color: 'tint' }),
      Spacer(),
      Text(title).font('caption').secondary(),
    ),
    Text(value).font('header').bold(),
  )
  .padding('lg')
  .background('card')
  .cornerRadius('md')
  .shadow();
}

Modal Dialog

Present content in a modal overlay with binding-driven visibility:

import { VStack, Text, Button, Modal, createBinding } from 'react-native-swiftui-dsl';

function buildModalExample(isVisible: boolean, setVisible: (v: boolean) => void) {
  const $visible = createBinding(isVisible, setVisible);

  return VStack(
    Button('Show Modal', () => setVisible(true), { style: 'outlined' }),

    Modal($visible, { animationType: 'fade', transparent: true },
      VStack(
        Text('Hello from Modal!').font('title').bold(),
        Text('This is a DSL-powered modal.').secondary(),
        Button('Dismiss', () => setVisible(false), { style: 'filled' }),
      )
      .padding('lg')
      .background('card')
      .cornerRadius('lg')
      .alignment('center'),
    ).onDismiss(() => console.log('dismissed')),
  );
}

Progress Indicator

Display a determinate progress bar with customizable colors:

import { VStack, Text, ProgressBar } from 'react-native-swiftui-dsl';

function buildProgressExample(progress: number) {
  return VStack(
    Text(`${Math.round(progress * 100)}% complete`).font('caption').secondary(),
    ProgressBar(progress, { trackColor: 'separator', progressColor: 'tint' })
      .cornerRadius('sm'),
  )
  .padding('md')
  .spacing(8);
}

Form with Bindings

Two-way bindings make forms effortless -- no manual value / onChangeText wiring:

import { VStack, TextInput, Toggle, Button, bindForm } from 'react-native-swiftui-dsl';

function buildSettingsForm(form: FormData, setForm: Setter) {
  const $form = bindForm(form, (key, value) => {
    setForm((prev) => ({ ...prev, [key]: value }));
  });

  return VStack(
    TextInput($form.name)
      .inputLabel('Display Name')
      .placeholder('Enter your name'),

    TextInput($form.email)
      .inputLabel('Email')
      .keyboardType('email-address')
      .autoCapitalize('none'),

    TextInput($form.bio)
      .inputLabel('Bio')
      .multiline(4)
      .maxLength(200),

    Toggle($form.notifications, { label: 'Push Notifications' }),

    Button('Save', () => handleSave(), { style: 'filled' }),
  )
  .padding('lg')
  .spacing(16);
}

Data List with Pull-to-Refresh

Virtualized list with conditional empty states and infinite scroll:

import {
  SafeArea, VStack, HStack, Text, LazyList, Spacer,
  If, Spinner, Divider,
} from 'react-native-swiftui-dsl';

function buildSessionsList(sessions: Session[], isLoading: boolean) {
  return SafeArea(
    VStack(
      Text('Practice Sessions').font('header').bold(),

      If(isLoading,
        () => Spinner('large'),
        () => LazyList(sessions, {
          keyExtractor: (s) => s.id,
          renderItem: (session) =>
            HStack(
              VStack(
                Text(session.title).font('subtitle').bold(),
                Text(session.date).font('caption').secondary(),
              ),
              Spacer(),
              Text(`${session.shots} shots`).foregroundColor('tint'),
            ).padding('md'),
        })
        .separator(() => Divider())
        .refreshControl(() => reload(), false)
        .onEndReached(() => loadMore(), 0.5)
        .emptyComponent(() =>
          Text('No sessions yet. Start practicing!').secondary().textAlign('center')
        ),
      ),
    ).spacing(16),
  );
}

API Reference: Primitives

Every primitive is a factory function that returns a chainable ViewBuilder.

Layout

| Primitive | Signature | Description | | ------------- | -------------------------- | ----------------------------------------------------- | | VStack | VStack(...children) | Vertical stack (flexDirection: column) | | HStack | HStack(...children) | Horizontal stack (flexDirection: row) | | ZStack | ZStack(...children) | Overlay stack (children layered on top of each other) | | Spacer | Spacer() | Flexible space (flex: 1) | | Divider | Divider() | Hairline separator | | SafeArea | SafeArea(...children) | Safe area wrapper with configurable edges | | ScrollStack | ScrollStack(...children) | Scrollable vertical container |

Content

| Primitive | Signature | Description | | ------------- | ------------------------------ | ---------------------------------------------------------------------------- | | Text | Text(string) | Text display with rich styling modifiers | | Image | Image(source, options?) | Image with resize modes and frame control | | Icon | Icon(name, options?) | FontAwesome icon (falls back to text without @expo/vector-icons) | | Spinner | Spinner(size?) | Activity indicator ('small' or 'large') | | ProgressBar | ProgressBar(value, options?) | Determinate progress bar (0-1 range) with customizable track and fill colors |

Input

| Primitive | Signature | Description | | ----------- | --------------------------------- | ---------------------------------------------------------- | | TextInput | TextInput(binding) | Text field with two-way binding | | Toggle | Toggle(binding, options?) | Boolean switch with two-way binding | | Button | Button(title, action, options?) | Pressable button -- 'filled', 'outlined', or 'plain' | | Link | Link(title, url) | Tappable link that opens a URL |

Lists

| Primitive | Signature | Description | | --------------- | ---------------------------------- | ------------------------------------------ | | LazyList | LazyList(data, options) | Virtualized list (FlatList) | | SectionedList | SectionedList(sections, options) | Sectioned virtualized list (SectionList) |

Overlay

| Primitive | Signature | Description | | --------- | --------------------------------------- | ---------------------------------------------------------------------- | | Modal | Modal(binding, options?, ...children) | Modal overlay with slide/fade animations and binding-driven visibility |

Utility

| Primitive | Signature | Description | | --------- | ------------------- | --------------------------------------- | | Raw | Raw(reactElement) | Embed any React element in the DSL tree |


DSL-to-React-Native Component Mapping

Every DSL primitive maps to standard React Native components at render time:

| DSL Primitive | React Native Component | SwiftUI Equivalent | | --------------- | -------------------------------- | --------------------------------- | | VStack | View (flexDirection: column) | VStack | | HStack | View (flexDirection: row) | HStack | | ZStack | View (position layering) | ZStack | | Text | Text | Text | | Image | Image | Image | | Button | Pressable + Text | Button | | TextInput | TextInput | TextField | | Toggle | Switch | Toggle | | Spacer | View (flex: 1) | Spacer | | Divider | View (hairlineWidth) | Divider | | Icon | FontAwesome / Text fallback | Image(systemName:) | | Spinner | ActivityIndicator | ProgressView | | ProgressBar | View (track + fill) | ProgressView | | ScrollStack | ScrollView | ScrollView | | LazyList | FlatList | List / LazyVStack | | SectionedList | SectionList | List with sections | | SafeArea | SafeAreaView | .ignoresSafeArea() | | Modal | Modal | .sheet() / .fullScreenCover() | | Link | Pressable + Linking.openURL | Link | | Raw | Passthrough (any ReactElement) | UIViewRepresentable |


API Reference: Modifiers

All modifiers are chainable and return the same ViewBuilder instance. There are 60+ modifiers organized into categories.

Text Modifiers

Text('Hello World')
  .font('title')             // FontSizeToken | number
  .fontWeight('bold')        // FontWeightToken
  .bold()                    // shortcut for fontWeight('bold')
  .semibold()                // shortcut for fontWeight('semibold')
  .medium()                  // shortcut for fontWeight('medium')
  .light()                   // shortcut for fontWeight('light')
  .thin()                    // shortcut for fontWeight('thin')
  .heavy()                   // shortcut for fontWeight('heavy')
  .black()                   // shortcut for fontWeight('black')
  .italic()                  // fontStyle: 'italic'
  .underline()               // textDecorationLine: 'underline'
  .strikethrough()           // textDecorationLine: 'line-through'
  .fontFamily('Courier')     // custom font family
  .foregroundColor('tint')   // color token or hex value
  .secondary()               // shortcut for foregroundColor('secondaryText')
  .caption()                 // shortcut for font('caption')
  .textTransform('uppercase')
  .letterSpacing(0.5)
  .lineHeight(24)
  .lineLimit(2)
  .textAlign('center')

Font weight tokens: regular (400), medium (500), semibold (600), bold (700) are required in every theme config. The tokens thin (100), ultralight (200), light (300), heavy (800), black (900) are optional -- the framework provides built-in fallbacks if your theme config doesn't define them.

Layout Modifiers

VStack(/* ... */)
  .padding('md')                      // SpacingToken | number
  .paddingHorizontal('lg')
  .paddingVertical(8)
  .paddingTop(12)
  .paddingBottom(12)
  .paddingLeft(12)
  .paddingRight(12)
  .margin('sm')
  .marginHorizontal('md')
  .marginVertical(8)
  .marginTop(4)
  .marginBottom(4)
  .marginLeft(8)
  .marginRight(8)
  .flex(1)
  .frame({ width: 100, height: 50 })
  .spacing(12)                        // gap between children
  .gap(8)                             // alias for spacing
  .justifyContent('spaceBetween')
  .alignItems('center')
  .alignment('center')                // SwiftUI-style alignment shortcut
  .flexWrap()
  .position('absolute')
  .positionEdges({ top: 0, left: 0 })
  .zIndex(10)
  .overflow('hidden')
  .aspectRatio(1.5)
  .alignSelf('center')
  .display('none')
  .hidden()                           // shortcut for display('none')

Style Modifiers

VStack(/* ... */)
  .background('card')                 // color token or hex
  .backgroundAlpha('tint', 0.08)      // background with alpha channel
  .foregroundColor('error')
  .cornerRadius('md')                 // BorderRadiusToken | number
  .border(1, 'cardBorder')
  .borderStyle('dashed')              // 'solid' | 'dotted' | 'dashed'
  .shadow()                           // default subtle shadow
  .shadow({ color: 'error', radius: 4, elevation: 2 })
  .opacity(0.5)

Interaction Modifiers

Text('Tap me')
  .onTap(() => handleTap())
  .onLongPress(() => handleLongPress())
  .disabled()

Accessibility Modifiers

Text('Important')
  .accessibilityLabel('Important notice')
  .accessibilityRole('header')
  .accessibilityHint('Double tap to expand')
  .testID('notice-text')

Scroll & List Modifiers

ScrollStack(/* ... */)
  .hideScrollIndicator()
  .contentPadding('md')
  .contentPaddingBottom(32)
  .horizontal()                       // alias for scrollDirection('horizontal')
  .keyboardAvoiding(100)
  .keyboardShouldPersistTaps('handled')
  .bounces(false)
  .refreshControl(onRefresh, isRefreshing)

LazyList(data, { keyExtractor, renderItem })
  .refreshControl(onRefresh, isRefreshing)
  .onEndReached(loadMore, 0.5)
  .separator(() => Divider())
  .numColumns(2)
  .emptyComponent(() => Text('No items'))

TextInput Modifiers

TextInput(binding)
  .placeholder('Enter text...')
  .inputLabel('Email')
  .inputError('Invalid email')
  .keyboardType('email-address')
  .multiline(4)
  .secureEntry()
  .maxLength(100)
  .autoCapitalize('none')
  .returnKeyType('done')
  .inputHeight(56)

Modal Modifiers

Modal(binding, { animationType: 'slide' }, ...children)
  .onDismiss(() => console.log('closed'))

Screen Navigation Modifiers

VStack(/* ... */)
  .screenTitle('Profile')
  .headerRight(() => <SettingsButton />)
  .headerLeft(() => <BackButton />)

DSL Modifier-to-RN Property Mapping

Key modifiers and the React Native style properties they resolve to:

| DSL Modifier | React Native Style Property | Example | | -------------------------------- | ------------------------------------------------------------------- | ----------------------------- | | .padding('md') | padding: 16 | Spacing token resolved | | .paddingHorizontal('lg') | paddingHorizontal: 24 | Edge-specific padding | | .margin('sm') | margin: 8 | Spacing token resolved | | .background('card') | backgroundColor: '#F2F2F7' | Color token resolved | | .backgroundAlpha('tint', 0.08) | backgroundColor: '#007AFF14' | Hex + alpha suffix | | .foregroundColor('tint') | color: '#007AFF' | Text/icon color | | .cornerRadius('md') | borderRadius: 8 | Border radius token | | .border(1, 'separator') | borderWidth: 1, borderColor: '#C6C6C8' | Combined border | | .borderStyle('dashed') | borderStyle: 'dashed' | Direct passthrough | | .shadow() | shadowColor, shadowOffset, shadowOpacity, shadowRadius, elevation | Platform shadow | | .opacity(0.5) | opacity: 0.5 | Direct passthrough | | .font('title') | fontSize: 28 | Font size token resolved | | .fontWeight('bold') | fontWeight: '700' | Weight token resolved | | .bold() | fontWeight: '700' | Shortcut | | .light() | fontWeight: '300' | Fallback weight | | .thin() | fontWeight: '100' | Fallback weight | | .heavy() | fontWeight: '800' | Fallback weight | | .black() | fontWeight: '900' | Fallback weight | | .italic() | fontStyle: 'italic' | Direct passthrough | | .underline() | textDecorationLine: 'underline' | Direct passthrough | | .strikethrough() | textDecorationLine: 'line-through' | Direct passthrough | | .fontFamily('Courier') | fontFamily: 'Courier' | Direct passthrough | | .frame({ width: 100 }) | width: 100 | Dimension mapping | | .flex(1) | flex: 1 | Direct passthrough | | .spacing(12) | gap: 12 | Applied as gap on container | | .alignment('center') | alignItems: 'center' | SwiftUI-style mapping | | .onTap(handler) | Wraps in Pressable with onPress | Interaction wrapper | | .onLongPress(handler) | Wraps in Pressable with onLongPress | Interaction wrapper | | .hidden() | display: 'none' | Visibility toggle | | .disabled() | disabled: true on Pressable / Switch | Interaction state | | .position('absolute') | position: 'absolute' | Direct passthrough | | .zIndex(10) | zIndex: 10 | Direct passthrough | | .overflow('hidden') | overflow: 'hidden' | Direct passthrough | | .aspectRatio(1.5) | aspectRatio: 1.5 | Direct passthrough |


Theme System

The DSL uses a token-based design system. Define your tokens once in DSLThemeConfig, and every modifier resolves them automatically -- including light/dark mode switching.

Color Tokens

Any color modifier (.background(), .foregroundColor(), .border()) accepts either:

  • A token name from your config: 'tint', 'card', 'error', 'secondaryText'
  • A raw value: '#FF0000', 'rgba(0,0,0,0.5)'

Tokens resolve to the correct value based on the current colorScheme -- no manual useColorScheme() checks needed.

DSLColorConfig Union Type

The colors field of DSLThemeConfig accepts a DSLColorConfig union:

type DSLColorConfig =
  | { light: DSLColors; dark: DSLColors }  // Dual-scheme: separate palettes
  | DSLColors;                              // Single-scheme: same for both modes

When you pass a flat DSLColors object, the framework's normalizeColors() function mirrors it to both light and dark automatically.

Spacing Tokens

Padding and margin modifiers accept: 'xs' (4), 'sm' (8), 'md' (16), 'lg' (24), 'xl' (32) -- or raw numbers.

Font Size Tokens

All available font size tokens:

| Token | Default Size | | ---------- | ------------ | | micro | 10 | | small | 11 | | caption | 12 | | footnote | 13 | | body | 17 | | subtitle | 20 | | title2 | 22 | | title | 28 | | header | 34 | | hero | 40 |

Font Weight Tokens

| Token | Weight Value | Required | | ------------ | ------------ | ---------------------- | | regular | 400 | Yes | | medium | 500 | Yes | | semibold | 600 | Yes | | bold | 700 | Yes | | thin | 100 | No (fallback provided) | | ultralight | 200 | No (fallback provided) | | light | 300 | No (fallback provided) | | heavy | 800 | No (fallback provided) | | black | 900 | No (fallback provided) |

Optional weight tokens (thin, ultralight, light, heavy, black) have built-in fallbacks in DSLDefaults.fontWeightFallbacks, so they work even if your theme config only defines the four required weights.

Accessing Theme in Custom Components

import { useDSLTheme } from 'react-native-swiftui-dsl';

function CustomComponent() {
  const { config, colorScheme } = useDSLTheme();
  const bgColor = config.colors[colorScheme].card;
  // Use bgColor in your custom View...
}

If no DSLThemeProvider is present, useDSLTheme() returns defaultThemeConfig with colorScheme: 'light' instead of throwing.


Bindings

Inspired by SwiftUI's $ binding syntax for two-way data flow.

createBinding<T>(value, update)

Creates a single binding for one value:

const [name, setName] = useState('');
const $name = createBinding(name, setName);

TextInput($name).placeholder('Your name')

bindForm<T>(data, setter)

Creates bindings for all fields of a form object at once using a Proxy:

const [form, setForm] = useState({ title: '', notes: '', rating: 0 });

const $form = bindForm(form, (key, value) => {
  setForm((prev) => ({ ...prev, [key]: value }));
});

VStack(
  TextInput($form.title).inputLabel('Title'),
  TextInput($form.notes).inputLabel('Notes').multiline(4),
)

Bindings are cached -- accessing the same field returns the same Binding instance unless the value changes.


Conditionals

If(condition, thenBuilder, elseBuilder?)

VStack(
  If(isLoggedIn,
    () => Text('Welcome back!'),
    () => Button('Log In', login),
  ),
)

ForEach(data, keyExtractor, builder)

VStack(
  ...ForEach(users, (u) => u.id, (user) =>
    HStack(
      Text(user.name).bold(),
      Spacer(),
      Text(user.role).secondary(),
    )
  ),
)

Group(...children)

Groups children without adding a wrapper view (renders as React.Fragment):

Group(
  Text('First'),
  Text('Second'),
)

Configuration

All framework defaults are centralized in DSLDefaults:

| Default | Value | Description | | ------------------------- | --------- | --------------------------------------- | | spacing | 'md' | Default spacing token | | edge | 'all' | Default padding/margin edge | | flex | 1 | Default flex value | | buttonHeight | 48 | Default button height (px) | | buttonCornerRadius | 12 | Default button corner radius (px) | | iconSize | 18 | Default icon size (px) | | pressedOpacity | 0.9 | Button pressed state opacity | | imageResizeMode | 'cover' | Default image resize mode | | onEndReachedThreshold | 0.5 | Infinite scroll trigger threshold | | progressBarHeight | 4 | Default progress bar height (px) | | progressBarCornerRadius | 2 | Default progress bar corner radius (px) |

You can also import the full default theme:

import { defaultThemeConfig } from 'react-native-swiftui-dsl';

This is the same config used automatically when no DSLThemeProvider is present.


Rendering

Every ViewBuilder has a .toElement() method that materializes the builder tree into React elements:

// Option 1: Inline
const element = VStack(Text('Hello')).padding('md').toElement();
return element;

// Option 2: DSLRenderer component
return <DSLRenderer builder={myBuilder} />;

Navigation

For projects using expo-router, the framework includes ScreenConfigRenderer to configure navigation headers:

import { ScreenConfigRenderer } from 'react-native-swiftui-dsl/src/Navigation/ScreenConfigRenderer';

function ProfileScreen() {
  return (
    <ScreenConfigRenderer options={{ title: 'Profile', headerShown: true }}>
      <DSLRenderer builder={buildProfileScreen()} />
    </ScreenConfigRenderer>
  );
}

Note: expo-router is optional. If it is not installed, screen navigation modifiers (.screenTitle(), .headerRight(), .headerLeft()) are silently ignored -- they will not throw errors.


Testing

The framework works in tests with no special setup, thanks to config-free defaults:

import { render } from '@testing-library/react-native';
import { Text } from 'react-native-swiftui-dsl';

it('renders styled text', () => {
  const { getByText } = render(
    Text('Hello').font('title').bold().toElement()
  );
  expect(getByText('Hello')).toBeTruthy();
});

For tests that need a specific theme or dark mode, wrap with DSLThemeProvider:

import { render } from '@testing-library/react-native';
import { DSLThemeProvider } from 'react-native-swiftui-dsl';

export function renderWithDSLTheme(
  ui: React.ReactElement,
  colorScheme: 'light' | 'dark' = 'light',
) {
  return render(ui, {
    wrapper: ({ children }) => (
      <DSLThemeProvider config={yourThemeConfig} colorScheme={colorScheme}>
        {children}
      </DSLThemeProvider>
    ),
  });
}

Architecture & Documentation

src/
├── Binding/         # Two-way binding system (createBinding, bindForm)
├── Conditionals/    # If, ForEach, Group
├── Config/          # DSLDefaults, defaultThemeConfig
├── Core/            # ViewBuilder, DSLRenderer, Modifier types, ThemeResolver
├── Navigation/      # expo-router ScreenConfigRenderer
├── Primitives/      # 20 primitive factory functions
├── Theme/           # DSLThemeProvider, DSLThemeContext, type definitions
└── index.ts         # Public API — single entry point

Subfolder Documentation

Each module has its own README with in-depth documentation:

| Module | Path | Description | | ---------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | Binding | [src/Binding/README.md](src/Binding/README.md) | Two-way bindings: createBinding, bindForm, Proxy-based field access, caching | | Conditionals | [src/Conditionals/README.md](src/Conditionals/README.md) | Declarative control flow: If, ForEach, Group | | Config | [src/Config/README.md](src/Config/README.md) | Centralized defaults, DSLDefaults, defaultThemeConfig | | Core | [src/Core/README.md](src/Core/README.md) | Core rendering engine: ViewBuilder, DSLRenderer, modifier resolution, ThemeResolver | | Navigation | [src/Navigation/README.md](src/Navigation/README.md) | Screen navigation: ScreenConfigRenderer, expo-router integration | | Primitives | [src/Primitives/README.md](src/Primitives/README.md) | All 20 UI component primitives with signatures and examples | | Theme | [src/Theme/README.md](src/Theme/README.md) | Theming system: DSLThemeProvider, DSLColorConfig, token types |


Platform Support

| Platform | Status | | -------- | -------------------------------- | | iOS | Fully supported | | Android | Fully supported | | Web | Partial (via react-native-web) |


Contributing

Contributions are welcome! Feel free to open issues for bug reports, feature requests, or questions.


License

MIT