back to blog
Why I'm Building Artic Native: A Zero-Runtime UI Library for React Native

Why I'm Building Artic Native: A Zero-Runtime UI Library for React Native

May 15, 2026

I’ve audited enough React Native codebases to know the problem isn’t a shortage of UI kits—it’s the weight of them. Most ship runtime styling engines, untyped theme objects, and animations that choke the JS thread. For my 2026 apps, I needed something colder. Immutable. Fast. Opinionated without being oppressive.

Enter artic-native.

This is not a “another button” package. It’s a set of primitives engineered to hit 60fps even on mid-range Android hardware, without giving up the design guardrails teams need.


The Architecture Playbook

artic-native is built on three guiding rules:

  1. Zero-Runtime Styling — Tokens compile to frozen StyleSheet objects and can be statically extracted into native modules for Hermes.
  2. Animation-First Primitives — Every interactive component embraces react-native-reanimated 3. Layout transitions, springs, and shared element hooks are first-class.
  3. Contracts over Config — Props are strict TypeScript interfaces. Variants are union types. No arbitrary style injection that breaks consistency.
// packages/artic-native/src/foundations/tokens.ts
export const ArticTokens = {
  radius: { sm: 8, md: 12, lg: 20 },
  spacing: { xs: 8, sm: 12, md: 16, lg: 24 },
  palette: {
    ink: "#0F172A",
    frost: "#E2E8F0",
    glacier: "#38BDF8",
    polar: "#F8FAFC",
  },
  typography: {
    label: { fontFamily: "Inter-SemiBold", fontSize: 12, letterSpacing: 0.6 },
    body: { fontFamily: "Inter-Regular", fontSize: 16, lineHeight: 22 },
  },
} as const;

Tokens ship as pure data. When consumers opt into “frozen mode,” the library emits a generated .native.ts file that compiles to JSI bindings—zero runtime cost when the app boots.


Primitive Spotlight

1. ArticButton: Haptic-Ready Call To Action

Buttons should feel alive. Press feedback, haptics, and shadow interpolation are built-in.

// packages/artic-native/src/primitives/ArticButton.tsx
import React from "react";
import { Pressable, Text, StyleSheet } from "react-native";
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated";
import * as Haptics from "expo-haptics";
import { ArticTokens } from "../foundations/tokens";

type ButtonVariant = "solid" | "outline" | "glass";

export interface ArticButtonProps {
  label: string;
  variant?: ButtonVariant;
  onPress?: () => void;
  disabled?: boolean;
}

const AnimatedPressable = Animated.createAnimatedComponent(Pressable);

export const ArticButton = ({ label, variant = "solid", onPress, disabled }: ArticButtonProps) => {
  const scale = useSharedValue(1);

  const pressIn = () => {
    if (disabled) return;
    scale.value = withSpring(0.95, { stiffness: 220 });
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
  };

  const pressOut = () => {
    scale.value = withSpring(1, { stiffness: 180 });
  };

  const rStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  return (
    <AnimatedPressable
      accessibilityRole="button"
      style={[styles.base, styles[variant], disabled && styles.disabled, rStyle]}
      onPress={disabled ? undefined : onPress}
      onPressIn={pressIn}
      onPressOut={pressOut}
    >
      <Text style={[styles.label, variant= "solid" ? styles.labelOnDark : styles.labelOnLight]}>
        {label}
      </Text>
    </AnimatedPressable>
  );
};

const styles = StyleSheet.create({
  base: {
    paddingVertical: ArticTokens.spacing.sm,
    paddingHorizontal: ArticTokens.spacing.lg,
    borderRadius: ArticTokens.radius.md,
    alignItems: "center",
    justifyContent: "center",
    shadowColor: "#000000",
    shadowOpacity: 0.1,
    shadowRadius: 10,
    shadowOffset: { width: 0, height: 4 },
  },
  solid: { backgroundColor: ArticTokens.palette.ink },
  outline: {
    backgroundColor: "transparent",
    borderWidth: 1,
    borderColor: ArticTokens.palette.frost,
  },
  glass: {
    backgroundColor: "rgba(255,255,255,0.12)",
    borderWidth: 1,
    borderColor: "rgba(255,255,255,0.24)",
  },
  disabled: { opacity: 0.5 },
  label: {
    ...ArticTokens.typography.body,
    fontWeight: "600",
  },
  labelOnDark: { color: "#FFFFFF" },
  labelOnLight: { color: ArticTokens.palette.ink },
});

2. ArticInput: Focus States as a Service

Forms are where UX dies. ArticInput handles floating labels, focus outlines, and validation messaging without leaking state outside.

