Build an X clone w/ Nuxt UI

Nuxt UI is a library that allows us to build scalable interfaces with Nuxt without the need to build from scratch. In this tutorial, we’ll be building a minimal X (Twitter) profile UI using the Nuxt UI component library.

Nuxt UI was created using technologies like Headless UI and Tailwind CSS. As a result, you’ll essentially be working with these two technologies while building with Nuxt UI, enabling developers to implement projects faster without the need to start from scratch.


Building with Nuxt UI

During the development process, we will be utilizing specific components such as tabs, buttons, containers, cards, and more from the Nuxt UI component library. This allows us to avoid building these components from scratch. Additionally, Nuxt UI uses Tailwind CSS classes to handle the styling, which comes prepackaged with the Nuxt UI module.

By the end of this tutorial, we will have build this demo:

Demo of X (Twitter) profile UI created in Nuxt UI

Here’s the source code for this project.


Setting up our X (Twitter) clone project

To get started, we’ll create a new Nuxt 3 project using this command:

npx nuxi@latest init twitter-clone

Then we’ll navigate into our project’s directory and run our server using these commands:

cd twitter-clone

npm run dev

We should see the initial Nuxt 3 starter UI in our browser:

Default Nuxt 3 welcome page

Once this is done, we can now install our Nuxt UI library using this command:

npm install @nuxt/ui

Now, in our nuxt.config.ts file, we’ll navigate to the modules section and add @nuxt/ui to the array so we have access to its features in our project.

export default defineNuxtConfig({
  modules: ['@nuxt/ui']
})

Building the App’s UI

Now that we have our project ready with Nuxt UI set up to be used, we can start building out the UI.

We’ll go into the components folder and create a new component named Twitter.vue. This is where we’ll be building out our X (Twitter) profile UI clone.

Then we’ll add the <Twitter/> component in our app.vue as a child.

Now this component is ready to be styled. As mentioned earlier, Nuxt UI comes pre-packaged with Tailwind CSS, so we can use a couple classes from that library here.

