<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dayana Jabif</title>
    <description>The latest articles on DEV Community by Dayana Jabif (@dayujabif).</description>
    <link>https://dev.to/dayujabif</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3981441%2Fb06852ab-8e58-4d36-9c48-fdef3e16c1b6.png</url>
      <title>DEV Community: Dayana Jabif</title>
      <link>https://dev.to/dayujabif</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dayujabif"/>
    <language>en</language>
    <item>
      <title>Content Projection in Angular: Building Flexible, Reusable Components</title>
      <dc:creator>Dayana Jabif</dc:creator>
      <pubDate>Fri, 10 Jul 2026 13:12:41 +0000</pubDate>
      <link>https://dev.to/dayujabif/content-projection-in-angular-building-flexible-reusable-components-26kk</link>
      <guid>https://dev.to/dayujabif/content-projection-in-angular-building-flexible-reusable-components-26kk</guid>
      <description>&lt;p&gt;Three years ago, when I started working at one project, I was handed the app's UI design and had to figure out how to translate it into Angular components.&lt;/p&gt;

&lt;p&gt;We had a clear use case: multiple content categories — events, deals, real estate, partners, among others — that shared a common UI (filters, cards, footer, header, reviews) but also had very distinct pieces from one another.&lt;/p&gt;

&lt;p&gt;Thinking through how to find a reusable component logic is what led me to this content projection architecture. &lt;strong&gt;Structuring the app this way from the ground up gave me a huge advantage&lt;/strong&gt; over my three and a half years on the project: as the app grew and new categories kept appearing, the component structure and the way components talked to each other just felt natural, almost like it had been built for exactly that.&lt;/p&gt;

&lt;p&gt;The same logic applied to smaller, encapsulated components: they shared common behavior, but their UI needed to be customizable depending on where they showed up — the same card, for example, would look different depending on which listing it was rendered in. CSS variables were key to building those small components in a flexible way.&lt;/p&gt;

&lt;p&gt;Designing this logic and building it all out was one of the parts of that project I enjoyed the most. &lt;strong&gt;Taking the time to structure your app's components to be as encapsulated and reusable as possible is what gives you the real advantage as your app keeps changing and growing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's why in this post I want to share a bit of the logic I used, and everything you can do with Angular content projection.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Creating reusable and flexible components is one of the fundamentals of building robust applications. In Angular, the feature that makes this possible is content projection — also known as transclusion. It lets a parent component pass content into a child component's template, so the same component can render very different things depending on who's using it.&lt;/p&gt;

&lt;p&gt;This post walks through content projection from the ground up: the core building blocks, single- and multi-slot projection, fallback content, ngProjectAs, and conditional projection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The building blocks
&lt;/h2&gt;

&lt;p&gt;Before diving into examples, here are the pieces you'll see throughout this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ng-content&lt;/code&gt;&lt;/strong&gt; — a placeholder that marks where projected content should render inside a component's template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ng-container&lt;/code&gt;&lt;/strong&gt; — a logical wrapper you can attach a directive to without adding an extra DOM element.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ng-template&lt;/code&gt;&lt;/strong&gt; — defines a block of template content that isn't rendered by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TemplateRef&lt;/code&gt;&lt;/strong&gt; — a reference to the content inside an &lt;code&gt;ng-template&lt;/code&gt;, used to instantiate views from it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ViewContainerRef&lt;/code&gt;&lt;/strong&gt; — a container that views can be attached to, typically the anchor point for something rendered from a &lt;code&gt;TemplateRef&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NgTemplateOutlet&lt;/code&gt;&lt;/strong&gt; — a directive that inserts an embedded view from a given &lt;code&gt;TemplateRef&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've spent time in an Angular codebase, you've likely already met most of these.&lt;/p&gt;

&lt;h2&gt;
  
  
  Single-slot content projection
&lt;/h2&gt;