// packages/artic-native/src/primitives/ArticInput.tsx
import React, { useState } from "react";
import { TextInput, View, Text, StyleSheet, TextInputProps } from "react-native";
import { ArticTokens } from "../foundations/tokens";

export interface ArticInputProps extends TextInputProps {
  label: string;
  error?: string;
}

export const ArticInput = ({ label, error, ...props }: ArticInputProps) => {
  const [focused, setFocused] = useState(false);

  return (
    <View style={styles.container}>
      <Text style={[styles.label, focused && styles.labelFocused, !!error && styles.labelError]}>
        {label.toUpperCase()}
      </Text>
      <TextInput
        style={[styles.input, focused && styles.inputFocused, !!error && styles.inputError]}
        placeholderTextColor="#94A3B8"
        onFocus={()=> setFocused(true)}
        onBlur={()=> setFocused(false)}
        {...props}
      />
      {!!error && <Text style={styles.errorText}>{error}</Text>}
    </View>
  );
};

const styles = StyleSheet.create({
  container: { marginBottom: ArticTokens.spacing.md },
  label: {
    ...ArticTokens.typography.label,
    color: "#64748B",
    marginBottom: 6,
  },
  labelFocused: { color: ArticTokens.palette.glacier },
  labelError: { color: "#EF4444" },
  input: {
    height: 52,
    borderWidth: 1,
    borderRadius: ArticTokens.radius.sm,
    borderColor: ArticTokens.palette.frost,
    backgroundColor: ArticTokens.palette.polar,
    paddingHorizontal: ArticTokens.spacing.md,
    ...ArticTokens.typography.body,
  },
  inputFocused: { borderColor: ArticTokens.palette.glacier, backgroundColor: "#FFFFFF" },
  inputError: { borderColor: "#EF4444", backgroundColor: "#FEF2F2" },
  errorText: { color: "#EF4444", fontSize: 12, marginTop: 4 },
});

3. ArticSurface: Layout Primitive with Motion Hooks

Every app needs cards and sheets. ArticSurface packages elevation, rounded corners, and shared element helpers.

// packages/artic-native/src/primitives/ArticSurface.tsx
import React from "react";
import { View, StyleSheet, ViewProps } from "react-native";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { ArticTokens } from "../foundations/tokens";

type Elevation = "base" | "floating" | "overlay";

interface ArticSurfaceProps extends ViewProps {
  elevation?: Elevation;
  asAnimated?: boolean;
}

const surfaceStyles: Record<Elevation, object> = {
  base: {
    shadowOpacity: 0.04,
    shadowRadius: 6,
    shadowOffset: { width: 0, height: 2 },
  },
  floating: {
    shadowOpacity: 0.08,
    shadowRadius: 18,
    shadowOffset: { width: 0, height: 12 },
  },
  overlay: {
    shadowOpacity: 0.18,
    shadowRadius: 24,
    shadowOffset: { width: 0, height: 20 },
  },
};

export const ArticSurface = ({ elevation = "base", style, asAnimated, ...rest }: ArticSurfaceProps) => {
  const Component = asAnimated ? Animated.View : View;

  return (
    <Component
      entering={asAnimated ? FadeIn.duration(180) : undefined}
      exiting={asAnimated ? FadeOut.duration(160) : undefined}
      style={[styles.base, surfaceStyles[elevation], style]}
      {...rest}
    />
  );
};

const styles = StyleSheet.create({
  base: {
    borderRadius: ArticTokens.radius.lg,
    backgroundColor: "#FFFFFF",
    padding: ArticTokens.spacing.lg,
  },
});

Packaging for the Edge

  • Tree Shaking: Every primitive ships as an ES module; consumers can import { ArticButton } from "artic-native/button" to avoid bundling the universe.
  • Theme Composer: A CLI (artic-native init) snapshots your brand palette and spits out a read-only theme artifact.
  • Hermes RIC: Reanimated + zero-runtime tokens means Hermes can keep UI logic inside the JSI sandbox—no bridge thrash.
npx artic-native init --namespace xaid --tokens ./design/tokens.json
npx artic-native build --platform ios --platform android

What’s Next

I’m rounding out the release with:

  • Slot-Based Layouts powered by React 19 use for deferred children.
  • Devtools Integration that logs component prop usage to a Supabase table for design audits.
  • Storybook Native recipes so designers can drive builds from Figma tokens directly.

The repo is private while I finish the tree-shaking benchmarks, but the waitlist is open. Drop your email on xaid.in/contact and you’ll get early access to artic-native + the sample expo app.