Vue.js

The Progressive JavaScript Framework. Incrementally adoptable with low learning curve, providing two-way data binding and component system.

JavaScriptFrameworkReactiveProgressiveComponentSPAUI

GitHub Overview

vuejs/core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

Stars51,272
Watchers754
Forks8,824
Created:June 12, 2018
Language:TypeScript
License:MIT License

Topics

None

Star History

vuejs/core Star History
Data as of: 8/13/2025, 01:43 AM

Framework

Vue.js

Overview

Vue.js is a progressive JavaScript framework designed for building user interfaces on the web. It is lightweight, easy to learn, and can be incrementally adopted.

Details

Vue.js (Vue) is a progressive JavaScript framework developed by Evan You in 2014. "Progressive" means it can be incrementally adopted into existing projects, supporting everything from small applications to large-scale SPAs. Vue 3 introduces the Composition API, which significantly improves component logic organization and reusability. Key features include reactive data binding (Proxy-based reactivity system), component-oriented architecture, virtual DOM, Single File Components (SFC), choice between Options API and Composition API, Vue Router (official router), and Vuex/Pinia (state management). Vue.js has a low learning curve with intuitive template syntax and excellent TypeScript support. It has a rich ecosystem including high-quality tools like Nuxt.js, Vite, VuePress, and VitePress, and is widely adopted by companies like GitLab, Adobe, Nintendo, and BMW.

Pros and Cons

Pros

  • Easy to Learn: Intuitive template syntax and gradual adoption approach
  • High Flexibility: Progressive framework supporting partial usage to full SPA development
  • Excellent Performance: Lightweight virtual DOM and compile-time optimizations
  • Reactive System: Vue 3's Proxy-based reactivity for efficient data binding
  • Rich Ecosystem: High-quality tools like Vue Router, Pinia, Nuxt.js, and Vite
  • TypeScript Support: Excellent type inference and development experience with Volar
  • Component-Oriented: Reusable component architecture
  • Great Documentation: Detailed and clear official documentation

Cons

  • Ecosystem Compared to React/Angular: Relatively fewer third-party library options
  • Enterprise Adoption: Fewer large-scale enterprise adoption cases compared to others
  • Developer Availability: Fewer Vue.js specialists compared to React/Angular developers
  • Version Migration Impact: Some breaking changes from Vue 2 to Vue 3 migration
  • Performance Tuning: Optimization can become complex in large applications
  • Internationalization: Smaller global community compared to React and others

Key Links

Code Examples

Hello World

<!-- App.vue -->
<template>
  <div class="app">
    <h1>{{ greeting }}</h1>
    <p>Count: {{ count }}</p>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
    <button @click="reset">Reset</button>
    
    <!-- Conditional rendering -->
    <p v-if="count > 10" class="warning">
      Count is getting high!
    </p>
    <p v-else-if="count < 0" class="error">
      Count is negative!
    </p>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

// Reactive state
const count = ref(0)
const name = ref('Vue.js')

// Computed property
const greeting = computed(() => {
  return `Welcome to ${name.value}!`
})

// Methods
function increment() {
  count.value++
}

function decrement() {
  count.value--
}

function reset() {
  count.value = 0
}
</script>

<style scoped>
.app {
  text-align: center;
  padding: 2rem;
  font-family: Arial, sans-serif;
}

h1 {
  color: #42b883;
  margin-bottom: 1rem;
}

button {
  margin: 0 0.5rem;
  padding: 0.5rem 1rem;
  background-color: #42b883;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #369870;
}

.warning {
  color: #f56565;
}

.error {
  color: #e53e3e;
}
</style>

Component Creation and Props

<!-- UserCard.vue -->
<template>
  <div class="user-card">
    <img :src="user.avatar" :alt="user.name" class="avatar" />
    <div class="user-info">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
      <span class="role" :class="roleClass">{{ user.role }}</span>
    </div>
    <div class="actions">
      <button @click="$emit('edit', user)" class="btn btn-primary">
        Edit
      </button>
      <button @click="$emit('delete', user.id)" class="btn btn-danger">
        Delete
      </button>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'