&lt;p&gt;The simplest form of content projection lets you project &lt;strong&gt;one&lt;/strong&gt; block of content into a component. You place an &lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; element wherever you want that content to appear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-container-component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;h2&amp;gt;Single-slot content projection&amp;lt;/h2&amp;gt;
    &amp;lt;ng-content /&amp;gt;
    &amp;lt;div&amp;gt;Here we can have more code.&amp;lt;/div&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContainerBasicComponent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Components are standalone by default, so there's no need for a &lt;code&gt;standalone: true&lt;/code&gt; flag here.&lt;/p&gt;

&lt;p&gt;Using it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;app-container-component&amp;gt;&lt;/span&gt;
  This content will be projected where the ng-content slot is.
&lt;span class="nt"&gt;&amp;lt;/app-container-component&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which renders as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Single-slot content projection&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
This content will be projected where the ng-content slot is.
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;Here we can have more code.&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; isn't a real DOM element — Angular's compiler resolves it at build time, and it can't be inserted, removed, or styled at runtime. Custom attributes like &lt;code&gt;class&lt;/code&gt; on &lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; are ignored too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh3e3dkls7nssrrs87x7u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh3e3dkls7nssrrs87x7u.png" alt="Diagram showing a parent template projecting a paragraph into a CustomCard component's ng-content placeholder" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  An example you can relate to
&lt;/h2&gt;

&lt;p&gt;Say your app shows content from a few different categories — events, restaurants, and hotels. Call them &lt;strong&gt;"items."&lt;/strong&gt; Each item type shares a lot: images, location, reviews, contact info. But each also has something unique — restaurants need a menu and pre-order button, events need a list of services, hotels need room types.&lt;/p&gt;

&lt;p&gt;You &lt;em&gt;could&lt;/em&gt; build a separate detail page per category and duplicate all the shared markup. You'll feel that pain quickly. A better option is one shared &lt;code&gt;ItemDetailComponent&lt;/code&gt; that owns the common layout, with content projection handling the parts that differ:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-item-detail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;h2&amp;gt;{{ item().title }}&amp;lt;/h2&amp;gt;

    &amp;lt;ng-content select="[mainContent]" /&amp;gt;

    &amp;lt;div class="footer"&amp;gt;
      &amp;lt;ng-content select="[footer]"&amp;gt;
        &amp;lt;p&amp;gt;No additional details for this item.&amp;lt;/p&amp;gt;
      &amp;lt;/ng-content&amp;gt;
    &amp;lt;/div&amp;gt;

    @for (review of item().reviews; track review.id) {
      &amp;lt;p&amp;gt;{{ review.rate }} – {{ review.message }}&amp;lt;/p&amp;gt;
    }
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemDetailComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;item&lt;/code&gt; input uses &lt;code&gt;input.required&amp;lt;Item&amp;gt;()&lt;/code&gt;, Angular's signal-based input API, and the review list uses the &lt;code&gt;@for&lt;/code&gt; block to iterate.&lt;/p&gt;

&lt;p&gt;Each category then builds a thin wrapper around the shared component. Restaurants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ItemDetailComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./item-detail.component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-restaurant-page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ItemDetailComponent&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;app-item-detail [item]="restaurant"&amp;gt;
      &amp;lt;div mainContent&amp;gt;{{ restaurant.menu }}&amp;lt;/div&amp;gt;
      &amp;lt;div footer&amp;gt;Pre-order available — order ahead and skip the line.&amp;lt;/div&amp;gt;
    &amp;lt;/app-item-detail&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RestaurantComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;restaurant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Restaurant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// restaurant data&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ItemDetailComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./item-detail.component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-event-page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ItemDetailComponent&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;app-item-detail [item]="event"&amp;gt;
      &amp;lt;div mainContent&amp;gt;{{ event.services }}&amp;lt;/div&amp;gt;
    &amp;lt;/app-item-detail&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EventComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// event data&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the event page doesn't provide a &lt;code&gt;footer&lt;/code&gt; block at all — which is exactly the case fallback content is built for (more on that below). Hotels would follow the same shape, with room types slotted into &lt;code&gt;mainContent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F70yygb0ol1v63fpecn5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F70yygb0ol1v63fpecn5d.png" alt="Diagram showing RestaurantPage, EventPage, and HotelPage all projecting content into a shared ItemDetailComponent" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-slot content projection
