Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 075 / 100

v-if vs v-show: what every Vue & Nuxt developer should know

Choose the right conditional.

Expona Admin8 min read

Introduction

After seventy-five days of building with Vue.js and Nuxt, you've probably reached for conditional rendering without thinking twice. But deciding between v-if and v-show—two tools that look similar on the surface—can mean the difference between snappy UX and unnecessary re-renders in production. In this article, we'll explore v-if vs v-show: what every Vue & Nuxt developer should know, so you can confidently choose the right conditional for every situation.

v-if adds and removes elements

v-if is a structural directive that completely removes or adds elements from the DOM based on a boolean condition. When the condition evaluates to false, the element and all its child components are unmounted. When it becomes true, Vue creates and mounts them fresh.

This is powerful because it means you're not rendering anything unnecessary. In a large application, this can save memory and processing power. However, there's a cost: mounting and unmounting is more expensive than simply hiding and showing.

Consider a real scenario: a modal dialog that appears rarely, only when a user wants to confirm a destructive action.

typescript
<template>
  <div>
    <button @click="showDeleteModal = true">Delete Account</button>
    
    <div v-if="showDeleteModal" class="modal-overlay">
      <div class="modal">
        <h2>Delete your account?</h2>
        <p>This action cannot be undone.</p>
        <button @click="deleteAccount()">Confirm Delete</button>
        <button @click="showDeleteModal = false">Cancel</button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const showDeleteModal = ref(false)

const deleteAccount = async () => {
  // API call
  await fetch('/api/account', { method: 'DELETE' })
  showDeleteModal.value = false
}
</script>

When showDeleteModal is false, the entire modal block—including its child elements, event listeners, and any nested components—is removed from the DOM. This is ideal here because the modal appears infrequently and we don't need to keep it resident in memory.

v-show toggles CSS display

v-show, by contrast, always keeps the element in the DOM. It simply toggles the CSS display property between none and its default value (or whatever you set). The element is mounted once and reused; only its visibility changes.

This makes v-show faster for frequent toggling because there's no mount/unmount overhead. However, the element remains in the DOM even when hidden, consuming memory and potentially interfering with CSS selectors or layout calculations.

A practical example: a collapsible section in a settings panel that users toggle back and forth repeatedly.

typescript
<template>
  <div class="settings-panel">
    <div class="section">
      <button @click="advancedOpen = !advancedOpen" class="section-header">
        Advanced Settings
        <span class="icon">{{ advancedOpen ? '▼' : '▶' }}</span>
      </button>
      
      <div v-show="advancedOpen" class="section-content">
        <label>
          <input type="checkbox" v-model="enableDebugMode" />
          Enable Debug Mode
        </label>
        <label>
          <input type="number" v-model.number="cacheTTL" />
          Cache TTL (seconds)
        </label>
        <label>
          <input type="checkbox" v-model="enableAnalytics" />
          Send Anonymous Analytics
        </label>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const advancedOpen = ref(false)
const enableDebugMode = ref(false)
const cacheTTL = ref(3600)
const enableAnalytics = ref(true)
</script>

<style scoped>
.section-content {
  transition: opacity 0.2s ease;
}
</style>

Here, users might toggle the Advanced Settings section multiple times per session. With v-show, we avoid the overhead of repeatedly mounting and unmounting the form inputs. The DOM structure stays stable, and CSS transitions can animate the visibility smoothly.

v-show for frequent toggles

When your condition changes often—think accordions, expandable filters, tabs, or dropdowns—v-show is the right choice. The single mount cost is paid once, and each toggle after that is just a CSS change. Performance-sensitive dashboards and real-time interfaces benefit from this pattern.

In a Nuxt application, if you're building a complex SPA with many collapsible sections, using v-show keeps your interaction responsive because Vue isn't constantly re-creating component instances.

Here's a more complex example: a data table with expandable rows.

typescript
<template>
  <table>
    <tbody>
      <template v-for="user in users" :key="user.id">
        <tr class="row-main" @click="toggleRowDetails(user.id)">
          <td>{{ user.name }}</td>
          <td>{{ user.email }}</td>
          <td><span class="toggle-icon">{{ expandedRows.has(user.id) ? '−' : '+' }}</span></td>
        </tr>
        
        <tr v-show="expandedRows.has(user.id)" class="row-details">
          <td colspan="3">
            <div class="details-content">
              <p><strong>Phone:</strong> {{ user.phone }}</p>
              <p><strong>Address:</strong> {{ user.address }}</p>
              <p><strong>Joined:</strong> {{ formatDate(user.createdAt) }}</p>
            </div>
          </td>
        </tr>
      </template>
    </tbody>
  </table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