// Props definition
const props = defineProps({
  user: {
    type: Object,
    required: true,
    validator: (user) => {
      return user && user.id && user.name && user.email
    }
  }
})

// Emits definition
const emit = defineEmits(['edit', 'delete'])

// Computed property
const roleClass = computed(() => {
  const roleMap = {
    admin: 'role-admin',
    user: 'role-user',
    guest: 'role-guest'
  }
  return roleMap[props.user.role] || 'role-default'
})
</script>

<style scoped>
.user-card {
  display: flex;
  align-items: center;
  padding: 1rem;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  gap: 1rem;
  margin-bottom: 1rem;
}

.avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  object-fit: cover;
}

.user-info {
  flex: 1;
}

.user-info h3 {
  margin: 0 0 0.5rem 0;
  color: #2d3748;
}

.user-info p {
  margin: 0;
  color: #718096;
}

.role {
  display: inline-block;
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  font-size: 0.875rem;
  font-weight: 500;
}

.role-admin {
  background-color: #fed7d7;
  color: #c53030;
}

.role-user {
  background-color: #bee3f8;
  color: #2b6cb0;
}

.role-guest {
  background-color: #f7fafc;
  color: #4a5568;
}

.actions {
  display: flex;
  gap: 0.5rem;
}

.btn {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.875rem;
}

.btn-primary {
  background-color: #4299e1;
  color: white;
}

.btn-danger {
  background-color: #f56565;
  color: white;
}
</style>

Reactive State and Watchers

<!-- UserList.vue -->
<template>
  <div class="user-list">
    <div class="filters">
      <input
        v-model="searchQuery"
        type="text"
        placeholder="Search users..."
        class="search-input"
      />
      <select v-model="selectedRole" class="role-filter">
        <option value="">All roles</option>
        <option value="admin">Admin</option>
        <option value="user">User</option>
        <option value="guest">Guest</option>
      </select>
    </div>

    <div class="stats">
      <p>Showing {{ filteredUsers.length }} of {{ users.length }} users</p>
    </div>

    <div v-if="loading" class="loading">
      Loading...
    </div>

    <div v-else class="users">
      <UserCard
        v-for="user in filteredUsers"
        :key="user.id"
        :user="user"
        @edit="editUser"
        @delete="deleteUser"
      />
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import UserCard from './UserCard.vue'

// Reactive state
const users = ref([])
const searchQuery = ref('')
const selectedRole = ref('')
const loading = ref(false)

// Computed property
const filteredUsers = computed(() => {
  let filtered = users.value

  // Filter by search query
  if (searchQuery.value) {
    const query = searchQuery.value.toLowerCase()
    filtered = filtered.filter(user =>
      user.name.toLowerCase().includes(query) ||
      user.email.toLowerCase().includes(query)
    )
  }

  // Filter by role
  if (selectedRole.value) {
    filtered = filtered.filter(user => user.role === selectedRole.value)
  }

  return filtered
})

// Watchers
watch(searchQuery, (newQuery) => {
  console.log('Search query changed:', newQuery)
  // Can implement debounce logic here
})

watch(selectedRole, (newRole) => {
  console.log('Selected role changed:', newRole)
})

// Watch multiple values
watch([searchQuery, selectedRole], ([newQuery, newRole]) => {
  console.log('Filters updated:', { newQuery, newRole })
})

// Lifecycle hook
onMounted(() => {
  fetchUsers()
})

// Methods
async function fetchUsers() {
  loading.value = true
  try {
    // Fetch user data from API (mock data)
    await new Promise(resolve => setTimeout(resolve, 1000))
    users.value = [
      { id: 1, name: 'John Doe', email: '[email protected]', role: 'admin', avatar: 'https://i.pravatar.cc/150?img=1' },
      { id: 2, name: 'Jane Smith', email: '[email protected]', role: 'user', avatar: 'https://i.pravatar.cc/150?img=2' },
      { id: 3, name: 'Bob Johnson', email: '[email protected]', role: 'user', avatar: 'https://i.pravatar.cc/150?img=3' },
      { id: 4, name: 'Alice Brown', email: '[email protected]', role: 'guest', avatar: 'https://i.pravatar.cc/150?img=4' }
    ]
  } catch (error) {
    console.error('Failed to fetch user data:', error)
  } finally {
    loading.value = false
  }
}

function editUser(user) {
  console.log('Edit user:', user)
  // Implement edit logic
}

function deleteUser(userId) {
  if (confirm('Are you sure you want to delete this user?')) {
    users.value = users.value.filter(user => user.id !== userId)
    console.log('User deleted:', userId)
  }
}
</script>

<style scoped>
.user-list {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

.filters {
  display: flex;
  gap: 1rem;
  margin-bottom: 1rem;
}

.search-input,
.role-filter {
  padding: 0.5rem;
  border: 1px solid #d2d6dc;
  border-radius: 4px;
  font-size: 1rem;
}

.search-input {
  flex: 1;
}

.stats {
  margin-bottom: 1rem;
  color: #718096;
}

.loading {
  text-align: center;
  padding: 2rem;
  color: #4a5568;
}

.users {
  display: grid;
  gap: 1rem;
}
</style>

Form Handling and Validation

<!-- ContactForm.vue -->
<template>
  <form @submit.prevent="submitForm" class="contact-form">
    <h2>Contact Form</h2>

    <div class="form-group">
      <label for="name">Name *</label>
      <input
        id="name"
        v-model="form.name"
        type="text"
        required
        :class="{ error: errors.name }"
        @blur="validateField('name')"
      />
      <span v-if="errors.name" class="error-message">{{ errors.name }}</span>
    </div>

    <div class="form-group">
      <label for="email">Email Address *</label>
      <input
        id="email"
        v-model="form.email"
        type="email"
        required
        :class="{ error: errors.email }"
        @blur="validateField('email')"
      />
      <span v-if="errors.email" class="error-message">{{ errors.email }}</span>
    </div>

    <div class="form-group">
      <label for="category">Category</label>
      <select id="category" v-model="form.category">
        <option value="">Please select</option>
        <option value="general">General Inquiry</option>
        <option value="support">Support</option>
        <option value="business">Business</option>
        <option value="bug">Bug Report</option>
      </select>
    </div>

    <div class="form-group">
      <label for="message">Message *</label>
      <textarea
        id="message"
        v-model="form.message"
        rows="5"
        required
        :class="{ error: errors.message }"
        @blur="validateField('message')"
      ></textarea>
      <span v-if="errors.message" class="error-message">{{ errors.message }}</span>
    </div>

    <div class="form-group">
      <label class="checkbox-label">
        <input
          v-model="form.agreeToTerms"
          type="checkbox"
          required
        />
        I agree to the terms and conditions *
      </label>
      <span v-if="errors.agreeToTerms" class="error-message">{{ errors.agreeToTerms }}</span>
    </div>

    <button type="submit" :disabled="!isFormValid || isSubmitting" class="submit-btn">
      {{ isSubmitting ? 'Submitting...' : 'Submit' }}
    </button>

    <div v-if="submitStatus === 'success'" class="success-message">
      Thank you for your message! We'll get back to you soon.
    </div>

    <div v-if="submitStatus === 'error'" class="error-message">
      Submission failed. Please try again later.
    </div>
  </form>
</template>

<script setup>
import { ref, reactive, computed } from 'vue'

// Form data
const form = reactive({
  name: '',
  email: '',
  category: '',
  message: '',
  agreeToTerms: false
})

// Error state
const errors = reactive({
  name: '',
  email: '',
  message: '',
  agreeToTerms: ''
})

// UI state
const isSubmitting = ref(false)
const submitStatus = ref('')

// Validation functions
const validators = {
  name: (value) => {
    if (!value) return 'Name is required'
    if (value.length < 2) return 'Name must be at least 2 characters'
    return ''
  },
  email: (value) => {
    if (!value) return 'Email is required'
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    if (!emailRegex.test(value)) return 'Please enter a valid email address'
    return ''
  },
  message: (value) => {
    if (!value) return 'Message is required'
    if (value.length < 10) return 'Message must be at least 10 characters'
    return ''
  },
  agreeToTerms: (value) => {
    if (!value) return 'You must agree to the terms and conditions'
    return ''
  }
}

// Field validation
function validateField(field) {
  if (validators[field]) {
    errors[field] = validators[field](form[field])
  }
}

// Form validation
function validateForm() {
  Object.keys(validators).forEach(field => {
    validateField(field)
  })
}

// Form validity check
const isFormValid = computed(() => {
  return Object.values(errors).every(error => !error) &&
         form.name && form.email && form.message && form.agreeToTerms
})

// Form submission
async function submitForm() {
  validateForm()
  
  if (!isFormValid.value) {
    return
  }

  isSubmitting.value = true
  submitStatus.value = ''

  try {
    // Submit data to API endpoint (mock)
    await new Promise(resolve => setTimeout(resolve, 2000))
    
    // Success handling
    submitStatus.value = 'success'
    
    // Reset form
    Object.keys(form).forEach(key => {
      form[key] = typeof form[key] === 'boolean' ? false : ''
    })
    Object.keys(errors).forEach(key => {
      errors[key] = ''
    })
    
  } catch (error) {
    console.error('Submission error:', error)
    submitStatus.value = 'error'
  } finally {
    isSubmitting.value = false
  }
}
</script>

<style scoped>
.contact-form {
  max-width: 600px;
  margin: 0 auto;
  padding: 2rem;
  background-color: #f7fafc;
  border-radius: 8px;
}

.form-group {
  margin-bottom: 1rem;
}

label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
  color: #2d3748;
}