&lt;/h2&gt;

&lt;p&gt;A component can expose more than one slot. Each &lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; can carry a &lt;code&gt;select&lt;/code&gt; attribute specifying a CSS selector — tag name, attribute, class, or even &lt;code&gt;:not()&lt;/code&gt; — that determines which projected content lands in that slot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;card-title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;ng-content /&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardTitle&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;card-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;ng-content /&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardBody&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;custom-card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CardTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CardBody&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;div class="card-shadow"&amp;gt;
      &amp;lt;ng-content select="card-title" /&amp;gt;
      &amp;lt;div class="card-divider"&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;ng-content select="card-body" /&amp;gt;
    &amp;lt;/div&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomCard&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;custom-card&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;card-title&amp;gt;&lt;/span&gt;Hello&lt;span class="nt"&gt;&amp;lt;/card-title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;card-body&amp;gt;&lt;/span&gt;Welcome to the example&lt;span class="nt"&gt;&amp;lt;/card-body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/custom-card&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you include one &lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; with a &lt;code&gt;select&lt;/code&gt; and one without, the unselected one captures everything that didn't match another selector. If there's no unselected &lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; at all, unmatched elements simply don't render.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwpnxpcfcd03bdz1n7grt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwpnxpcfcd03bdz1n7grt.png" alt="Diagram showing card-title and card-body content routed into two separate ng-content select slots" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fallback content
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; slots can define &lt;strong&gt;fallback content&lt;/strong&gt; that renders automatically when nothing matches them. You define it by putting content directly inside the &lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Component template --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-shadow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ng-content&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"card-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Untitled&lt;span class="nt"&gt;&amp;lt;/ng-content&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-divider"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ng-content&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"card-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;No description provided.&lt;span class="nt"&gt;&amp;lt;/ng-content&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Using the component --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;custom-card&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;card-title&amp;gt;&lt;/span&gt;Hello&lt;span class="nt"&gt;&amp;lt;/card-title&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- no card-body provided --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/custom-card&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Rendered DOM --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-shadow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;card-title&amp;gt;&lt;/span&gt;Hello&lt;span class="nt"&gt;&amp;lt;/card-title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-divider"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  No description provided.
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a clean way to give a slot a sensible default without writing any extra logic in the component class.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0umm6mqj4qjpmvtd1988.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0umm6mqj4qjpmvtd1988.png" alt="Diagram showing a card-body slot rendering its default fallback text when the parent provides no content" width="800" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Aliasing content with &lt;code&gt;ngProjectAs&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Sometimes the element you want to project doesn't natively match a slot's selector — maybe you're projecting an &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; into a slot that expects &lt;code&gt;card-title&lt;/code&gt;. &lt;code&gt;ngProjectAs&lt;/code&gt; lets you tell Angular to treat an element as if it matched a different selector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Component template --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-shadow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ng-content&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"card-title"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-divider"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ng-content&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Using the component --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;custom-card&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;ngProjectAs=&lt;/span&gt;&lt;span class="s"&gt;"card-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Hello&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Welcome to the example&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/custom-card&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ngProjectAs&lt;/code&gt; only accepts static values — it can't be bound to a dynamic expression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conditional content projection
&lt;/h2&gt;

&lt;p&gt;If a component needs to conditionally render projected content, or render it more than once, wrap that content in an &lt;code&gt;&amp;lt;ng-template&amp;gt;&lt;/code&gt; so it isn't rendered until something explicitly instantiates it — typically through &lt;code&gt;@ContentChild&lt;/code&gt;/&lt;code&gt;contentChild&lt;/code&gt; plus &lt;code&gt;NgTemplateOutlet&lt;/code&gt;, or through a structural directive.&lt;/p&gt;

