NJ
Vue 3 Components: Building Reusable UI Blocks | Nerando Johnson
Vue.js logo on a light blue background with geometric shapes Vue, JavaScript, Tutorials

Vue 3 Components: Building Reusable UI Blocks

Nerando Johnson
#vue3 #javascript #frontend #tutorial #components #composition-api

6 min read

6 views

Vue 3 Components: Building Reusable UI Blocks

Part 4 of the Vue 3 Fundamentals series — understanding SFCs, props, and component events.


Every Vue application is a tree of components.

At the root, you have App.vue. Underneath it, you have everything else — your header, your search bar, your individual weather cards. Each is a self-contained piece of UI with its own logic, template, and styles.

This is the component model. It’s how Vue (and every modern framework) manages complexity. Instead of one massive HTML file with thousands of lines of JavaScript, you break your UI into small, focused, reusable pieces.

In this article, we’ll look at what components are, how to build them, and how to make them talk to each other — using our weather dashboard as the domain throughout.


What Is a Component?

Breaking UI into reusable pieces

A Vue component is a reusable, self-contained unit of UI. It encapsulates three things:

  • Template — the HTML structure
  • Script — the logic (data, methods, computed values)
  • Style — the CSS that applies to it

In Vue 3, we write components as Single-File Components (SFCs) — .vue files that contain all three in one place:

<!-- src/components/WeatherCard.vue -->
<script setup>
const conditionEmoji = {
  Sunny: '☀️',
  Cloudy: '☁️',
  Rainy: '🌧️',
  Windy: '💨',
  Snowy: '❄️',
}
</script>

<template>
  <div class="card">
    <h2>Atlanta, GA</h2>
    <p class="temp">72°F</p>
    <p class="condition">{{ conditionEmoji['Sunny'] }} Sunny</p>
  </div>
</template>

<style scoped>
.card {
  padding: 1.5rem;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  background: white;
}
.temp { font-size: 2.5rem; font-weight: bold; margin: 0.25rem 0; }
.condition { color: #64748b; }
</style>

That’s a complete component. No external dependencies. Drop it anywhere in your app and it works.

Using a Component

Plugging components into the app

To use WeatherCard in App.vue, import it and place it in the template:

<!-- src/App.vue -->
<script setup>
import WeatherCard from './components/WeatherCard.vue'
</script>

<template>
  <main>
    <h1>🌍 Weather Dashboard</h1>
    <WeatherCard />
  </main>
</template>

With <script setup>, imported components are automatically available in the template — no registration step needed.

Notice the <WeatherCard /> syntax — Vue components use PascalCase in templates, which visually distinguishes them from native HTML elements like <div> and <input>.

Props: Passing Data Into a Component

Passing data down to child components

A component that always shows the same city and temperature isn’t very useful. Props let a parent pass data into a child.

Let’s update WeatherCard to accept real city data:

<!-- src/components/WeatherCard.vue -->
<script setup>
import { computed } from 'vue'

const props = defineProps({
  city: {
    type: Object,
    required: true
  },
  unit: {
    type: String,
    default: 'F'
  }
})

const conditionEmoji = {
  Sunny: '☀️', Cloudy: '☁️', Rainy: '🌧️', Windy: '💨', Snowy: '❄️',
}

const displayTemp = computed(() =>
  props.unit === 'C'
    ? Math.round((props.city.temp - 32) * 5 / 9)
    : props.city.temp
)
</script>

<template>
  <div class="card">
    <h2>{{ city.name }}</h2>
    <p class="temp">{{ displayTemp }}°{{ unit }}</p>
    <p class="condition">
      {{ conditionEmoji[city.condition] ?? '🌡️' }}
      {{ city.condition }}
    </p>
    <p class="meta">💧 {{ city.humidity }}% · 💨 {{ city.wind }} mph</p>
  </div>
</template>

defineProps is a Vue macro — you don’t import it, it’s built into <script setup>. It declares what data the component expects to receive.

Now the parent passes city data as attributes:

<WeatherCard
  :city="{ id: 1, name: 'Atlanta, GA', temp: 72, condition: 'Sunny', humidity: 45, wind: 8 }"
  unit="F"
/>

Props flow downward — from parent to child. A child component should never directly modify the props it receives. If you need to signal a change back, the child communicates upward through events.

Events: Sending Data Back Up

Events flowing back up to the parent

When something happens inside a child — a button click, a user removing a city — the child emits an event and the parent listens for it.

Let’s add a “Remove” button to WeatherCard:

<!-- src/components/WeatherCard.vue -->
<script setup>
import { computed } from 'vue'

const props = defineProps({
  city: { type: Object, required: true },
  unit: { type: String, default: 'F' }
})

const emit = defineEmits(['remove'])

const conditionEmoji = { Sunny: '☀️', Cloudy: '☁️', Rainy: '🌧️', Windy: '💨', Snowy: '❄️' }

const displayTemp = computed(() =>
  props.unit === 'C'
    ? Math.round((props.city.temp - 32) * 5 / 9)
    : props.city.temp
)
</script>

<template>
  <div class="card">
    <header>
      <h2>{{ city.name }}</h2>
      <button class="remove-btn" @click="emit('remove', city.id)"></button>
    </header>
    <p class="temp">{{ displayTemp }}°{{ unit }}</p>
    <p class="condition">{{ conditionEmoji[city.condition] ?? '🌡️' }} {{ city.condition }}</p>
    <p class="meta">💧 {{ city.humidity }}% · 💨 {{ city.wind }} mph</p>
  </div>
</template>

defineEmits declares what events this component can fire. When the remove button is clicked, we emit 'remove' with the city’s id as the payload.

The parent listens with @event-name:

<!-- src/App.vue -->
<script setup>
import { ref } from 'vue'
import WeatherCard from './components/WeatherCard.vue'

const cities = ref([
  { id: 1, name: 'Atlanta, GA',  temp: 72, condition: 'Sunny',  humidity: 45, wind: 8  },
  { id: 2, name: 'New York, NY', temp: 61, condition: 'Cloudy', humidity: 60, wind: 12 },
])
const unit = ref('F')

function removeCity(id) {
  cities.value = cities.value.filter(c => c.id !== id)
}
</script>

<template>
  <main>
    <WeatherCard
      v-for="city in cities"
      :key="city.id"
      :city="city"
      :unit="unit"
      @remove="removeCity"
    />
  </main>
</template>

This is the props down, events up pattern. The parent owns the data. The child only reports what happened. Data flow stays predictable and components stay decoupled.

Scoped Styles

Styles that stay in their lane

The scoped attribute on <style> means the styles in a component only apply to that component’s HTML:

<style scoped>
/* This .card class won't affect any other .card elements in the app */
.card {
  background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
  border-radius: 12px;
}
</style>

Vue achieves this by adding a unique data attribute to the component’s elements at compile time. No naming collisions, no specificity battles between components.

Component Organization

Keeping things organized

As your app grows, keep your components folder organized by responsibility:

src/
└── components/
    ├── WeatherCard.vue     → Individual city weather card
    ├── WeatherSearch.vue   → City search input + add button
    └── UnitToggle.vue      → °F / °C switcher

Each component has one clear job. WeatherCard displays weather for one city. WeatherSearch handles the search/add flow. UnitToggle switches the temperature unit. None of them need to know what the others are doing.

That separation makes each component independently testable — and means you can update one without risking breaking the others.

The Mental Model

The full picture coming together
  • SFC → one .vue file = template + script + scoped styles.
  • defineProps → declare what data flows in from the parent.
  • defineEmits → declare what events flow out to the parent.
  • Props down, events up → the rule that keeps data flow predictable.
  • Scoped styles → CSS that stays in its lane.

Your Turn

Your turn

Try these before moving to Article 5:

  1. Create a UnitToggle.vue component that emits a 'toggle' event when clicked.
  2. In App.vue, listen to @toggle and flip the unit ref between 'F' and 'C'.
  3. Pass the updated unit as a prop to each WeatherCard and watch all the temperatures update at once.

If all three work — you’ve just implemented the entire props-down events-up pattern on a real feature.

What’s Next

What's next

In Article 5, we’ll cover the most important directives you’ll use every day: v-if, v-for, v-bind, v-model, and v-on.

Progress over perfection. Let’s go.


This is Part 4 of the Vue 3 Fundamentals series. Sources: Vue.js Component Basics, Vue School — Vue Component Fundamentals with the Composition API.