We will add bg-[#000101] to set a dark background color, and h-screen to make the site’s height match the size of the viewport.

<template>
  <Twitter class="bg-[#000101] h-screen" />
</template>

Adding Images using Nuxt Image

In every X (Twitter) profile page, there is a header image and a profile image (also known as the avatar). In order to add these images to our Nuxt project, we’ll use Nuxt’s official image module, Nuxt Image.

Nuxt Image automatically optimizes images in our Nuxt apps and comes with impressive features such as dynamically generating responsive sizes for your images.

Let’s install the module by running this command in the terminal:

npm install @nuxt/image@rc

Now, we’ll go into the the nuxt.config.ts and include it in the modules array, along with our Nuxt UI module from earlier.

export default defineNuxtConfig({
  modules: ["@nuxt/ui", "@nuxt/image"]
})

Centering the Wrapper

We first need to center our wrapper div to contain all the UI elements on the profile page. First, we’d set the display of the outer wrapper to grid and justify the items to the center with justify-center. Then, we have an inner wrapper with border-x and border-gray-600, which creates borders on the two vertical sides of the wrapper. Plus, it defines the color of the borders.

<div class="grid justify-center">
  <div class="border-x border-gray-600"></div>
</div>

Adding the header image

To add our header image, we’ll use <NuxtImg/> to pull in the image (vue-cover.jpeg), located in our public folder.

<div class="grid justify-center">
  <div class="border-x border-gray-600">
    <NuxtImg src="/vue-cover.jpeg" />
  </div>
</div>

Now, the image is the first UI component in the wrapper.

  <div class="grid justify-center">
    <div class="border-x border-gray-600">
      <NuxtImg src="/vue-cover.jpeg" />
    </div>
  </div>

Adding the profile picture

Next, let’s add our second image: the profile picture. For this, we’ll use the Nuxt UI <UAvatar/> component, and style it with Tailwind CSS classes.

<template>
  <div class="grid justify-center">
    <div class="border-x border-gray-600">
      <NuxtImg src="/vue-cover.jpeg" />
      <div class="flex mx-4">
        <UAvatar
          class="-mt-16"
          src="/logo.jpeg"
          alt="Profile Avatar"
          size="xl"
        />
      </div>
    </div>
  </div>
</template>

Let’s now wrap the <UAvatar/> in a <div/> . The display of this <div/> is defined as flex . While, mx-4 to add a bit of margin to both sides.

The avatar component comes with its own size prop, with assigned values for height, width and text sizes (as a fallback for avatars without images) which are styled using Tailwind CSS classes.

You may decide to use the default size preset (sm, md, lg, xl) included in the component, but if you’d like to override the default values with your own custom Tailwind CSS classes, you can do that by editing the app.config.ts file, which should be in the root folder of your app.

Since we do want to override the default values, we’ll go into our app.config.ts file and update the xl values to be "h-32 w-32".

export default defineAppConfig({
  ui: {
    strategy: "override",
    primary: "cool",
    avatar: {
      size: {
        xl: "h-32 w-32",
      },
    }
  },
});

These Tailwind classes give us an image that is larger than the default xl preset was.

Our next step is to add the Nuxt UI <UButton> component for our “Edit Profile” button.

<template>
  <div class="grid justify-center">
    <div class="border-x border-gray-600">
      <NuxtImg src="/vue-cover.jpeg" />
      <div class="flex mx-4">
        <UAvatar
          class="-mt-16"
          src="/logo.jpeg"
          alt="Profile Avatar"
          size="xl"
        />
        <UButton
          :ui="{ rounded: 'rounded-full' }"
          color="gray"
          variant="outline"
          class="ml-auto h-min mt-3"
          >Edit Profile</UButton
        >
      </div>
    </div>
  </div>
</template>

We’ll add certain props to the <UButton> component. Let’s set the color prop as gray and variant to outline . We then add our own margin class styles "ml-auto h-min mt-3" to position the button to the right end and add a bit of margin to the top.

Finally, we have the the :ui prop that allows us overwrite the default value of the other props directly within the component. In this case, we’re overwriting the the rounded prop with rounded-full.

With all this in place, we should have this displayed in our local app:

X (Twitter) clone UI with profile picture, cover image and “edit profile” button


Building the Profile Bio section

Next up is our bio section. This is quite straightforward as it contains only text. Let’s add the following code to our Twitter.vue component:

<template>
    <div class="grid justify-center">
        <div class="border-x border-gray-600">
            <NuxtImg src="/vue-cover.jpeg" />
            <div class="flex mx-4">
                <UAvatar class="-mt-16" src="/logo.jpeg" 
                  alt="Profile Avatar" size="xl" />
                <UButton :ui="{ rounded: 'rounded-full' }" 
                          color="gray" variant="outline" 
                          class="ml-auto h-min mt-3">
                    Edit Profile
                </UButton>
            </div>

      <!-- Profile bio section -->
      <div class="px-4">
        <h3 class="text-xl font-bold">Vue Mastery</h3>
        <span class="text-[#686E73] mb-3">@VueMastery</span>
        <p class="mb-6">
          The ultimate learning platform for Vue.js developers
        </p>
        <p
          >Learn from the experts,<br />Elevate your code, <br />Master your
          craft</p
        >
        <a href="vuemastery.com/courses" class="text-[#2C9AF0]">vuemastery.com/courses</a>
        <a class="flex my-3 items-center text-[#2C9AF0]" href="vuemastery.com/courses">
                <svg viewBox="0 0 24 24" aria-hidden="true" class="h-5 w-5 mr-1 fill-current text-[#71777B]">
                  <g>
                    <path
                          d="M18.36 5.64c-1.95-1.96-5.11-1.96-7.07 0L9.88 7.05 8.46 5.64l1.42-1.42c2.73-2.73 7.16-2.73 9.9 0 2.73 2.74 2.73 7.17 0 9.9l-1.42 1.42-1.41-1.42 1.41-1.41c1.96-1.96 1.96-5.12 0-7.07zm-2.12 3.53l-7.07 7.07-1.41-1.41 7.07-7.07 1.41 1.41zm-12.02.71l1.42-1.42 1.41 1.42-1.41 1.41c-1.96 1.96-1.96 5.12 0 7.07 1.95 1.96 5.11 1.96 7.07 0l1.41-1.41 1.42 1.41-1.42 1.42c-2.73 2.73-7.16 2.73-9.9 0-2.73-2.74-2.73-7.17 0-9.9z">
                    </path>
                  </g>
              </svg>
              <span>vuemastery.com/courses</span>
          </a>
      </div>
    </div>
  </div>
</template>

Under the Profile bio section above, we have all the necessary texts.

This starts with the profile name and X(twitter) handle. The name is bold with the font styles: "text-xl font-bold" . Then, the handle has a color of text-[#686E73] with a bit of margin on top to separate it from the rest of the bio texts below using mb-3.

Then, we have the bio texts that are separated from each other with margins at the top and bottom. The final part of the bio contains a link just before the main link section. It has margins on top and the bottom using mt-6 mb-3, and we define the color of the link text using text-[#2C9AF0]

Finally, we have the the link section. It consists of the SVG icon and the text. The SVG contains CSS styles describing its size (height and width) using *class*="h-5 w-5". After which, we add some margin to the text and customize the color of the icon using mr-1 fill-current text-[#71777B].

This will result in:

X (Twitter) clone UI with bio section

Adding Tabs to our X (Twitter) Profile

Firstly, let’s go back to our app.config.ts to add custom styles to our container, card and tabs.

export default defineAppConfig({
  ui: {
    strategy: "override",
    primary: "cool",
    avatar: {
      size: {
        xl: "h-32 w-32",
      },
    },
    card: {
      background: "bg-transparent",
      divide: "divide-none",
      ring: "ring-0 ",
      base: "border-b border-gray-600 ",
      rounded: "rounded-none",
    },
    tabs: {
      list: {
        rounded: "rounded-none",
        background: "bg-transparent",
        base: "border-b border-gray-600",
        marker: {
          base: "w-full h-full",
          background: "bg-transparent",
          }
        },
      },
    },
    container: {
      padding: "pa-0",
    },
  },
});

The padding of our container component is set to 0 using pa-0. For our card, we’re setting the background to be transparent. Next, we remove the border, the outline and border-radius with divide-none ,ring-0 , and rounded-none respectively. We then add our preferred border with "border-b border-gray-600".

Similarly, for Tabs we’re defining the border-radius with rounded-none, setting the background color to bg-transparent, and setting the marker ’s base to 100% width and height using w-full h-full. We also add border at the bottom with border-b border-gray-600.

Back on our X profile page, there is a tabs section that has Posts and Replies. So let’s build that out now.

Screenshot 2023-10-19 at 3.23.27 PM.png

Inside the <script/> tag of our Twitter.vue component, we will add three arrays:

  1. The items array will store the number of items in our tabs.
  2. The posts array will contain the content of each individual post.
  3. The replies array will hold the content of each individual reply.

To give our demo UI some content to display inside of these tabs, we’ll be adding in some hard-coded data.

Twitter.vue

<script setup>
const items = [
  {
    key: "posts",
    label: "Posts"
  },
  {
    key: "replies",
    label: "Replies"
  },
];
const posts = [
  {
    content:
      "Our latest course gets you up-and-running with the modern @vitest_dev 🧪",
  },
  {
    content: "New Nuxt UI content coming 🔜 🤓",
  },
];
const replies = [
  {
    content:
      "The Nuxt 3 DevTools are packed full of features. Are you using them to their fullest potential?",
  },
  {
    content: "Are you using the Nuxt 3 DevTools to their full potential?",
  },
];
</script>

This content will be presented as tabs using Nuxt UI’s <UTabs/> component.


<UTabs :items="items">
  <template #default="{ item, selected }">
    <div
      v-if="selected"
      class="flex items-center align-center gap-2 relative border-b-4 border-[#2C9AF0]"
    >
      <span class="m-2 align-center"> {{ item.label }}</span>
    </div>
  </template>
  <template #item="{ item }"></template>
</UTabs>

<UTabs/> loops through our items array to display our tabs. We have only two tabs: Posts and Replies.

Inside <UTabs/> , we have two <template/> components. The first template displays each individual tab item and recognises when the tab is selected by the user. Here, we define the styles for a selected tab by specifying this with the v-if="selected" and the class styles "flex items-center align-center gap-2 relative border-b-4 border-[#2C9AF0]". The most distinctive part of the selected classes is the bottom border.

We then add the actual label of each tab and align it to the center with "m-2 align-center".

The second <template/> contains both tabs’ content.

<template #item="{ item }"></template>

Each tab’s content will be wrapped in <UContainer>. We’ll conditionally display the list of posts or replies using the v-if and v-else-if conditionals.

We’ll use v-if="item*.*key === 'posts'" to display the list of posts:

<UContainer v-if="item.key === 'posts'">

And v-else-if="item*.*key === 'replies'" to display the list of replies:

<UContainer v-else-if="item.key === 'replies'">

Inside of the container, we’ll use v-for to loop over a div of the posts/replies array to display each item, respectively.

<div v-for="(post, index) in posts" :key="{ index }" class="space-y-3">

As we print them out, we’re using the <UCard/> Nuxt UI component to display each item.

<UCard
  :ui="{
    body: {
      padding: 'py-0',
    },
  }"
>

Within the <UCard/> component, let’s add py-2 for the class styles. Then, there’s the :ui prop that overrides the component’s default styles. In the case, it’s the padding prop that’s set to py-0.

<UCard
  :ui="{
    body: {
      padding: 'py-0',
    },
  }"
>
  <div class="flex w-full"></div>
</UCard>

Next, let’s add more UI components inside our <UCard/>. To begin with create a div wrapper and define it’s display as flex and width as w-full.

Each post has an avatar that we’d add using <UAvatar/> . We’ll use <UAvatar/>to pull in the image (logo.jpeg), located in our public folder. Then, add some margin to the left using the class ml-4

<UCard
  :ui="{
    body: {
      padding: 'py-0',
    },
  }"
>
  <div class="flex w-full">
    <UAvatar class="ml-4" src="/logo.jpeg" alt="Avatar" size="md" />
  </div>
</UCard>

Next, we’d add our name, X(twitter) handle, and it’s content. To achieve the correct styling, there’s a bit of margin to the left (ml-3). Then, we add an inner div with flex and items-baseline; this allows the name and twitter handle be side-by-side at the bottom of the div.

<div class="flex w-full">
  <UAvatar class="ml-4" src="/logo.jpeg" alt="Avatar" size="md" />
  <div class="ml-3 w-full">
    <div class="flex items-baseline">
      <h3 class="text-base font-bold">Vue Mastery</h3>
      <span class="text-[#686E73] text-sm ml-2">@VueMastery</<span>
    </div>
    <p>{{ post.content }}</p>
  </div>
</div>

Then, we have the post.content to display the content of the post.

Each post should look just like this:

Screenshot 2023-10-23 at 14.29.04.png

The final piece to our work are the likes, comments, shares, and views icons.

Let’s create a wrapper div that’s styled to display flex. Here, we’d add justify-between to ensure that our icons and their count are well spaced. While mt-3 helps ensure padding on the top.

Each icon and their count is wrapped around a div. We add the text color with text-[#71777B], define the display as flex, and center it’s children with items-center.

The SVG itself has CSS classes to set the height, spacing, and fill it’s color with "h-5 w-5 mr-1 fill-current". The count component has a little spacing between itself and the icon and it’s added as class="ml-2".

<div class="flex w-full justify-between mt-3">
  <div class="flex items-center text-[#71777B]">
    <svg
      viewBox="0 0 24 24"
      aria-hidden="true"
      class="h-5 w-5 mr-1 fill-current"
    >
      <g>
        <path
          d="M1.751 10c0-4.42 3.584-8 8.005-8h4.366c4.49 0 8.129 3.64 8.129 8.13 0 2.96-1.607 5.68-4.196 7.11l-8.054 4.46v-3.69h-.067c-4.49.1-8.183-3.51-8.183-8.01zm8.005-6c-3.317 0-6.005 2.69-6.005 6 0 3.37 2.77 6.08 6.138 6.01l.351-.01h1.761v2.3l5.087-2.81c1.951-1.08 3.163-3.13 3.163-5.36 0-3.39-2.744-6.13-6.129-6.13H9.756z"
        ></path>
      </g>
    </svg>
    <h3 class="ml-2">2</h3>
  </div>
  <div class="flex items-center text-[#71777B]">
    <svg
      viewBox="0 0 24 24"
      aria-hidden="true"
      class="h-5 w-5 mr-1 fill-current"
    >
      <g>
        <path
          d="M4.5 3.88l4.432 4.14-1.364 1.46L5.5 7.55V16c0 1.1.896 2 2 2H13v2H7.5c-2.209 0-4-1.79-4-4V7.55L1.432 9.48.068 8.02 4.5 3.88zM16.5 6H11V4h5.5c2.209 0 4 1.79 4 4v8.45l2.068-1.93 1.364 1.46-4.432 4.14-4.432-4.14 1.364-1.46 2.068 1.93V8c0-1.1-.896-2-2-2z"
        ></path>
      </g>
    </svg>
    <h3 class="ml-2">1</h3>
  </div>
  <div class="flex items-center text-[#71777B]">
    <svg
      viewBox="0 0 24 24"
      aria-hidden="true"
      class="h-5 w-5 mr-1 fill-current"
    >
      <g>
        <path
          d="M16.697 5.5c-1.222-.06-2.679.51-3.89 2.16l-.805 1.09-.806-1.09C9.984 6.01 8.526 5.44 7.304 5.5c-1.243.07-2.349.78-2.91 1.91-.552 1.12-.633 2.78.479 4.82 1.074 1.97 3.257 4.27 7.129 6.61 3.87-2.34 6.052-4.64 7.126-6.61 1.111-2.04 1.03-3.7.477-4.82-.561-1.13-1.666-1.84-2.908-1.91zm4.187 7.69c-1.351 2.48-4.001 5.12-8.379 7.67l-.503.3-.504-.3c-4.379-2.55-7.029-5.19-8.382-7.67-1.36-2.5-1.41-4.86-.514-6.67.887-1.79 2.647-2.91 4.601-3.01 1.651-.09 3.368.56 4.798 2.01 1.429-1.45 3.146-2.1 4.796-2.01 1.954.1 3.714 1.22 4.601 3.01.896 1.81.846 4.17-.514 6.67z"
        ></path>
      </g>
    </svg>
    <h3 class="ml-2">1</h3>
  </div>
  <div class="flex items-center text-[#71777B]">
    <svg
      viewBox="0 0 24 24"
      aria-hidden="true"
      class="h-5 w-5 mr-1 fill-current"
    >
      <g>
        <path
          d="M8.75 21V3h2v18h-2zM18 21V8.5h2V21h-2zM4 21l.004-10h2L6 21H4zm9.248 0v-7h2v7h-2z"
        ></path>
      </g>
    </svg>
    <h3 class="ml-2">100</h3>
  </div>
  <div class="flex items-center pr-3">
    <svg
      viewBox="0 0 24 24"
      aria-hidden="true"
      class="h-5 w-5 mr-1 rounded-full fill-current text-[#71777B]"
    >
      <g>
        <path
          d="M12 2.59l5.7 5.7-1.41 1.42L13 6.41V16h-2V6.41l-3.3 3.3-1.41-1.42L12 2.59zM21 15l-.02 3.51c0 1.38-1.12 2.49-2.5 2.49H5.5C4.11 21 3 19.88 3 18.5V15h2v3.5c0 .28.22.5.5.5h12.98c.28 0 .5-.22.5-.5L19 15h2z"
        ></path>
      </g>
    </svg>
  </div>
</div>

One thing to note is that the last div containing the share SVG has an extra padding to the right (pr-3) to add extra spacing to the edge.

Screenshot 2023-10-23 at 15.04.26.png


We’ve successfully displayed our Posts.

Post section of the X page

As mentioned above, Replies basically duplicates the same thing we’ve done for our Posts using the replies array. In the spirit of the DRY (Don’t Repeat Yourself) principle, let’s have everything inside another component and import it into our own Twitter.vue component.

Create a new component inside our components folder. Lets’s call it **Tweets.vue**.

// Tweets.vue

<script setup>
  const props = defineProps({
    items: {
      type: Array,
      required: true,
    },
  });
  const { items } = props;
</script>

<template>
  <UContainer>
    <div :key="{ index }" v-for="(item, index) in items" class="space-y-3">
      <UCard
        class="py-2"
        :ui="{
          body: {
            padding: 'py-0',
          },
        }"
      >
        <div class="flex w-full">
          <UAvatar class="ml-4" src="/logo.jpeg" alt="Avatar" size="md" />
          <div class="ml-3 w-full">
            <div class="flex items-baseline">
              <h3 class="text-base font-bold">Vue Mastery</h3>
              <h6 class="text-[#686E73] text-sm ml-2">@VueMastery</h6>
            </div>

            <h5>{{ item.content }}</h5>
            <div class="flex w-full justify-between mt-3">
              <div class="flex items-center">
                <svg
                  viewBox="0 0 24 24"
                  aria-hidden="true"
                  class="h-5 w-5 mr-1 rounded-full fill-current text-[#71777B]"
                >
                  <g>
                    <path
                      d="M1.751 10c0-4.42 3.584-8 8.005-8h4.366c4.49 0 8.129 3.64 8.129 8.13 0 2.96-1.607 5.68-4.196 7.11l-8.054 4.46v-3.69h-.067c-4.49.1-8.183-3.51-8.183-8.01zm8.005-6c-3.317 0-6.005 2.69-6.005 6 0 3.37 2.77 6.08 6.138 6.01l.351-.01h1.761v2.3l5.087-2.81c1.951-1.08 3.163-3.13 3.163-5.36 0-3.39-2.744-6.13-6.129-6.13H9.756z"
                    ></path>
                  </g>
                </svg>
                <h3 class="ml-2 text-[#71777B]">2</h3>
              </div>
              <div class="flex items-center">
                <svg
                  viewBox="0 0 24 24"
                  aria-hidden="true"
                  class="h-5 w-5 mr-1 rounded-full fill-current text-[#71777B]"
                >
                  <g>
                    <path
                      d="M4.5 3.88l4.432 4.14-1.364 1.46L5.5 7.55V16c0 1.1.896 2 2 2H13v2H7.5c-2.209 0-4-1.79-4-4V7.55L1.432 9.48.068 8.02 4.5 3.88zM16.5 6H11V4h5.5c2.209 0 4 1.79 4 4v8.45l2.068-1.93 1.364 1.46-4.432 4.14-4.432-4.14 1.364-1.46 2.068 1.93V8c0-1.1-.896-2-2-2z"
                    ></path>
                  </g>
                </svg>
                <h3 class="ml-2 text-[#71777B]">1</h3>
              </div>
              <div class="flex items-center">
                <svg
                  viewBox="0 0 24 24"
                  aria-hidden="true"
                  class="h-5 w-5 mr-1 rounded-full fill-current text-[#71777B]"
                >
                  <g>
                    <path
                      d="M16.697 5.5c-1.222-.06-2.679.51-3.89 2.16l-.805 1.09-.806-1.09C9.984 6.01 8.526 5.44 7.304 5.5c-1.243.07-2.349.78-2.91 1.91-.552 1.12-.633 2.78.479 4.82 1.074 1.97 3.257 4.27 7.129 6.61 3.87-2.34 6.052-4.64 7.126-6.61 1.111-2.04 1.03-3.7.477-4.82-.561-1.13-1.666-1.84-2.908-1.91zm4.187 7.69c-1.351 2.48-4.001 5.12-8.379 7.67l-.503.3-.504-.3c-4.379-2.55-7.029-5.19-8.382-7.67-1.36-2.5-1.41-4.86-.514-6.67.887-1.79 2.647-2.91 4.601-3.01 1.651-.09 3.368.56 4.798 2.01 1.429-1.45 3.146-2.1 4.796-2.01 1.954.1 3.714 1.22 4.601 3.01.896 1.81.846 4.17-.514 6.67z"
                    ></path>
                  </g>
                </svg>
                <h3 class="ml-2 text-[#71777B]">1</h3>
              </div>
              <div class="flex items-center">
                <svg
                  viewBox="0 0 24 24"
                  aria-hidden="true"
                  class="h-5 w-5 mr-1 rounded-full fill-current text-[#71777B]"
                >
                  <g>
                    <path
                      d="M8.75 21V3h2v18h-2zM18 21V8.5h2V21h-2zM4 21l.004-10h2L6 21H4zm9.248 0v-7h2v7h-2z"
                    ></path>
                  </g>
                </svg>
                <h3 class="ml-2 text-[#71777B]">100</h3>
              </div>
              <div class="flex items-center pr-3">
                <svg
                  viewBox="0 0 24 24"
                  aria-hidden="true"
                  class="h-5 w-5 mr-1 rounded-full fill-current text-[#71777B]"
                >
                  <g>
                    <path
                      d="M12 2.59l5.7 5.7-1.41 1.42L13 6.41V16h-2V6.41l-3.3 3.3-1.41-1.42L12 2.59zM21 15l-.02 3.51c0 1.38-1.12 2.49-2.5 2.49H5.5C4.11 21 3 19.88 3 18.5V15h2v3.5c0 .28.22.5.5.5h12.98c.28 0 .5-.22.5-.5L19 15h2z"
                    ></path>
                  </g>
                </svg>
              </div>
            </div>
          </div>
        </div>
      </UCard>
    </div>
  </UContainer>