&lt;p&gt;One important rule: &lt;strong&gt;don't wrap &lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; itself in &lt;code&gt;@if&lt;/code&gt;, &lt;code&gt;@for&lt;/code&gt;, or &lt;code&gt;@switch&lt;/code&gt;.&lt;/strong&gt; Angular always instantiates the DOM nodes for whatever's projected into an &lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; slot, even if that slot is hidden — so conditionally gating the &lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; tag doesn't skip that work, it just hides it.&lt;/p&gt;

&lt;p&gt;For a lot of "show this or show that" cases, you can express the intent directly with &lt;code&gt;@if&lt;/code&gt;/&lt;code&gt;@else&lt;/code&gt; across two separate slots instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-item-detail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    @if (isLoading()) {
      &amp;lt;ng-content select="[loading]"&amp;gt;
        &amp;lt;p&amp;gt;Loading…&amp;lt;/p&amp;gt;
      &amp;lt;/ng-content&amp;gt;
    } @else {
      &amp;lt;ng-content select="[content]" /&amp;gt;
    }
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemDetailComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ng-template&lt;/code&gt; is still what powers structural directives internally — &lt;code&gt;*ngIf&lt;/code&gt;, &lt;code&gt;*ngFor&lt;/code&gt;, and &lt;code&gt;*ngSwitch&lt;/code&gt; all desugar into it. If you're querying a &lt;code&gt;TemplateRef&lt;/code&gt; from a parent's projected content, &lt;code&gt;@ContentChild(TemplateRef)&lt;/code&gt; works, and there's also a signal-based &lt;code&gt;contentChild(TemplateRef)&lt;/code&gt; alternative that fits naturally alongside signal inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;ng-container&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ng-container&lt;/code&gt; gives you a place to attach a structural directive to a group of elements without adding an extra node to the DOM. That matters whenever you rely on precise DOM structure for styling — flex containers, &lt;code&gt;:first-child&lt;/code&gt; selectors, and so on — since an unnecessary wrapper &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; can break all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Content projection is what makes Angular components genuinely reusable: it separates a component's structure from its content, so the same component can adapt to whatever a consumer passes in. &lt;code&gt;ng-content&lt;/code&gt; handles the simple cases, multi-slot projection with &lt;code&gt;select&lt;/code&gt; handles layouts with multiple distinct regions, fallback content covers sensible defaults, &lt;code&gt;ngProjectAs&lt;/code&gt; handles content that doesn't natively match a selector, and &lt;code&gt;ng-template&lt;/code&gt; combined with structural directives or the &lt;code&gt;@if&lt;/code&gt;/&lt;code&gt;@for&lt;/code&gt;/&lt;code&gt;@switch&lt;/code&gt; blocks covers anything conditional.&lt;/p&gt;