input,
select,
textarea {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #d2d6dc;
  border-radius: 4px;
  font-size: 1rem;
}

input.error,
textarea.error {
  border-color: #f56565;
}

.checkbox-label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.checkbox-label input {
  width: auto;
}

.error-message {
  color: #f56565;
  font-size: 0.875rem;
  margin-top: 0.25rem;
  display: block;
}

.submit-btn {
  background-color: #4299e1;
  color: white;
  padding: 0.75rem 2rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  width: 100%;
}

.submit-btn:disabled {
  background-color: #a0aec0;
  cursor: not-allowed;
}

.success-message {
  background-color: #c6f6d5;
  color: #22543d;
  padding: 1rem;
  border-radius: 4px;
  margin-top: 1rem;
}
</style>

Lifecycle and Composables

<!-- Dashboard.vue -->
<template>
  <div class="dashboard">
    <h1>Dashboard</h1>
    
    <div class="stats-grid">
      <div class="stat-card">
        <h3>Total Users</h3>
        <p class="stat-number">{{ stats.totalUsers }}</p>
      </div>
      <div class="stat-card">
        <h3>Active Users</h3>
        <p class="stat-number">{{ stats.activeUsers }}</p>
      </div>
      <div class="stat-card">
        <h3>Today's Revenue</h3>
        <p class="stat-number">${{ stats.todayRevenue.toLocaleString() }}</p>
      </div>
    </div>

    <div class="current-time">
      <p>Current Time: {{ currentTime }}</p>
      <p>Online Time: {{ onlineTime }}</p>
    </div>

    <div class="window-size">
      <p>Window Size: {{ windowSize.width }} × {{ windowSize.height }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useStats, useCurrentTime, useWindowSize } from '@/composables'

// Using composables
const { stats, fetchStats } = useStats()
const { currentTime, onlineTime, startTimer, stopTimer } = useCurrentTime()
const { windowSize } = useWindowSize()

// Component-specific state
const refreshInterval = ref(null)

// Lifecycle hooks
onMounted(async () => {
  console.log('Dashboard mounted')
  
  // Fetch initial data
  await fetchStats()
  
  // Start timer
  startTimer()
  
  // Set up periodic data refresh (every 30 seconds)
  refreshInterval.value = setInterval(() => {
    fetchStats()
  }, 30000)
})

onUnmounted(() => {
  console.log('Dashboard unmounted')
  
  // Stop timer
  stopTimer()
  
  // Clear interval
  if (refreshInterval.value) {
    clearInterval(refreshInterval.value)
  }
})
</script>

<style scoped>
.dashboard {
  padding: 2rem;
  max-width: 1200px;
  margin: 0 auto;
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1rem;
  margin-bottom: 2rem;
}

.stat-card {
  background-color: white;
  padding: 1.5rem;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.stat-card h3 {
  margin: 0 0 0.5rem 0;
  color: #4a5568;
  font-size: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.stat-number {
  margin: 0;
  font-size: 2rem;
  font-weight: bold;
  color: #2d3748;
}

.current-time,
.window-size {
  background-color: #f7fafc;
  padding: 1rem;
  border-radius: 4px;
  margin-bottom: 1rem;
}
</style>
// composables/useStats.js
import { ref } from 'vue'

export function useStats() {
  const stats = ref({
    totalUsers: 0,
    activeUsers: 0,
    todayRevenue: 0
  })

  const isLoading = ref(false)
  const error = ref(null)

  async function fetchStats() {
    isLoading.value = true
    error.value = null
    
    try {
      // Fetch data from API (mock)
      await new Promise(resolve => setTimeout(resolve, 1000))
      
      stats.value = {
        totalUsers: Math.floor(Math.random() * 10000) + 1000,
        activeUsers: Math.floor(Math.random() * 1000) + 100,
        todayRevenue: Math.floor(Math.random() * 1000000) + 100000
      }
    } catch (err) {
      error.value = err.message
    } finally {
      isLoading.value = false
    }
  }

  return {
    stats,
    isLoading,
    error,
    fetchStats
  }
}

// composables/useCurrentTime.js
import { ref, onUnmounted } from 'vue'

export function useCurrentTime() {
  const currentTime = ref('')
  const onlineTime = ref('')
  const startTime = ref(null)
  let timeInterval = null

  function updateTime() {
    const now = new Date()
    currentTime.value = now.toLocaleString('en-US')
    
    if (startTime.value) {
      const diff = now - startTime.value
      const hours = Math.floor(diff / 3600000)
      const minutes = Math.floor((diff % 3600000) / 60000)
      const seconds = Math.floor((diff % 60000) / 1000)
      onlineTime.value = `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
    }
  }

  function startTimer() {
    startTime.value = new Date()
    updateTime()
    timeInterval = setInterval(updateTime, 1000)
  }

  function stopTimer() {
    if (timeInterval) {
      clearInterval(timeInterval)
      timeInterval = null
    }
  }

  onUnmounted(() => {
    stopTimer()
  })

  return {
    currentTime,
    onlineTime,
    startTimer,
    stopTimer
  }
}

// composables/useWindowSize.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useWindowSize() {
  const windowSize = ref({
    width: 0,
    height: 0
  })

  function updateSize() {
    windowSize.value.width = window.innerWidth
    windowSize.value.height = window.innerHeight
  }

  onMounted(() => {
    updateSize()
    window.addEventListener('resize', updateSize)
  })

  onUnmounted(() => {
    window.removeEventListener('resize', updateSize)
  })

  return {
    windowSize
  }
}

Vue Router Integration

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Dashboard from '@/views/Dashboard.vue'
import Users from '@/views/Users.vue'
import UserDetail from '@/views/UserDetail.vue'
import Contact from '@/views/Contact.vue'
import NotFound from '@/views/NotFound.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: { requiresAuth: true }
  },
  {
    path: '/users',
    name: 'Users',
    component: Users
  },
  {
    path: '/users/:id',
    name: 'UserDetail',
    component: UserDetail,
    props: true
  },
  {
    path: '/contact',
    name: 'Contact',
    component: Contact
  },
  {
    path: '/about',
    name: 'About',
    // Lazy loading
    component: () => import('@/views/About.vue')
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// Navigation guards
router.beforeEach((to, from, next) => {
  // Check authentication for protected pages
  if (to.meta.requiresAuth) {
    const isAuthenticated = localStorage.getItem('auth-token')
    if (!isAuthenticated) {
      next('/login')
      return
    }
  }
  next()
})

export default router

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')