</template>

We create an items prop that we’d use to pass in either the posts or replies array from our Twitter.vue. We then move the entire <UContainer/> into this component. This loops over the items array prop at every point in time.

Go back into the Twitter.vue component and replace the entire <UTabs/> component with this below:

<UTabs :items="items">
  <template #default="{ item, selected }">
    <div
      v-if="selected"
      class="flex items-center align-center gap-2 relative border-b-4 border-[#2C9AF0]"
    >
      <span class="m-2 align-center"> {{ item.label }}</span>
    </div>
  </template>
  <template #item="{ item }">
    <Tweets v-if="item.key === 'posts'" :items="posts" />

    <Tweets v-else-if="item.key === 'replies'" :items="replies" />
  </template>
</UTabs>

All we’ve done here is to replace the both <UContainer/> components with the <Tweets/> components. This now works perfectly for both Posts and Replies.

Here’s the final version of our demo:

Final version of our X (Twitter) profile UI demo


Continue Learning

Congratulations on completing this tutorial. We covered the steps you’ll need to use Nuxt UI in your project. From installation to initialization with the correct config, and finally using the components within our project.

You can checkout the official Nuxt UI documentation to explore the powerful features of Nuxt UI and incorporate the various components within your own projects. To continue deepening your understanding of the Nuxt ecosystem, check out the courses recommended below.

Download the cheatsheets

Save time and energy with our cheat sheets.