&lt;p&gt;Once you start designing components around what they &lt;em&gt;host&lt;/em&gt; rather than what they &lt;em&gt;contain&lt;/em&gt;, you end up writing a lot less duplicate code — and the components you build stay useful well beyond the feature you first built them for.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How does Angular content projection improve reusable component design?&lt;/strong&gt;&lt;br&gt;
It lets you separate a component's structure from its content, so the same component can be reused across contexts with different content supplied by each consumer at runtime — reducing duplication without sacrificing flexibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should you use &lt;code&gt;ng-content&lt;/code&gt; in Angular components?&lt;/strong&gt;&lt;br&gt;
Whenever a component's job is to &lt;em&gt;wrap or decorate&lt;/em&gt; content rather than dictate it — cards, modals, layout wrappers, and similar container components. If you find yourself creating near-identical component variants just to change what's inside, that's a signal to reach for projection instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the easiest way to give a projected slot a sensible default?&lt;/strong&gt;&lt;br&gt;
Fallback content. Put the default markup directly inside the &lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt; tag and Angular renders it automatically whenever nothing is projected into that slot — no extra component logic required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which use cases benefit most from content projection?&lt;/strong&gt;&lt;br&gt;
Layout containers where content varies (cards, panels, modals), wrapper components that add styling or behavior without fixing the inner markup, and reusable UI patterns — like the shared item-detail example above — that need to host different content per consumer.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Convert Your Lovable App to mobile iOS &amp; Android Apps</title>
      <dc:creator>Dayana Jabif</dc:creator>
      <pubDate>Fri, 03 Jul 2026 13:16:33 +0000</pubDate>
      <link>https://dev.to/dayujabif/convert-your-lovable-app-to-mobile-ios-android-apps-5219</link>
      <guid>https://dev.to/dayujabif/convert-your-lovable-app-to-mobile-ios-android-apps-5219</guid>
      <description>&lt;p&gt;You described your app to Lovable, watched it appear in the browser, and now it works. Great. But it lives at a URL, and what you actually want is an icon on someone's home screen and a listing on the App Store and Google Play.&lt;/p&gt;

&lt;p&gt;The good news: you don't need to rewrite anything, and you don't need to learn Swift or Kotlin. &lt;a href="https://capacitorjs.com/" rel="noopener noreferrer"&gt;Capacitor&lt;/a&gt; wraps the web app you already have into real native iOS and Android projects.&lt;/p&gt;