interface User {
  id: number
  name: string
  email: string
  phone: string
  address: string
  createdAt: string
}

const users = ref<User[]>([
  { id: 1, name: 'Alice', email: 'alice@example.com', phone: '555-0001', address: '123 Main St', createdAt: '2023-01-15' },
  { id: 2, name: 'Bob', email: 'bob@example.com', phone: '555-0002', address: '456 Oak Ave', createdAt: '2023-02-20' },
])

const expandedRows = ref<Set<number>>(new Set())

const toggleRowDetails = (userId: number) => {
  if (expandedRows.value.has(userId)) {
    expandedRows.value.delete(userId)
  } else {
    expandedRows.value.add(userId)
  }
}

const formatDate = (dateStr: string) => new Date(dateStr).toLocaleDateString()
</script>

Users will expand and collapse rows repeatedly. Using v-show means each toggle is instant, with no re-render cost for mounting the detail row.

v-if for rarely shown content

v-if shines when content appears once in a blue moon, or only under specific conditions that rarely change. Empty states, error messages, success notifications, or initialization screens are good candidates. You want to keep your DOM lean and avoid maintaining component state for things that won't be visible most of the time.

Imagine an onboarding flow where users see different screens based on their account setup progress:

typescript
<template>
  <div class="onboarding-container">
    <div v-if="!accountCreated" class="step">
      <h2>Create Your Account</h2>
      <CreateAccountForm @complete="accountCreated = true" />
    </div>
    
    <div v-else-if="!profileComplete" class="step">
      <h2>Complete Your Profile</h2>
      <ProfileForm @complete="profileComplete = true" />
    </div>
    
    <div v-else-if="!billingAdded" class="step">
      <h2>Add Billing Information</h2>
      <BillingForm @complete="billingAdded = true" />
    </div>
    
    <div v-else class="step success">
      <h2>You're All Set!</h2>
      <p>Your account is ready to use.</p>
      <NuxtLink to="/dashboard">Go to Dashboard</NuxtLink>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const accountCreated = ref(false)
const profileComplete = ref(false)
const billingAdded = ref(false)
</script>

Here, only one step is visible at a time, and the flow is linear. Using v-if with v-else-if chains ensures we never keep previous steps mounted. Each component is created and destroyed as the user progresses. This keeps memory usage low and makes it easy to reset state between steps.

Common mistakes

Mistake 1: Using v-show for everything. Some developers default to v-show thinking it's always faster. But if a modal or sidebar appears once per session, you're keeping unnecessary DOM around. Use v-if for truly conditional content.

Mistake 2: Using v-if for high-frequency toggles. Toggling a UI control hundreds of times per second with v-if will cause performance issues. Buttons, tabs, and filters should use v-show.

Mistake 3: Forgetting about CSS transitions with v-show. When you use v-show, transitions still work, but you need to ensure your CSS doesn't conflict. A hidden element with display: none won't receive pointer events, but CSS can still affect layout—be cautious with visibility: hidden vs. display: none.

Mistake 4: Relying on component lifecycle in hidden v-show elements. If you hide a component with v-show, it's still mounted. If you expected onMounted to run when it becomes visible, you'll be disappointed. The hook ran when the component was first created, not when it was shown.

typescript
<!-- This component mounts ONCE, when the page loads -->
<div v-show="isVisible">
  <MyComponent /> <!-- onMounted fires here, not when isVisible becomes true -->
</div>

<!-- This component mounts EACH TIME condition is true -->
<div v-if="isVisible">
  <MyComponent /> <!-- onMounted fires each time isVisible becomes true -->
</div>

Key takeaways

  • v-if removes elements from the DOM entirely when the condition is false, making it memory-efficient for rarely shown content but more expensive when toggling frequently.
  • v-show keeps elements in the DOM and toggles display: none, making it fast for frequent toggles but less memory-efficient when elements are rarely visible.
  • For modals, drawers, and initialization screens, prefer v-if. They appear infrequently, so the mount cost is negligible.
  • For accordions, tabs, filters, and collapsible sections, prefer v-show. Users interact with them repeatedly in a session.
  • Component lifecycle hooks differ between the two. With v-if, onMounted fires each time the condition becomes true. With v-show, onMounted fires only once.
  • Profile before deciding. In doubt, measure. Use browser DevTools to inspect how many times components mount or unmount, and watch for layout thrashing.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles