<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Balogun Wahab - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Balogun Wahab - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 22 Aug 2026 09:58:27 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/03balogun/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Use Apple’s Foundation Models in a Web App with a macOS Companion ]]>
                </title>
                <description>
                    <![CDATA[ Not every AI feature needs a cloud model, with its per-token bills, network round-trips, and private data leaving your machine. If you're on a modern Mac, a capable language model is already on your d ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-apple-s-foundation-models-in-a-web-app-with-a-macos-companion/</link>
                <guid isPermaLink="false">6a5e92afe12aa31dae6e8a79</guid>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ macOS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Swift ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Balogun Wahab ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 21:27:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/7f0e2343-7394-46b5-a4c8-3ef0fecfa57a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Not every AI feature needs a cloud model, with its per-token bills, network round-trips, and private data leaving your machine. If you're on a modern Mac, a capable language model is already on your disk.</p>
<p><strong>Foundation Models</strong> is Apple's Swift framework for working with large language models. It's the on-device model behind Apple Intelligence, Apple's Private Cloud Compute, or another provider's server model.</p>
<p>This tutorial targets the on-device model: you send it a prompt and it runs entirely on the Mac's own hardware locally, free-per-call, and offline-friendly.</p>
<p>Paired with Apple Vision for reading images on device, that's enough to build real AI features like summaries, classification, and structured extraction without the data ever leaving your machine.</p>
<h2 id="heading-table-of-contents">Table Of Contents</h2>
<ul>
<li><p><a href="#heading-what-you-will-build">What You Will Build</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-a-macos-companion-app">Why a macOS Companion App?</a></p>
</li>
<li><p><a href="#heading-foundation-models-cant-read-images-directly">Foundation Models Can't Read Images Directly</a></p>
</li>
<li><p><a href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a href="#heading-build-the-react-app">Build the React App</a></p>
<ul>
<li><p><a href="#heading-check-companion-health">Check Companion Health</a></p>
</li>
<li><p><a href="#heading-convert-the-image-to-base64">Convert the Image to Base64</a></p>
</li>
<li><p><a href="#heading-analyze-immediately-after-upload">Analyze Immediately After Upload</a></p>
</li>
<li><p><a href="#heading-send-the-image-to-the-companion">Send the Image to the Companion</a></p>
</li>
<li><p><a href="#heading-render-the-json-output">Render the JSON Output</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-build-the-macos-companion-app">Build the macOS Companion App</a></p>
</li>
<li><p><a href="#heading-check-foundation-models-availability">Check Foundation Models Availability</a></p>
</li>
<li><p><a href="#heading-extract-text-with-apple-vision">Extract Text with Apple Vision</a></p>
</li>
<li><p><a href="#heading-ask-foundation-models-to-explain-the-vision-output">Ask Foundation Models to Explain the Vision Output</a></p>
</li>
<li><p><a href="#heading-return-json-to-the-browser">Return JSON to the Browser</a></p>
</li>
<li><p><a href="#heading-run-the-app">Run the App</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-what-you-will-build">What You Will Build</h2>
<p>You'll build <strong>Vision Bridge</strong>, a web app that sends an image to a local macOS companion. The companion reads the image with Apple Vision, reasons about it with Foundation Models, and returns structured JSON to the browser: private, on-device AI behind a plain web interface.</p>
<p>You can find the complete source code in this GitHub repository: <a href="http://github.com/03balogun/vision-bridge">github.com/03balogun/vision-bridge</a>.</p>
<p>The goal isn't to build a giant product but rather to understand the architecture behind how this works.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/6d18db01-e921-4291-bb2e-26be2c02b304.png" alt="Screenshot of the Vision Bridge app, with image upload on the left and JSON output on the right" style="display:block;margin:0 auto" width="3024" height="1714" loading="lazy">

<p>Vision Bridge has two parts:</p>
<ul>
<li><p>A React app with a split-screen interface.</p>
</li>
<li><p>A macOS companion app that exposes a local API.</p>
</li>
</ul>
<p>The React app has:</p>
<ul>
<li><p>An image upload area</p>
</li>
<li><p>An image preview</p>
</li>
<li><p>Automatic analysis after upload</p>
</li>
<li><p>A JSON output viewer</p>
</li>
<li><p>A companion health status indicator</p>
</li>
</ul>
<p>The macOS companion app has:</p>
<ul>
<li><p><code>GET /v1/health</code></p>
</li>
<li><p><code>POST /v1/analyze-image</code></p>
</li>
<li><p>Apple Vision OCR</p>
</li>
<li><p>Foundation Models availability checks</p>
</li>
<li><p>Foundation Models reasoning over Vision output</p>
</li>
</ul>
<p>The final response looks like this:</p>
<pre><code class="language-json">{
  "support": {
    "visionAvailable": true,
    "foundationModelAvailable": true,
    "foundationModelStatus": "available"
  },
  "image": {
    "filename": "screenshot.png",
    "contentType": "image/png",
    "byteCount": 1048576,
    "width": 1440,
    "height": 900
  },
  "vision": {
    "detectedText": [
      {
        "text": "Build failed",
        "confidence": 0.96,
        "boundingBox": {
          "x": 0.12,
          "y": 0.31,
          "width": 0.45,
          "height": 0.08
        }
      }
    ]
  },
  "model": {
    "summary": "The image appears to show a software build failure.",
    "description": "A developer tool window is showing an error state with diagnostic text.",
    "suggestedTags": ["screenshot", "developer-tool", "error"],
    "possibleUses": [
      "Generate alt text",
      "Summarize screenshots",
      "Extract document data"
    ]
  }
}
</code></pre>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you need:</p>
<ul>
<li><p>macOS 26 or newer</p>
</li>
<li><p>Xcode with the macOS 26 SDK</p>
</li>
<li><p>Node.js 20 or newer</p>
</li>
<li><p>Basic React knowledge</p>
</li>
<li><p>Basic Swift knowledge</p>
</li>
<li><p>A Mac that supports Apple Intelligence</p>
</li>
</ul>
<p>Foundation Models availability depends on the Mac, the OS version, and Apple Intelligence settings. The companion checks this at runtime, which we'll cover below.</p>
<h2 id="heading-why-a-macos-companion-app">Why a macOS Companion App?</h2>
<p>You can't write this in a regular React app:</p>
<pre><code class="language-ts">import FoundationModels from "apple-frameworks";
</code></pre>
<p>That API doesn't exist in the browser. A native macOS app, however, can use any Apple framework, so the companion acts as a local bridge. The same pattern works for any native capability the web platform doesn't expose.</p>
<h2 id="heading-foundation-models-cant-read-images-directly">Foundation Models Can't Read Images Directly</h2>
<p>The public Foundation Models framework is a language model interface. It doesn't currently expose direct image input the way a multimodal cloud model might, so this tutorial never sends the image to the model. Instead, the companion feeds the Vision OCR observations and image metadata into the prompt. The model reasons over structured text, never the original pixels.</p>
<p>That split plays to each framework's strength: Vision is excellent at pulling machine-readable information out of images, and Foundation Models turns that information into summaries, labels, explanations, and structured output.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/a5c11ad4-dcac-4690-bc6d-08b27fd6fed8.png" alt="Vision Bridge architecture: the browser sends the image over localhost to the Swift companion, which runs Apple Vision OCR, feeds the observations to Foundation Models, and returns structured JSON" style="display:block;margin:0 auto" width="2492" height="1572" loading="lazy">

<p>The above diagram shows the round trip that the rest of this tutorial builds. The browser sends the uploaded image as base64 JSON over localhost to the Swift companion. Inside the companion, Apple Vision runs OCR on the image and produces text observations: the recognized strings, their confidence scores, and their bounding boxes.</p>
<p>Those observations, not the image itself, are formatted into a prompt for Foundation Models, which generates a summary, description, and tags. The companion then bundles the Vision output and the model output into one JSON response and returns it to the browser.</p>
<h2 id="heading-project-structure">Project Structure</h2>
<p>Create a project with this structure:</p>
<pre><code class="language-text">vision-bridge/
  apps/
    web/
      src/
        main.tsx
        styles.css
      package.json
      vite.config.ts
    macos-companion/
      Package.swift
      Sources/
        VisionBridgeCompanion/
          main.swift
  package.json
  README.md
</code></pre>
<p>The root <code>package.json</code> gives us a few convenient commands:</p>
<pre><code class="language-json">{
  "scripts": {
    "dev": "npm --workspace apps/web run dev",
    "build": "npm --workspace apps/web run build",
    "companion": "swift run --package-path apps/macos-companion VisionBridgeCompanion"
  },
  "workspaces": ["apps/web"]
}
</code></pre>
<h2 id="heading-build-the-react-app">Build the React App</h2>
<p>The web app is intentionally simple. It has one job: let the user pick an image and show the JSON returned by the companion.</p>
<p>The web app uses Vite, React, Lucide icons, and a JSON viewer:</p>
<pre><code class="language-json">{
  "dependencies": {
    "@vitejs/plugin-react": "^6.0.3",
    "lucide-react": "^0.468.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-json-view-lite": "^2.5.0",
    "vite": "^8.1.3"
  }
}
</code></pre>
<p>After defining the dependencies, install them:</p>
<pre><code class="language-plaintext">npm install
</code></pre>
<p>The API base URL points to the local companion:</p>
<pre><code class="language-ts">const API_BASE_URL = "http://127.0.0.1:43119";
</code></pre>
<h3 id="heading-check-companion-health">Check Companion Health</h3>
<p>The web app pings the companion so the UI can show whether the native bridge is online:</p>
<pre><code class="language-ts">async function checkHealth() {
  setHealthError(null);

  try {
    const response = await fetch(`${API_BASE_URL}/v1/health`);
    if (!response.ok) {
      throw new Error(`Health check failed with ${response.status}`);
    }

    const payload = await response.json();
    setHealth(payload);
  } catch (error) {
    setHealth(null);
    setHealthError(error instanceof Error ? error.message : "Companion unavailable");
  }
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/dc3c37eb-1d9c-4b44-82db-f38adada4f19.png" alt="Screenshot of the companion online status pill" style="display:block;margin:0 auto" width="732" height="212" loading="lazy">

<h3 id="heading-convert-the-image-to-base64">Convert the Image to Base64</h3>
<p>When the user selects a file, the app converts it to base64 so it can be sent as JSON:</p>
<pre><code class="language-ts">function readFileAsBase64(file: File) {
  return new Promise&lt;string&gt;((resolve, reject) =&gt; {
    const reader = new FileReader();
    reader.onload = () =&gt; {
      const result = String(reader.result);
      resolve(result.includes(",") ? result.split(",")[1] : result);
    };
    reader.onerror = () =&gt; reject(reader.error);
    reader.readAsDataURL(file);
  });
}
</code></pre>
<p>This isn't the only way to upload files. You could also use <code>multipart/form-data</code>, but JSON keeps the demo easy to inspect.</p>
<h3 id="heading-analyze-immediately-after-upload">Analyze Immediately After Upload</h3>
<p>The app starts analysis as soon as an image is uploaded:</p>
<pre><code class="language-ts">async function handleFile(file: File) {
  if (!file.type.startsWith("image/")) {
    setError("Choose a PNG, JPEG, HEIC, or another browser-readable image.");
    return;
  }

  const base64 = await readFileAsBase64(file);
  const nextImage = {
    file,
    previewUrl: URL.createObjectURL(file),
    base64,
  };

  setSelectedImage(nextImage);
  setAnalysis(null);
  setError(null);
  setCopied(false);

  analyzeImage(nextImage);
}
</code></pre>
<p><code>handleFile</code> does the preparation work for every new image. It rejects anything that isn't a browser-readable image, converts the file to base64, and builds a single object holding everything the rest of the flow needs: the original <code>File</code> (for its name and MIME type), an object URL for the preview, and the base64 payload for the API call.</p>
<p>It then clears out the previous run the old analysis, any error message, and the "copied" indicator so the UI never shows results from the last image next to a new one. Finally, it kicks off <code>analyzeImage(nextImage)</code> immediately.</p>
<p>Note that it passes the fresh object directly instead of relying on the <code>selectedImage</code> state: React state updates don't apply until the next render, so reading the state here would still give you the <em>previous</em> image.</p>
<p>The <code>Analyze</code> button still exists in the UI, but it works as a manual rerun button.</p>
<h3 id="heading-send-the-image-to-the-companion">Send the Image to the Companion</h3>
<p>Here's the core request:</p>
<pre><code class="language-ts">const analysisRequestId = useRef(0);

async function analyzeImage(image = selectedImage) {
  if (!image) {
    setError("Choose an image first.");
    return;
  }

  const requestId = analysisRequestId.current + 1;
  analysisRequestId.current = requestId;

  setRequestState("loading");
  setError(null);
  setCopied(false);

  try {
    const response = await fetch(`${API_BASE_URL}/v1/analyze-image`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        filename: image.file.name,
        mimeType: image.file.type || "application/octet-stream",
        base64: image.base64,
      }),
    });

    const payload = await response.json();

    if (requestId !== analysisRequestId.current) {
      return;
    }

    if (!response.ok) {
      throw new Error(payload.error?.message ?? `Analysis failed with ${response.status}`);
    }

    setAnalysis(payload);
    setRequestState("success");
  } catch (error) {
    if (requestId !== analysisRequestId.current) {
      return;
    }

    setRequestState("error");
    setError(error instanceof Error ? error.message : "Could not analyze image");
  }
}
</code></pre>
<p>This function is the entire client side of the bridge. It flips <code>requestState</code> to <code>loading</code> (which drives the spinner and disables the button), then sends a <code>POST</code> to <code>/v1/analyze-image</code> with a JSON body containing three fields: the filename, the MIME type, and the base64 image data. That body maps one-to-one onto the <code>AnalyzeImageRequest</code> struct the Swift companion decodes later.</p>
<p>Notice that the response is parsed as JSON <em>before</em> checking <code>response.ok</code>. That's deliberate: when the companion rejects a request (bad base64, oversized image), it still returns a JSON body with an <code>error.message</code> field, so the UI can show the companion's own explanation instead of a generic status code. On success, the payload goes straight into state, and the JSON viewer re-renders with the result.</p>
<p>The <code>requestId</code> bookkeeping guards against stale responses. If a user uploads a second image while the first is still analyzing, whichever request finishes <em>last</em> would win, and OCR plus model generation takes long enough that responses can genuinely arrive out of order. So every call increments a counter stored in a ref and remembers its own ID.</p>
<p>After the <code>await</code>, it checks whether it's still the newest request; if a newer upload started in the meantime, the older response is silently discarded instead of overwriting the latest image's result. The same check runs in the <code>catch</code> block, so an old failure can't clobber a newer success either. If you also want to cancel the in-flight HTTP request rather than just ignore its result, an <code>AbortController</code> is the natural next step.</p>
<h3 id="heading-render-the-json-output">Render the JSON Output</h3>
<p>The output pane uses <code>react-json-view-lite</code>:</p>
<pre><code class="language-tsx">&lt;JsonView
  data={jsonData}
  shouldExpandNode={allExpanded}
  style={jsonViewTheme}
/&gt;
</code></pre>
<h2 id="heading-build-the-macos-companion-app">Build the macOS Companion App</h2>
<p>The companion is a Swift command-line app. It exposes a small local HTTP API.</p>
<p>If you come from the web side, the mapping is simple: Swift Package Manager is Swift's npm, <code>Package.swift</code> is its <code>package.json</code>, and <code>swift run</code> is its <code>npm start</code>. It ships with Xcode, so there's nothing extra to install.</p>
<p>The <code>Package.swift</code> file looks like this:</p>
<pre><code class="language-swift">// swift-tools-version: 6.0

import PackageDescription

let package = Package(
    name: "VisionBridgeCompanion",
    platforms: [
        .macOS("26.0")
    ],
    products: [
        .executable(
            name: "VisionBridgeCompanion",
            targets: ["VisionBridgeCompanion"]
        )
    ],
    targets: [
        .executableTarget(
            name: "VisionBridgeCompanion"
        )
    ]
)
</code></pre>
<p>The companion imports the Apple frameworks it needs:</p>
<pre><code class="language-swift">import Foundation
import FoundationModels
import ImageIO
import Network
import Vision
</code></pre>
<p>It listens on <code>127.0.0.1:43119</code>:</p>
<pre><code class="language-swift">private let defaultPort: UInt16 = 43119
</code></pre>
<p>The app exposes two routes:</p>
<pre><code class="language-swift">switch (request.method, request.path) {
case ("GET", "/v1/health"):
    let health = HealthResponse(support: ModelSupport.current)
    return try json(health)

case ("POST", "/v1/analyze-image"):
    let payload = try JSONDecoder().decode(AnalyzeImageRequest.self, from: request.body)
    let response = try await service.analyze(payload)
    return try json(response)

default:
    return try json(
        ErrorResponse(error: APIErrorPayload(message: "Route not found")),
        status: .notFound
    )
}
</code></pre>
<p>This <code>switch</code> is the companion's entire routing layer — no web framework, just pattern matching on the method and path.</p>
<p>The two routes split the work cleanly:</p>
<ul>
<li><p><code>GET /v1/health</code> is the cheap, read-only route. It runs no analysis, it just reports whether Vision and Foundation Models are usable on this Mac via <code>ModelSupport.current</code> (covered in the next section). The React app calls it on load to render the online/offline status pill, so the user knows the bridge is up before they upload anything.</p>
</li>
<li><p><code>POST /v1/analyze-image</code> is where the real work happens. It decodes the request body into an <code>AnalyzeImageRequest</code> (with the same <code>filename</code>, <code>mimeType</code>, and <code>base64</code> fields the browser sent) and hands it to the analysis service. This validates the image, runs Vision OCR, prompts Foundation Models, and returns the combined result. The <code>try await</code> matters here: analysis is asynchronous, and the route simply waits for it before serializing the response.</p>
</li>
</ul>
<p>Anything else falls through to a JSON 404, so even unknown routes respond in the same format the browser already knows how to parse.</p>
<p>Errors work the same way: thrown errors are caught in one place and converted into JSON error responses with an appropriate status code, which is exactly what the web app's <code>payload.error?.message</code> check reads.</p>
<p>One practical detail: because the browser calls the companion from a different origin (the Vite dev server), every response also carries CORS headers, and the router answers preflight <code>OPTIONS</code> requests with an empty <code>204</code>. Without that, the browser would block the <code>fetch</code> before it ever reached these routes.</p>
<h2 id="heading-check-foundation-models-availability">Check Foundation Models Availability</h2>
<p>The companion shouldn't assume that the model is available. Check it first:</p>
<pre><code class="language-swift">private struct ModelSupport: Encodable {
    let visionAvailable: Bool
    let foundationModelAvailable: Bool
    let foundationModelStatus: String

    static var current: ModelSupport {
        let model = SystemLanguageModel.default

        switch model.availability {
        case .available:
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: true,
                foundationModelStatus: "available"
            )

        case .unavailable(let reason):
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: false,
                foundationModelStatus: "unavailable.\(reason.description)"
            )

        @unknown default:
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: false,
                foundationModelStatus: "unavailable.unknown"
            )
        }
    }
}
</code></pre>
<p>A user might have an unsupported Mac, Apple Intelligence might be disabled, or the model might not be ready yet. The response tells the browser which case it's dealing with.</p>
<h2 id="heading-extract-text-with-apple-vision">Extract Text with Apple Vision</h2>
<p>The companion decodes the base64 image, checks its metadata, then runs Vision OCR.</p>
<p>Here's the text recognition flow:</p>
<pre><code class="language-swift">private func recognizeText(in imageData: Data) async throws -&gt; [DetectedText] {
    var request = RecognizeTextRequest()
    request.recognitionLevel = .accurate
    request.automaticallyDetectsLanguage = true
    request.usesLanguageCorrection = true

    let observations = try await request.perform(on: imageData)

    var detectedText: [DetectedText] = []

    for observation in observations {
        guard let candidate = observation.topCandidates(1).first else {
            continue
        }

        let bounds = NormalizedBox.from(points: [
            observation.topLeft,
            observation.topRight,
            observation.bottomRight,
            observation.bottomLeft
        ])

        detectedText.append(DetectedText(
            text: candidate.string,
            confidence: Double(candidate.confidence),
            boundingBox: bounds
        ))
    }

    return detectedText
}
</code></pre>
<p>Vision gives us structured observations:</p>
<ul>
<li><p>recognized text</p>
</li>
<li><p>confidence scores</p>
</li>
<li><p>normalized bounding boxes</p>
</li>
</ul>
<p>Those observations become the model’s context.</p>
<h2 id="heading-ask-foundation-models-to-explain-the-vision-output">Ask Foundation Models to Explain the Vision Output</h2>
<p>Now the companion creates a prompt from the image metadata and OCR results.</p>
<p>Notice the instruction:</p>
<pre><code class="language-text">You cannot see the original image. Use only the metadata and OCR observations below.
</code></pre>
<p>That keeps the model honest. It shouldn't pretend to see pixels it never received.</p>
<p>Here's the prompt shape:</p>
<pre><code class="language-swift">let textPreview = detectedText
    .prefix(30)
    .map { "- \($0.text) (confidence: \(String(format: "%.2f", $0.confidence)))" }
    .joined(separator: "\n")

let prompt = """
You are summarizing Apple Vision OCR output for a developer tool named Vision Bridge.
You cannot see the original image. Use only the metadata and OCR observations below.

Image:
- filename: \(image.filename)
- content type: \(image.contentType)
- size: \(image.width ?? 0)x\(image.height ?? 0)

OCR observations:
\(textPreview.isEmpty ? "- No text detected." : textPreview)

Return a compact JSON object with these exact keys:
summary: one sentence
description: one short paragraph
suggestedTags: 3 to 6 short tags
possibleUses: 3 to 5 practical use cases for this kind of image analysis
"""
</code></pre>
<p>Then call the model:</p>
<pre><code class="language-swift">let session = LanguageModelSession(
    model: .default,
    instructions: "Return valid JSON only. Do not include Markdown fences."
)

let response = try await session.respond(to: prompt)
let raw = response.content.trimmingCharacters(in: .whitespacesAndNewlines)
</code></pre>
<p>Even when you ask for JSON, always validate the output. Models can still return Markdown fences or malformed text. The sample app strips simple Markdown code fences and falls back to a raw response if parsing fails.</p>
<h2 id="heading-return-json-to-the-browser">Return JSON to the Browser</h2>
<p>The companion combines the support state, image metadata, Vision results, and model output:</p>
<pre><code class="language-swift">return AnalyzeImageResponse(
    support: support,
    image: metadata,
    vision: VisionPayload(detectedText: detectedText),
    model: modelInsight
)
</code></pre>
<p>The browser doesn't need to know how Vision or Foundation Models work. It just receives JSON. The native app owns the native capabilities, while the web app owns the interface.</p>
<p>It's worth pausing on what each of the four blocks actually gives you, because they're not all the same kind of data:</p>
<ul>
<li><p><code>support</code> tells you what was possible on this Mac. If <code>foundationModelAvailable</code> is <code>false</code>, the <code>model</code> block still exists but contains a fallback message rather than real analysis, and the <code>foundationModelStatus</code> string (for example, <code>unavailable.appleIntelligenceNotEnabled</code>) tells the UI <em>why</em>, so it can explain rather than silently degrade.</p>
</li>
<li><p><code>image</code> echoes back the file's metadata plus the measured pixel dimensions. It's useful as a sanity check, and you need the width and height to do anything spatial with the Vision results.</p>
</li>
<li><p><code>vision</code> is the ground truth. Each entry in <code>detectedText</code> is a string Vision actually found, with a confidence score between 0 and 1 and a normalized bounding box: coordinates expressed as fractions of the image size, so <code>x: 0.12, width: 0.45</code> means "starts 12% from the left and spans 45% of the width." Because the boxes are normalized, you can draw highlight overlays on the preview at any display size by multiplying by the rendered dimensions. Low-confidence entries are worth filtering or flagging before you trust them.</p>
</li>
<li><p><code>model</code> is interpretation, not observation. The <code>summary</code>, <code>description</code>, <code>suggestedTags</code>, and <code>possibleUses</code> fields are generated by the language model from the OCR text. This is useful as alt text, captions, or tag suggestions, but they inherit whatever the OCR missed and should be treated as a draft, not a fact. When the model's output can't be parsed as JSON, <code>rawResponse</code> carries the unparsed text so nothing is lost.</p>
</li>
</ul>
<p>For a screenshot of a failed build, the model block might come back like this:</p>
<pre><code class="language-json">{
  "model": {
    "summary": "The image appears to show a software build failure.",
    "description": "A developer tool window is showing an error state with diagnostic text.",
    "suggestedTags": ["screenshot", "developer-tool", "error"],
    "possibleUses": [
      "Generate alt text",
      "Summarize screenshots",
      "Extract document data"
    ]
  }
}
</code></pre>
<p>That combination (exact text with positions from Vision, plus a human-readable interpretation from the model) is enough to build real features on top of a searchable screenshot library indexed by <code>detectedText</code> and <code>suggestedTags</code>, automatic alt text for uploaded images, or click-to-highlight overlays powered by the bounding boxes.</p>
<p>And because the prompt lives in the companion, changing what comes back (say, extracting line items from receipts instead of tagging screenshots) is a prompt edit, not an architecture change.</p>
<h2 id="heading-run-the-app">Run the App</h2>
<p>Start the companion:</p>
<pre><code class="language-sh">npm run companion
</code></pre>
<p>In another terminal, start the web app:</p>
<pre><code class="language-sh">npm run dev
</code></pre>
<p>Open the Vite URL:</p>
<pre><code class="language-text">http://127.0.0.1:5173
</code></pre>
<p>If that port is busy, Vite will choose another one.</p>
<p>The companion should be available at:</p>
<pre><code class="language-text">http://127.0.0.1:43119
</code></pre>
<p>You can test it directly:</p>
<pre><code class="language-sh">curl http://127.0.0.1:43119/v1/health
</code></pre>
<p>Expected response:</p>
<pre><code class="language-json">{
  "app": "Vision Bridge Companion",
  "ok": true,
  "support": {
    "foundationModelAvailable": true,
    "foundationModelStatus": "available",
    "visionAvailable": true
  },
  "version": "0.1.0"
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/76a47c9a-934c-4133-ba7c-e2a9c6b6dad4.png" alt="Screenshot of terminal running companion" style="display:block;margin:0 auto" width="1448" height="556" loading="lazy">

<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a React interface that uploads an image, a Swift companion that analyzes it with Apple-native frameworks, and structured JSON flowing between them.</p>
<p>Vision Bridge is intentionally small, but the bridge itself is reusable. Once you have a trusted native companion, a web app can do more than send prompts to a remote model: it can ask the Mac to work with local context, use any Apple framework, and return structured data the browser can render, store, or sync.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a href="https://developer.apple.com/documentation/foundationmodels">Apple Foundation Models documentation</a></p>
</li>
<li><p><a href="https://developer.apple.com/documentation/vision">Apple Vision documentation</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create Fluid Animations with React Native Reanimated v4 ]]>
                </title>
                <description>
                    <![CDATA[ Reanimated 4 brings Cascading Style Sheets (CSS) animations to React Native while keeping full backward compatibility with its worklet-based API. You can now build 60+ frames-per-second (FPS) animations using familiar web syntax, or drop down to work... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-create-fluid-animations-with-react-native-reanimated-v4/</link>
                <guid isPermaLink="false">691b3eab5aa173ac953652e2</guid>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ react native reanimated ]]>
                    </category>
                
                    <category>
                        <![CDATA[ animation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Balogun Wahab ]]>
                </dc:creator>
                <pubDate>Mon, 17 Nov 2025 15:26:35 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763052228638/4416e81d-b76e-4c40-987e-0aff1d82ff7b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Reanimated 4 brings Cascading Style Sheets (CSS) animations to React Native while keeping full backward compatibility with its worklet-based API. You can now build 60+ frames-per-second (FPS) animations using familiar web syntax, or drop down to worklets for gesture-driven interactions.</p>
<p>The library requires React Native's New Architecture (Fabric), so you'll need version 0.76 or newer.</p>
<p>In this tutorial, you'll learn:</p>
<ul>
<li><p>How to use CSS transitions for state-driven animations</p>
</li>
<li><p>When to use worklets for gesture and scroll interactions</p>
</li>
<li><p>How to migrate from Reanimated 3 to 4</p>
</li>
<li><p>Practical patterns for collapsing headers, bottom sheets, and carousels</p>
</li>
<li><p>Performance optimization techniques</p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>You should have:</p>
<ul>
<li><p>React Native 0.76+ with New Architecture enabled</p>
</li>
<li><p>Basic React hooks knowledge (useState, useEffect)</p>
</li>
<li><p>Node.js and npm or yarn are installed</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-installation-and-setup">Installation and Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-the-two-approaches">Understanding the Two Approaches (CSS Animations and Worklets)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-migrate-from-reanimated-version-3-to-version-4">How to Migrate from Reanimated Version 3 to Version 4</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-css-animations-tutorial">CSS Animations Tutorial</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-worklets-tutorial">Worklets Tutorial</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-real-world-patterns">Real-World Patterns</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-performance-optimizations">Performance Optimizations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-debugging-tips">Debugging Tips</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-installation-and-setup">Installation and Setup</h2>
<p>To get started, you'll need to install the required packages:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># For Expo</span>
npx expo install react-native-reanimated react-native-worklets

<span class="hljs-comment"># For React Native CLI  </span>
npm install react-native-reanimated react-native-worklets
<span class="hljs-built_in">cd</span> ios &amp;&amp; pod install &amp;&amp; <span class="hljs-built_in">cd</span> ..
</code></pre>
<p>Update <code>babel.config.js</code> (the plugin must be last):</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">presets</span>: [<span class="hljs-string">'module:metro-react-native-babel-preset'</span>],
  <span class="hljs-attr">plugins</span>: [
    <span class="hljs-string">'react-native-worklets/plugin'</span>, <span class="hljs-comment">// Must be last</span>
  ],
};
</code></pre>
<p>Then clear the cache and rebuild:</p>
<pre><code class="lang-bash">npm start -- --reset-cache
npx react-native run-ios
</code></pre>
<h2 id="heading-understanding-the-two-approaches">Understanding the Two Approaches</h2>
<p>React Native Reanimated is an animation library that runs animations on the native thread instead of the JavaScript thread. This means your animations stay smooth even when your JavaScript code is busy processing data or handling user interactions.</p>
<p>Unlike React Native's built-in Animated API, Reanimated executes animation logic directly on the UI thread. This eliminates the performance bottleneck caused by communication between JavaScript and native code, which enables Reanimated to maintain 60 FPS even during complex operations.</p>
<p>Reanimated 4 offers two animation systems, each designed for different use cases.</p>
<h3 id="heading-css-animations">CSS Animations</h3>
<p>CSS animations work declaratively, meaning you describe what you want to happen rather than how to make it happen. You define which properties should animate (like width, color, or opacity), specify the timing and easing, then simply change the values through React state updates. Reanimated automatically handles the animation between the old and new values.</p>
<p>This approach excels at predictable, state-driven animations where you know both the starting and ending states. It's ideal for:</p>
<ul>
<li><p>Showing and hiding UI elements (modals, tooltips, notifications)</p>
</li>
<li><p>Expanding and collapsing content (accordions, dropdown menus)</p>
</li>
<li><p>Visual feedback for state changes (button hover effects, selection highlights)</p>
</li>
<li><p>Loading indicators and progress animations</p>
</li>
<li><p>Color and opacity transitions</p>
</li>
</ul>
<h3 id="heading-worklets">Worklets</h3>
<p>Worklets take a different approach by giving you imperative, frame-by-frame control over animations. They run on the UI thread and use "shared values" – special variables that can be accessed and modified from both JavaScript and native code without any communication overhead.</p>
<p>Worklets are essential for interactive animations that need to respond in real-time to user input or continuous data streams. They're best for:</p>
<ul>
<li><p>Gesture-driven interactions (drag-and-drop, swipe-to-dismiss, pinch-to-zoom)</p>
</li>
<li><p>Scroll-linked effects (parallax images, collapsing headers, sticky elements)</p>
</li>
<li><p>Physics-based animations (spring effects, momentum scrolling)</p>
</li>
<li><p>Sensor-based animations (responding to device orientation)</p>
</li>
<li><p>Any animation requiring dynamic, real-time control</p>
</li>
</ul>
<p>Now that you understand the two approaches Reanimated offers, let's look at how to migrate from version 3 if you're already using the library.</p>
<h2 id="heading-how-to-migrate-from-reanimated-version-3-to-version-4">How to Migrate from Reanimated Version 3 to Version 4</h2>
<p>If you're currently using Reanimated 3, you'll be happy to know that version 4 maintains backward compatibility. Your existing animations using worklets, shared values, and <code>useAnimatedStyle</code> will continue to work without modification.</p>
<p>But version 4 introduces some architectural changes and removes deprecated APIs, so you'll need to make a few updates to your project configuration and code. Let's walk through the migration process step by step.</p>
<h3 id="heading-what-changed-in-version-4">What Changed in Version 4</h3>
<p>The most significant change is that worklets have been extracted into a separate package called <code>react-native-worklets-core</code>. This modular approach allows other libraries beyond Reanimated to leverage worklet functionality.</p>
<p>Because of this separation, you'll need to update your Babel configuration. Change the plugin from <code>react-native-reanimated/plugin</code> to <code>react-native-worklets/plugin</code>.</p>
<p>Version 4 also exclusively supports React Native's New Architecture (Fabric). The old Paper renderer is no longer compatible. If your project hasn't migrated to the New Architecture yet, you'll need to either upgrade to React Native 0.76+ (which has New Architecture enabled by default) or stay on Reanimated 3.x until you're ready to make that transition.</p>
<h3 id="heading-removed-apis">Removed APIs</h3>
<p>Several APIs that were deprecated in version 3 have been removed in version 4. Here's what you need to replace:</p>
<ul>
<li><p><code>useAnimatedGestureHandler</code> → Use the <code>Gesture</code> API from react-native-gesture-handler 2.x instead</p>
</li>
<li><p><code>useWorkletCallback</code> → Use <code>useCallback</code> with the <code>'worklet'</code> directive</p>
</li>
<li><p><code>combineTransition</code> → Use <code>EntryExitTransition.entering().exiting()</code></p>
</li>
</ul>
<p>The <code>useScrollViewOffset</code> hook has been renamed to <code>useScrollOffset</code>. The old name still works but is deprecated, so update your code to use the new name.</p>
<h3 id="heading-spring-configuration-change">Spring Configuration Change</h3>
<p>The spring animation configuration has changed to feel more natural. The <code>duration</code> parameter now represents "perceptual duration" rather than exact milliseconds. The actual animation runs approximately 1.5 times longer than the specified duration, creating springs that feel more organic and less mechanical.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Version 3</span>
withSpring(<span class="hljs-number">100</span>, { <span class="hljs-attr">duration</span>: <span class="hljs-number">300</span> }) <span class="hljs-comment">// Runs for exactly 300ms</span>

<span class="hljs-comment">// Version 4  </span>
withSpring(<span class="hljs-number">100</span>, { <span class="hljs-attr">duration</span>: <span class="hljs-number">200</span> }) <span class="hljs-comment">// Runs for approximately 300ms</span>
</code></pre>
<p>If you need to maintain the exact timing from version 3, divide your duration values by 1.5.</p>
<h3 id="heading-step-by-step-migration-process">Step-by-Step Migration Process</h3>
<p>Here's how to migrate your project from Reanimated 3 to version 4:</p>
<p><strong>Step 1:</strong> Verify your project is using React Native 0.76 or newer with New Architecture enabled. Check your iOS Podfile for <code>ENV['RCT_NEW_ARCH_ENABLED'] = '1'</code> and your Android gradle.properties for <code>newArchEnabled=true</code>.</p>
<p><strong>Step 2:</strong> Install the new versions of Reanimated and the worklets package:</p>
<pre><code class="lang-bash">npm install react-native-reanimated@^4.1.0 react-native-worklets@^0.5.0
</code></pre>
<p><strong>Step 3:</strong> Update your <code>babel.config.js</code> to use the new worklets plugin:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">plugins</span>: [
    <span class="hljs-string">'react-native-worklets/plugin'</span>, <span class="hljs-comment">// Changed from react-native-reanimated/plugin</span>
  ],
};
</code></pre>
<p><strong>Step 4:</strong> Search your codebase for the removed APIs and replace them:</p>
<ul>
<li><p>Replace <code>useAnimatedGestureHandler</code> with the <code>Gesture</code> API</p>
</li>
<li><p>Replace <code>useWorkletCallback</code> with <code>useCallback</code> and add <code>'worklet'</code> directive</p>
</li>
<li><p>Replace <code>combineTransition</code> with <code>EntryExitTransition.entering().exiting()</code></p>
</li>
<li><p>Rename <code>useScrollViewOffset</code> to <code>useScrollOffset</code></p>
</li>
</ul>
<p><strong>Step 5:</strong> Rebuild your native apps:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> ios &amp;&amp; pod install &amp;&amp; <span class="hljs-built_in">cd</span> ..
npx react-native run-ios
<span class="hljs-comment"># or for Android</span>
npx react-native run-android
</code></pre>
<p>After completing these steps, your app should be running on Reanimated 4 with all your existing animations working as before. You're now ready to start using the new CSS animation features alongside your existing worklet-based animations. In the next section, you'll learn how to build animations using the CSS syntax.</p>
<h2 id="heading-css-animations-tutorial">CSS Animations Tutorial</h2>
<p>CSS animations provide a clean, declarative way to handle transitions that are triggered by state changes. Instead of manually managing animation values, you simply declare which properties should animate and how, then update your component state – Reanimated handles the rest.</p>
<p>This approach is particularly powerful for animations where you know the start and end states ahead of time. It's perfect for UI elements that toggle between different visual states, like modals appearing and disappearing, buttons providing feedback on press, or content expanding and collapsing.</p>
<h3 id="heading-basic-transitions">Basic Transitions</h3>
<p>A transition animates the change between two property values. When you specify a property that should transition, Reanimated automatically interpolates between the old and new values over the specified duration.</p>
<p>Let's look at an expandable card that grows when tapped:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React, { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { Pressable, Text } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native'</span>;
<span class="hljs-keyword">import</span> Animated <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native-reanimated'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ExpandableCard</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [expanded, setExpanded] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Pressable</span> <span class="hljs-attr">onPress</span>=<span class="hljs-string">{()</span> =&gt;</span> setExpanded(!expanded)}&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
        <span class="hljs-attr">width:</span> <span class="hljs-attr">expanded</span> ? <span class="hljs-attr">300</span> <span class="hljs-attr">:</span> <span class="hljs-attr">200</span>,
        <span class="hljs-attr">height:</span> <span class="hljs-attr">expanded</span> ? <span class="hljs-attr">200</span> <span class="hljs-attr">:</span> <span class="hljs-attr">100</span>,
        <span class="hljs-attr">backgroundColor:</span> <span class="hljs-attr">expanded</span> ? '#<span class="hljs-attr">4ade80</span>' <span class="hljs-attr">:</span> '#<span class="hljs-attr">86efac</span>',
        <span class="hljs-attr">transitionProperty:</span> ['<span class="hljs-attr">width</span>', '<span class="hljs-attr">height</span>', '<span class="hljs-attr">backgroundColor</span>'],
        <span class="hljs-attr">transitionDuration:</span> <span class="hljs-attr">300</span>,
        <span class="hljs-attr">transitionTimingFunction:</span> '<span class="hljs-attr">ease-in-out</span>',
      }}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Tap to toggle<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Pressable</span>&gt;</span></span>
  );
}
</code></pre>
<p>Here's what's happening: The card's width, height, and background color are controlled by the <code>expanded</code> state. The <code>transitionProperty</code> array tells Reanimated which properties to animate. When <code>expanded</code> changes, Reanimated smoothly animates from the current values to the new values over 300 milliseconds, using an ease-in-out timing function that starts slow, speeds up, then slows down again.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762274975831/7e25aa04-24b8-43eb-9db0-a3c088c44132.gif" alt="7e25aa04-24b8-43eb-9db0-a3c088c44132" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<h3 id="heading-keyframe-animations">Keyframe Animations</h3>
<p>While transitions handle changes between two states, keyframe animations let you define multi-step sequences with precise control over each stage. You create an object where each key represents a percentage of the animation timeline, and the value defines what properties should look like at that point.</p>
<p>Here's a pulsing badge that scales up and fades slightly, then returns to normal:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> pulseAnimation = {
  <span class="hljs-string">'0%'</span>: { <span class="hljs-attr">scale</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">opacity</span>: <span class="hljs-number">1</span> },
  <span class="hljs-string">'50%'</span>: { <span class="hljs-attr">scale</span>: <span class="hljs-number">1.05</span>, <span class="hljs-attr">opacity</span>: <span class="hljs-number">0.8</span> },
  <span class="hljs-string">'100%'</span>: { <span class="hljs-attr">scale</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">opacity</span>: <span class="hljs-number">1</span> },
};

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PulsingBadge</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
      <span class="hljs-attr">width:</span> <span class="hljs-attr">50</span>,
      <span class="hljs-attr">height:</span> <span class="hljs-attr">50</span>,
      <span class="hljs-attr">borderRadius:</span> <span class="hljs-attr">25</span>,
      <span class="hljs-attr">backgroundColor:</span> '#<span class="hljs-attr">ef4444</span>',
      <span class="hljs-attr">animationName:</span> <span class="hljs-attr">pulseAnimation</span>,
      <span class="hljs-attr">animationDuration:</span> <span class="hljs-attr">2000</span>,
      <span class="hljs-attr">animationIterationCount:</span> '<span class="hljs-attr">infinite</span>',
    }} /&gt;</span></span>
  );
}
</code></pre>
<p>The animation starts at 0% (normal size and opacity), grows and fades at the 50% mark, then returns to the original state at 100%. By setting <code>animationIterationCount</code> to 'infinite', the animation loops continuously. This creates the pulsing effect you often see on notification badges or live indicators.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762274903470/d10d856f-03b8-4d6c-b616-22cbea3434c2.gif" alt="d10d856f-03b8-4d6c-b616-22cbea3434c2" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<h3 id="heading-built-in-animations">Built-in Animations</h3>
<p>Reanimated includes a collection of pre-built animations for common entrance and exit effects. These save you from writing animation configurations for standard patterns like fading, sliding, and zooming.</p>
<p>Here's a modal that fades in when shown and fades out when hidden:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { FadeIn, FadeOut } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native-reanimated'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Modal</span>(<span class="hljs-params">{ visible, children }</span>) </span>{
  <span class="hljs-keyword">if</span> (!visible) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> 
      <span class="hljs-attr">entering</span>=<span class="hljs-string">{FadeIn.duration(300)}</span>
      <span class="hljs-attr">exiting</span>=<span class="hljs-string">{FadeOut.duration(200)}</span>
    &gt;</span>
      {children}
    <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span></span>
  );
}
</code></pre>
<p>The <code>entering</code> prop automatically applies the fade-in animation when the component mounts, and <code>exiting</code> applies the fade-out before unmounting. Other commonly used built-in animations include <code>SlideInRight</code>, <code>SlideOutLeft</code> (for drawer-style entrances), and <code>ZoomIn</code>, <code>ZoomOut</code> (for attention-grabbing pop-ins).</p>
<p>Now that you understand CSS animations for state-driven transitions, let's explore worklets for creating interactive animations that respond to user input in real-time.</p>
<h2 id="heading-worklets-tutorial">Worklets Tutorial</h2>
<p>While CSS animations excel at predefined state transitions, many animations need to respond dynamically to user input. This is where worklets come in. Worklets give you frame-by-frame control over animations, allowing them to follow gestures, scroll position, or any other real-time input source.</p>
<p>Interactive animations differ from CSS animations in that they don't have predefined start and end states. Instead, they continuously update based on user input. For example, a draggable element needs to follow your finger precisely as you move it – there's no way to know ahead of time where you'll drag it. This requires imperative control, where you directly manipulate animation values in response to events.</p>
<h3 id="heading-basic-worklet-animation">Basic Worklet Animation</h3>
<p>Shared values are the foundation of worklet-based animations. They're special variables that exist simultaneously in both the JavaScript and UI threads, allowing you to update them from JavaScript while the UI thread reads them to update the display – all without any communication overhead.</p>
<p>Here's a button that scales down when pressed and bounces back when released:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> Animated, { 
  useSharedValue, 
  useAnimatedStyle, 
  withSpring 
} <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native-reanimated'</span>;
<span class="hljs-keyword">import</span> { Pressable } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">BouncyButton</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> scale = useSharedValue(<span class="hljs-number">1</span>);

  <span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
    <span class="hljs-attr">transform</span>: [{ <span class="hljs-attr">scale</span>: scale.value }],
  }));

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Pressable</span>
      <span class="hljs-attr">onPressIn</span>=<span class="hljs-string">{()</span> =&gt;</span> { scale.value = withSpring(0.9); }}
      onPressOut={() =&gt; { scale.value = withSpring(1); }}
    &gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.button,</span> <span class="hljs-attr">animatedStyle</span>]}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Press Me<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Pressable</span>&gt;</span></span>
  );
}
</code></pre>
<p>The <code>useSharedValue(1)</code> creates a shared value initialized to 1 (normal scale). The <code>useAnimatedStyle</code> hook creates a style object that depends on this shared value and runs on the UI thread. When you press the button, <code>scale.value = withSpring(0.9)</code> updates the shared value, and <code>withSpring</code> creates a spring animation to the new value. The <code>useAnimatedStyle</code> hook automatically re-runs, updating the transform with the new scale value.</p>
<h3 id="heading-gesture-animations">Gesture Animations</h3>
<p>Gestures require even tighter integration between user input and animation. The react-native-gesture-handler library provides high-performance gesture recognition that works seamlessly with Reanimated.</p>
<p>First, install the gesture handler:</p>
<pre><code class="lang-bash">npm install react-native-gesture-handler
<span class="hljs-built_in">cd</span> ios &amp;&amp; pod install &amp;&amp; <span class="hljs-built_in">cd</span> ..
</code></pre>
<p>Next, you need to wrap your app with <code>GestureHandlerRootView</code>. This component sets up the gesture handling system at the root of your application. Without it, gestures won't work. Think of it as activating the gesture system for your entire app:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { GestureHandlerRootView } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native-gesture-handler'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">GestureHandlerRootView</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">flex:</span> <span class="hljs-attr">1</span> }}&gt;</span>
      {/* Your app content goes here */}
    <span class="hljs-tag">&lt;/<span class="hljs-name">GestureHandlerRootView</span>&gt;</span></span>
  );
}
</code></pre>
<p>Now you can create gesture-driven animations. Here's a box you can drag around the screen that springs back to center when released:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { Gesture, GestureDetector } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native-gesture-handler'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DraggableBox</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> offsetX = useSharedValue(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> offsetY = useSharedValue(<span class="hljs-number">0</span>);

  <span class="hljs-keyword">const</span> pan = Gesture.Pan()
    .onChange(<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      offsetX.value += event.changeX;
      offsetY.value += event.changeY;
    })
    .onEnd(<span class="hljs-function">() =&gt;</span> {
      offsetX.value = withSpring(<span class="hljs-number">0</span>);
      offsetY.value = withSpring(<span class="hljs-number">0</span>);
    });

  <span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
    <span class="hljs-attr">transform</span>: [
      { <span class="hljs-attr">translateX</span>: offsetX.value },
      { <span class="hljs-attr">translateY</span>: offsetY.value },
    ],
  }));

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">GestureDetector</span> <span class="hljs-attr">gesture</span>=<span class="hljs-string">{pan}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.box,</span> <span class="hljs-attr">animatedStyle</span>]} /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">GestureDetector</span>&gt;</span></span>
  );
}
</code></pre>
<p>The <code>Gesture.Pan()</code> creates a pan gesture recognizer. The <code>.onChange()</code> callback fires continuously while you're dragging – <code>event.changeX</code> and <code>event.changeY</code> tell you how much the finger moved since the last frame. By adding these values to the offsets, the box follows your finger. When you lift your finger, <code>.onEnd()</code> fires and springs the box back to the center (0, 0).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762275456257/3a536406-d678-46eb-9cfe-f689428d3412.gif" alt="3a536406-d678-46eb-9cfe-f689428d3412" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<h3 id="heading-scroll-linked-animations">Scroll-Linked Animations</h3>
<p>Another common use case for worklets is creating effects that respond to scroll position, like headers that shrink as you scroll down or parallax backgrounds.</p>
<p>Here's a header that collapses as you scroll:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ParallaxHeader</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> scrollY = useSharedValue(<span class="hljs-number">0</span>);

  <span class="hljs-keyword">const</span> scrollHandler = useAnimatedScrollHandler({
    <span class="hljs-attr">onScroll</span>: <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      scrollY.value = event.contentOffset.y;
    },
  });

  <span class="hljs-keyword">const</span> headerStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> height = interpolate(
      scrollY.value,
      [<span class="hljs-number">0</span>, <span class="hljs-number">150</span>],
      [<span class="hljs-number">200</span>, <span class="hljs-number">60</span>],
      <span class="hljs-string">'clamp'</span>
    );

    <span class="hljs-keyword">return</span> { height };
  });

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.header,</span> <span class="hljs-attr">headerStyle</span>]}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Header<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.ScrollView</span>
        <span class="hljs-attr">onScroll</span>=<span class="hljs-string">{scrollHandler}</span>
        <span class="hljs-attr">scrollEventThrottle</span>=<span class="hljs-string">{16}</span>
      &gt;</span>
        {/* Content */}
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.ScrollView</span>&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
  );
}
</code></pre>
<p>The <code>useAnimatedScrollHandler</code> creates a scroll event handler that runs on the UI thread. Every time you scroll, it updates <code>scrollY</code> with the current scroll position. The <code>interpolate</code> function maps the scroll position to the header height – when scrollY is 0 (top of the scroll), height is 200. When scrollY reaches 150, height is 60. The 'clamp' option prevents the height from going outside this range.</p>
<p>With these fundamentals of CSS animations and worklets covered, let's look at how to apply them to common real-world scenarios.</p>
<h2 id="heading-real-world-patterns">Real-World Patterns</h2>
<p>Now that you understand both CSS animations and worklets, let's combine them to build three patterns you'll frequently encounter in production apps. These examples demonstrate when to use each animation approach and how to structure your code for maintainability.</p>
<p>In this section, you'll learn how to build a collapsing header that shrinks as users scroll (using worklets for scroll tracking), a bottom sheet that responds to drag gestures (using worklets for gesture control), and a swipe-to-delete interaction for list items (combining worklets for gesture detection with animations for the deletion effect).</p>
<h3 id="heading-collapsing-header">Collapsing Header</h3>
<p>A collapsing header is a navigation bar that starts tall and shrinks as you scroll down. This pattern is popular because it maximizes content space while keeping navigation accessible. You'll use worklets here because the animation needs to follow the scroll position in real-time.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CollapsibleHeader</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> scrollY = useSharedValue(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> HEADER_MAX = <span class="hljs-number">200</span>;
  <span class="hljs-keyword">const</span> HEADER_MIN = <span class="hljs-number">60</span>;

  <span class="hljs-keyword">const</span> scrollHandler = useAnimatedScrollHandler({
    <span class="hljs-attr">onScroll</span>: <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      scrollY.value = event.contentOffset.y;
    },
  });

  <span class="hljs-keyword">const</span> headerStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
    <span class="hljs-attr">height</span>: interpolate(
      scrollY.value,
      [<span class="hljs-number">0</span>, HEADER_MAX - HEADER_MIN],
      [HEADER_MAX, HEADER_MIN],
      <span class="hljs-string">'clamp'</span>
    ),
  }));

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">flex:</span> <span class="hljs-attr">1</span> }}&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.header,</span> <span class="hljs-attr">headerStyle</span>]}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>My App<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.ScrollView</span>
        <span class="hljs-attr">onScroll</span>=<span class="hljs-string">{scrollHandler}</span>
        <span class="hljs-attr">scrollEventThrottle</span>=<span class="hljs-string">{16}</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">height:</span> <span class="hljs-attr">1000</span>, <span class="hljs-attr">padding:</span> <span class="hljs-attr">16</span> }}&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Scroll to see header collapse<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.ScrollView</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span></span>
  );
}
</code></pre>
<p>This pattern tracks scroll position in <code>scrollY</code> and uses <code>interpolate</code> to map it to header height. When you're at the top (scrollY = 0), the header is 200 pixels tall. As you scroll down 140 pixels, the header shrinks to 60 pixels. The animation happens on every frame as you scroll, which is why worklets are necessary – CSS animations couldn't track scroll position this smoothly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762326945758/044ca9d6-dd6e-4891-b9f4-3a4dc8590b58.gif" alt="044ca9d6-dd6e-4891-b9f4-3a4dc8590b58" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<h3 id="heading-bottom-sheet">Bottom Sheet</h3>
<p>A bottom sheet is a panel that slides up from the bottom of the screen, commonly used for action menus, filters, or additional content. Users can drag it to different heights or dismiss it with a swipe down. This requires worklets because it needs frame-by-frame gesture tracking.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">BottomSheet</span>(<span class="hljs-params">{ children }</span>) </span>{
  <span class="hljs-keyword">const</span> translateY = useSharedValue(<span class="hljs-number">300</span>);
  <span class="hljs-keyword">const</span> context = useSharedValue({ <span class="hljs-attr">y</span>: <span class="hljs-number">0</span> });

  <span class="hljs-keyword">const</span> pan = Gesture.Pan()
    .onStart(<span class="hljs-function">() =&gt;</span> {
      context.value = { <span class="hljs-attr">y</span>: translateY.value };
    })
    .onChange(<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      translateY.value = <span class="hljs-built_in">Math</span>.max(
        event.translationY + context.value.y,
        <span class="hljs-number">-300</span>
      );
    })
    .onEnd(<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (event.velocityY &gt; <span class="hljs-number">500</span>) {
        translateY.value = withSpring(<span class="hljs-number">300</span>); <span class="hljs-comment">// Dismiss</span>
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (translateY.value &gt; <span class="hljs-number">-100</span>) {
        translateY.value = withSpring(<span class="hljs-number">-50</span>); <span class="hljs-comment">// Collapsed</span>
      } <span class="hljs-keyword">else</span> {
        translateY.value = withSpring(<span class="hljs-number">-300</span>); <span class="hljs-comment">// Expanded</span>
      }
    });

  <span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
    <span class="hljs-attr">transform</span>: [{ <span class="hljs-attr">translateY</span>: translateY.value }],
  }));

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">GestureDetector</span> <span class="hljs-attr">gesture</span>=<span class="hljs-string">{pan}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.bottomSheet,</span> <span class="hljs-attr">animatedStyle</span>]}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.handle}</span> /&gt;</span>
        {children}
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">GestureDetector</span>&gt;</span></span>
  );
}
</code></pre>
<p>The bottom sheet starts off-screen at translateY = 300. When you start dragging, <code>.onStart()</code> saves the starting position in <code>context</code>. As you drag, <code>.onChange()</code> updates the position, but <code>Math.max()</code> prevents it from going below -300 (fully expanded). When you release, <code>.onEnd()</code> checks the velocity – if you swiped down quickly (velocity &gt; 500), it dismisses. Otherwise, it snaps to either the collapsed (-50) or expanded (-300) position based on where you released it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763051520805/dd09d10f-4d77-40a9-be9c-ef85d69be69e.gif" alt="Bottom sheet demo" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<h3 id="heading-swipe-to-delete">Swipe to Delete</h3>
<p>Swipe-to-delete lets users remove items from a list by swiping left. It's a common pattern in email apps and to-do lists. This uses worklets for gesture tracking and timing functions for the deletion animation.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SwipeToDelete</span>(<span class="hljs-params">{ children, onDelete }</span>) </span>{
  <span class="hljs-keyword">const</span> translateX = useSharedValue(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> itemHeight = useSharedValue(<span class="hljs-number">60</span>);

  <span class="hljs-keyword">const</span> pan = Gesture.Pan()
    .activeOffsetX([<span class="hljs-number">-10</span>, <span class="hljs-number">10</span>])
    .onChange(<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (event.translationX &lt; <span class="hljs-number">0</span>) {
        translateX.value = event.translationX;
      }
    })
    .onEnd(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">if</span> (translateX.value &lt; <span class="hljs-number">-100</span>) {
        translateX.value = withTiming(<span class="hljs-number">-500</span>, { <span class="hljs-attr">duration</span>: <span class="hljs-number">200</span> });
        itemHeight.value = withTiming(<span class="hljs-number">0</span>, { <span class="hljs-attr">duration</span>: <span class="hljs-number">200</span> }, <span class="hljs-function">() =&gt;</span> {
          runOnJS(onDelete)();
        });
      } <span class="hljs-keyword">else</span> {
        translateX.value = withSpring(<span class="hljs-number">0</span>);
      }
    });

  <span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
    <span class="hljs-attr">transform</span>: [{ <span class="hljs-attr">translateX</span>: translateX.value }],
    <span class="hljs-attr">height</span>: itemHeight.value,
  }));

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">GestureDetector</span> <span class="hljs-attr">gesture</span>=<span class="hljs-string">{pan}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.item,</span> <span class="hljs-attr">animatedStyle</span>]}&gt;</span>
        {children}
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">GestureDetector</span>&gt;</span></span>
  );
}
</code></pre>
<p>The <code>.activeOffsetX([-10, 10])</code> setting means the gesture only activates after you've moved 10 pixels horizontally, preventing accidental triggers during vertical scrolling. The <code>if (event.translationX &lt; 0)</code> check ensures you can only swipe left, not right. If you swipe past -100 pixels and release, it triggers the deletion: the item slides off-screen (-500), the height collapses to 0, and <code>runOnJS</code> calls your delete function from the UI thread back to JavaScript.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763051815614/6e2c2d60-3d2e-49ec-9fb5-c17db00e9120.gif" alt="6e2c2d60-3d2e-49ec-9fb5-c17db00e9120" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<p>These patterns demonstrate the power of combining Reanimated's animation approaches with gesture handling. Now, let's look at how to keep these animations performing smoothly.</p>
<h2 id="heading-performance-optimizations">Performance Optimizations</h2>
<p>Even though Reanimated runs on the UI thread, poorly structured animations can still drop frames. Here are four key optimizations that will keep your animations consistently smooth at 60 FPS.</p>
<h3 id="heading-memoize-animations">Memoize Animations</h3>
<p>Every time your component re-renders, any animations you create inside the render function are recreated. This wastes memory and processing time.</p>
<p>Don't do this – creating a new animation object on every render:</p>
<pre><code class="lang-javascript">{items.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> (
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">entering</span>=<span class="hljs-string">{FadeIn.duration(300)}</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{item.id}</span> /&gt;</span></span>
))}
</code></pre>
<p>Instead, create the animation once outside the component or memoize it:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> fadeIn = FadeIn.duration(<span class="hljs-number">300</span>);
{items.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> (
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">entering</span>=<span class="hljs-string">{fadeIn}</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{item.id}</span> /&gt;</span></span>
))}
</code></pre>
<p>By storing the animation in a constant, you create it once and reuse the same object for all items. This reduces memory allocation and garbage collection, keeping your animations smooth even with long lists.</p>
<h3 id="heading-use-usederivedvalue">Use useDerivedValue</h3>
<p>If you're doing expensive calculations inside <code>useAnimatedStyle</code>, those calculations run every frame, even if the dependencies haven't changed.</p>
<p>Don't do this – recalculating every frame:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
  <span class="hljs-attr">width</span>: <span class="hljs-built_in">Math</span>.min(<span class="hljs-built_in">Math</span>.max(offset.value * <span class="hljs-number">2</span>, <span class="hljs-number">100</span>), <span class="hljs-number">500</span>),
}));
</code></pre>
<p>Instead, use <code>useDerivedValue</code> to compute the value only when dependencies change:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> width = useDerivedValue(<span class="hljs-function">() =&gt;</span> 
  <span class="hljs-built_in">Math</span>.min(<span class="hljs-built_in">Math</span>.max(offset.value * <span class="hljs-number">2</span>, <span class="hljs-number">100</span>), <span class="hljs-number">500</span>)
);

<span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
  <span class="hljs-attr">width</span>: width.value,
}));
</code></pre>
<p>Now the complex calculation only runs when <code>offset.value</code> changes, not on every frame. The <code>useAnimatedStyle</code> just reads the pre-computed width, which is much faster.</p>
<h3 id="heading-batch-updates">Batch Updates</h3>
<p>When you update multiple shared values, each update can trigger a separate re-render. This creates unnecessary work for the UI thread.</p>
<p>Don't do this – triggering multiple re-renders:</p>
<pre><code class="lang-javascript">scale.value = withSpring(<span class="hljs-number">1.2</span>);
opacity.value = withSpring(<span class="hljs-number">0.8</span>);
</code></pre>
<p>Instead, batch the updates using <code>runOnUI</code>:</p>
<pre><code class="lang-javascript">runOnUI(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-string">'worklet'</span>;
  scale.value = withSpring(<span class="hljs-number">1.2</span>);
  opacity.value = withSpring(<span class="hljs-number">0.8</span>);
})();
</code></pre>
<p>The <code>runOnUI</code> function ensures both updates happen in the same frame, so the UI only re-renders once. This is especially important when updating many values at once, like in complex gestures or choreographed animations.</p>
<h3 id="heading-prefer-transform-over-layout">Prefer Transform Over Layout</h3>
<p>Animating layout properties like width, height, or margins forces React Native to recalculate the position of every element that depends on the changing element. This is expensive.</p>
<p>Don't do this – expensive layout recalculation:</p>
<pre><code class="lang-javascript">width: withSpring(newWidth)
</code></pre>
<p>Instead, use transform properties, which only affect the visual appearance without triggering layout:</p>
<pre><code class="lang-javascript">transform: [{ <span class="hljs-attr">scaleX</span>: withSpring(scale) }]
</code></pre>
<p>Transform operations are hardware-accelerated and don't affect layout, making them dramatically faster. Whenever possible, use <code>translateX/Y</code> instead of changing position, <code>scale</code> instead of changing size, and <code>rotate</code> instead of changing orientation.</p>
<p>These optimizations will keep your animations buttery smooth. Now let's look at how to debug issues when they arise.</p>
<h2 id="heading-debugging-tips">Debugging Tips</h2>
<p>Even with proper setup, you may encounter issues with animations. Here are the most common problems and their solutions, written as complete troubleshooting steps.</p>
<h3 id="heading-animations-not-working">Animations Not Working</h3>
<p>If your animations aren't running at all, the most common cause is a missing or incorrectly configured Babel plugin. Open your <code>babel.config.js</code> file and verify that <code>react-native-worklets/plugin</code> is present in the plugins array and is the last plugin in the list. The order matters because the worklets plugin needs to process your code after all other transformations.</p>
<p>After confirming the plugin is correctly configured, clear your Metro bundler cache by running <code>npm start -- --reset-cache</code>, then rebuild your app completely. Simply reloading JavaScript won't work because Babel transformations happen during the build process.</p>
<h3 id="heading-app-crashes-on-startup-or-reload">App Crashes on Startup or Reload</h3>
<p>If your app crashes immediately after installing Reanimated or when you reload, the native modules likely aren't properly linked. With React Native 0.76+, this usually means the pods weren't installed or the native build is out of sync.</p>
<p>For iOS, run <code>cd ios &amp;&amp; pod install &amp;&amp; cd ..</code> then do a clean build with <code>npx react-native run-ios</code>. For Android, clean the build with <code>cd android &amp;&amp; ./gradlew clean &amp;&amp; cd ..</code> then rebuild with <code>npx react-native run-android</code>.</p>
<p>If you're getting build errors about missing headers or modules, make sure you've added both <code>react-native-reanimated</code> and <code>react-native-worklets</code> to your package.json dependencies.</p>
<h3 id="heading-turbomoduleregistry-not-found">"TurboModuleRegistry Not Found"</h3>
<p>If you see an error message saying "TurboModuleRegistry.get('NativeReanimated'): 'NativeReanimated' could not be found", it means the native code hasn't been properly linked to your JavaScript code.</p>
<p>First, verify you're using React Native 0.76 or newer, as Reanimated 4 requires the New Architecture. Check your <code>ios/Podfile</code> for <code>ENV['RCT_NEW_ARCH_ENABLED'] = '1'</code> and <code>android/gradle.properties</code> for <code>newArchEnabled=true</code>.</p>
<p>Then rebuild completely: <code>cd ios &amp;&amp; pod install &amp;&amp; cd .. &amp;&amp; npx react-native run-ios</code>.</p>
<h3 id="heading-logging-and-inspecting-shared-values">Logging and Inspecting Shared Values</h3>
<p>If you try to debug worklets using <code>console.log()</code>, you'll notice nothing appears in your console. This is because worklets run on the UI thread, which doesn't have direct access to the JavaScript console.</p>
<p>To log values from worklets, use the <code>useDerivedValue</code> hook:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> offset = useSharedValue(<span class="hljs-number">0</span>);