&lt;p&gt;In this post I'll cover the first stretch of that journey — the part you can finish in about 15 minutes on any OS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get your Lovable code into GitHub and onto your machine&lt;/li&gt;
&lt;li&gt;Add Capacitor and generate the native iOS/Android projects&lt;/li&gt;
&lt;li&gt;Avoid the two mistakes that bite almost everyone (SSR output and &lt;code&gt;.gitignore&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At the end I'll point you to the full guide (and companion video) that continues all the way to a signed build running on a real iPhone — without owning a Mac.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚠️ First, a gotcha: SSR apps and native shells don't mix
&lt;/h2&gt;

&lt;p&gt;This is the single most important thing to know before you start.&lt;/p&gt;

&lt;p&gt;Capacitor works by bundling a &lt;strong&gt;static web build&lt;/strong&gt; — a folder with an &lt;code&gt;index.html&lt;/code&gt; plus JS/CSS — inside the native app. That bundle runs in a WebView on the device. There is no server on the phone.&lt;/p&gt;

&lt;p&gt;Why does this matter for Lovable specifically? Since May 2026, new Lovable projects default to &lt;strong&gt;TanStack Start with server-side rendering&lt;/strong&gt;. SSR is great for SEO on the web, but an SSR app expects a server to render each request — which a native shell can't provide.&lt;/p&gt;

&lt;p&gt;Two ways to handle it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Starting a new project?&lt;/strong&gt; Ask Lovable explicitly for a &lt;strong&gt;single-page app (SPA)&lt;/strong&gt; / no SSR. You'll get a client-rendered React + Vite app that builds into a static &lt;code&gt;dist/&lt;/code&gt; folder — exactly what Capacitor wants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Already on TanStack Start?&lt;/strong&gt; You don't have to start over. Configure it to pre-render / output a static SPA. The only hard requirement is the end result: a build folder containing an &lt;code&gt;index.html&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you remember one thing: &lt;strong&gt;Capacitor needs a static build folder, and for a Vite SPA that folder is &lt;code&gt;dist&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Export your Lovable app to GitHub
&lt;/h2&gt;

&lt;p&gt;Lovable can push your project to a GitHub repo for you: in the editor, open the &lt;strong&gt;GitHub&lt;/strong&gt; integration, authorize your account, and connect the project. Lovable creates the repository and keeps pushing your changes to it.&lt;/p&gt;

&lt;p&gt;Once that's done, clone it and confirm it runs locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/YOUR-USERNAME/your-app.git
&lt;span class="nb"&gt;cd &lt;/span&gt;your-app
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the local URL the terminal prints (Lovable apps usually use port 8080). If your app renders, you're ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Add Capacitor and the native platforms
&lt;/h2&gt;

&lt;p&gt;Install the Capacitor core and CLI, then initialize:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @capacitor/core @capacitor/cli
npx cap init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npx cap init&lt;/code&gt; asks three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App name&lt;/strong&gt; — what appears under the icon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package ID&lt;/strong&gt; — reverse-domain style, e.g. &lt;code&gt;com.yourcompany.yourapp&lt;/code&gt;. Choose carefully: it must be &lt;strong&gt;globally unique&lt;/strong&gt; across both stores and can't change after publishing in the stores. Base it on a domain you actually control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web assets directory&lt;/strong&gt; — &lt;code&gt;dist&lt;/code&gt; for a Vite SPA. (&lt;code&gt;www&lt;/code&gt; for Angular)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With your answers, a new file &lt;code&gt;capacitor.config.json&lt;/code&gt; will be created.&lt;/p&gt;

&lt;p&gt;Now build the web app (Capacitor needs the Web assets directory folder, such as &lt;code&gt;dist&lt;/code&gt; to exist) and generate the native projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npm &lt;span class="nb"&gt;install&lt;/span&gt; @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android
npx cap &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your repo now contains an &lt;code&gt;ios/&lt;/code&gt; folder (a full Xcode project) and an &lt;code&gt;android/&lt;/code&gt; folder (a full Android Studio project), with your built web app copied inside each one. No CocoaPods step needed anymore — Capacitor 8 uses Swift Package Manager under the hood.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your-app/
├── android/          ← native Android project
├── ios/              ← native iOS project
├── dist/             ← your built web app
├── src/              ← your Lovable code
├── capacitor.config.ts
└── package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 — Check your &lt;code&gt;.gitignore&lt;/code&gt; before pushing
&lt;/h2&gt;

&lt;p&gt;Common trap: accidentally keeping the native projects out of Git.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ios/&lt;/code&gt; and &lt;code&gt;android/&lt;/code&gt; &lt;strong&gt;must be committed&lt;/strong&gt; — they're source, not build output. (The &lt;code&gt;.gitignore&lt;/code&gt; files Capacitor drops inside them already exclude the actual build artifacts.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dist/&lt;/code&gt; and &lt;code&gt;node_modules/&lt;/code&gt; should stay &lt;strong&gt;ignored&lt;/strong&gt; — any build pipeline regenerates them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then push the milestone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add Capacitor and native iOS/Android platforms"&lt;/span&gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;git status&lt;/code&gt; first if you want to double-check the native folders show up as tracked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;At this point you have real native projects, but not yet an app on a phone. The rest of the journey is: signing certificates, developer accounts, building the iOS app (traditionally: a Mac with Xcode — though cloud build services let you skip that entirely and build from Windows or Linux), installing on a device via TestFlight, adding native features like the camera, and shipping OTA fixes with live updates so a CSS tweak doesn't cost you a week of app review. And of course the same for Android.&lt;/p&gt;

&lt;p&gt;I wrote the full step-by-step (all 17 steps, including the errors we hit and how we fixed them) here:&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=_5l-wiVYkVY" rel="noopener noreferrer"&gt;Turn Your Lovable App into a mobile app&lt;/a&gt; all the flow recorded in real time, breakages included.&lt;br&gt;
📖 &lt;strong&gt;Guide:&lt;/strong&gt; &lt;a href="https://capawesome.io/blog/convert-lovable-app-to-mobile-app/" rel="noopener noreferrer"&gt;Convert Your Lovable App to iOS &amp;amp; Android Apps&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I'm a Developer Advocate at Capawesome, where the full guide is published.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Questions? Drop them in the comments — happy to help you get unstuck. 🚀&lt;/p&gt;

</description>
      <category>lovable</category>
      <category>mobile</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