useDerivedValue(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Offset:'</span>, offset.value);
  <span class="hljs-keyword">return</span> offset.value;
});
</code></pre>
<p>For more advanced debugging, React Native's built-in debugger (accessed through dev menu → "Open Debugger") now supports debugging both threads. You can set breakpoints in worklets and inspect shared values in real-time.</p>
<h3 id="heading-monitor-performance">Monitor Performance</h3>
<p>To see if your animations are actually running at 60 FPS, enable the Performance Monitor built into React Native. Shake your device (or press Cmd+D in the iOS simulator, Cmd+M in Android emulator) to open the dev menu, then select "Show Perf Monitor".</p>
<p>The monitor displays two critical numbers: JS thread FPS and UI thread FPS. Your animations run on the UI thread, so watch that number. If it stays at 60 FPS, your animations are smooth. If it drops below 60, your animations are skipping frames and will appear janky. The JS thread FPS shows whether your React code is keeping up – if this drops, it indicates issues with your component renders, not your animations.</p>
<p>For more detailed debugging information and advanced troubleshooting, check the <a target="_blank" href="https://docs.swmansion.com/react-native-reanimated/docs/guides/debugging/">official debugging guide here</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763053624344/7aeffb5f-4829-4871-bd49-6e589adeb8ad.png" alt="7aeffb5f-4829-4871-bd49-6e589adeb8ad" class="image--center mx-auto" width="1176" height="1090" loading="lazy"></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Reanimated 4 gives you two powerful approaches to animation: CSS animations for simple state changes and worklets for complex, interactive animations that need real-time control.</p>
<p>Start with CSS transitions when building your next animation feature. They're simpler to write, easier to maintain, and perfect for the majority of UI animations. Reach for worklets when you need gesture control, scroll effects, or any animation that requires frame-by-frame updates.</p>
<p>The <a target="_blank" href="https://docs.swmansion.com/react-native-reanimated">official documentation</a> provides complete API references, detailed guides, and interactive examples. The <a target="_blank" href="https://github.com/software-mansion/react-native-reanimated">GitHub repository</a> includes production-ready sample code you can study and adapt.</p>
<p>Building smooth animations isn't just about technical capability – it's about creating experiences that feel responsive, intuitive, and delightful to use. Reanimated 4 makes achieving that standard straightforward, whether you're animating a simple button press or building a complex screen transition with multiple coordinated elements.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
