vue-shadcn-frontend

star 1

Build production-grade Vue 3 applications with shadcn-vue components. Use when the user asks to create Vue.js applications, components, pages, or dashboards using shadcn-vue, Radix Vue, or when building modern Vue frontends with Composition API, TypeScript, and Tailwind CSS. Covers project setup, component patterns, state management with Pinia, routing with Vue Router, form handling with VeeValidate + Zod, and data fetching with TanStack Query.

iqbaladinur By iqbaladinur schedule Updated 1/21/2026

name: vue-shadcn-frontend description: Build production-grade Vue 3 applications with shadcn-vue components. Use when the user asks to create Vue.js applications, components, pages, or dashboards using shadcn-vue, Radix Vue, or when building modern Vue frontends with Composition API, TypeScript, and Tailwind CSS. Covers project setup, component patterns, state management with Pinia, routing with Vue Router, form handling with VeeValidate + Zod, and data fetching with TanStack Query.

Vue.js + shadcn-vue Frontend Skill

Build modern, accessible Vue 3 applications using shadcn-vue components, Composition API, and TypeScript.

Tech Stack

  • Vue 3 with Composition API and <script setup>
  • TypeScript for type safety
  • shadcn-vue for UI components (built on Radix Vue)
  • Tailwind CSS for styling
  • Pinia for state management
  • Vue Router for navigation
  • VeeValidate + Zod for form validation
  • TanStack Query (Vue Query) for data fetching

Project Setup

# Create new project
npm create vite@latest my-app -- --template vue-ts
cd my-app
npm install

# Add Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# Add shadcn-vue
npx shadcn-vue@latest init

# Add components as needed
npx shadcn-vue@latest add button card input form dialog

Core Patterns

Component Structure

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { Button } from '@/components/ui/button'
import type { ComponentProps } from '@/types'

// Props with defaults
const props = withDefaults(defineProps<{
  title: string
  disabled?: boolean
}>(), {
  disabled: false
})

// Emits with typed payload
const emit = defineEmits<{
  submit: [data: FormData]
  cancel: []
}>()

// Reactive state
const isLoading = ref(false)

// Computed
const buttonText = computed(() => 
  isLoading.value ? 'Loading...' : 'Submit'
)

// Methods
const handleSubmit = async () => {
  isLoading.value = true
  // ...
  emit('submit', formData)
}
</script>

<template>
  <div class="space-y-4">
    <h2 class="text-2xl font-bold">{{ title }}</h2>
    <Button :disabled="disabled || isLoading" @click="handleSubmit">
      {{ buttonText }}
    </Button>
  </div>
</template>

Composables Pattern

// composables/useAsync.ts
import { ref, type Ref } from 'vue'

export function useAsync<T>(asyncFn: () => Promise<T>) {
  const data: Ref<T | null> = ref(null)
  const error: Ref<Error | null> = ref(null)
  const loading = ref(false)

  const execute = async () => {
    loading.value = true
    error.value = null
    try {
      data.value = await asyncFn()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  return { data, error, loading, execute }
}

shadcn-vue Components

Form with Validation

<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form'

const schema = toTypedSchema(z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Min 8 characters'),
}))

const { handleSubmit, isSubmitting } = useForm({
  validationSchema: schema,
})

const onSubmit = handleSubmit(async (values) => {
  console.log(values)
})
</script>

<template>
  <form @submit="onSubmit" class="space-y-4">
    <FormField v-slot="{ componentField }" name="email">
      <FormItem>
        <FormLabel>Email</FormLabel>
        <FormControl>
          <Input type="email" placeholder="you@example.com" v-bind="componentField" />
        </FormControl>
        <FormMessage />
      </FormItem>
    </FormField>

    <FormField v-slot="{ componentField }" name="password">
      <FormItem>
        <FormLabel>Password</FormLabel>
        <FormControl>
          <Input type="password" v-bind="componentField" />
        </FormControl>
        <FormMessage />
      </FormItem>
    </FormField>

    <Button type="submit" :disabled="isSubmitting">
      {{ isSubmitting ? 'Submitting...' : 'Submit' }}
    </Button>
  </form>
</template>

Dialog/Modal

<script setup lang="ts">
import { ref } from 'vue'
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'

const open = ref(false)
</script>

<template>
  <Dialog v-model:open="open">
    <DialogTrigger as-child>
      <Button>Open Dialog</Button>
    </DialogTrigger>
    <DialogContent class="sm:max-w-md">
      <DialogHeader>
        <DialogTitle>Confirm Action</DialogTitle>
        <DialogDescription>
          Are you sure you want to proceed?
        </DialogDescription>
      </DialogHeader>
      <DialogFooter>
        <Button variant="outline" @click="open = false">Cancel</Button>
        <Button @click="handleConfirm">Confirm</Button>
      </DialogFooter>
    </DialogContent>
  </Dialog>
</template>

Data Table

<script setup lang="ts">
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table'

interface User {
  id: string
  name: string
  email: string
  role: string
}

defineProps<{ users: User[] }>()
</script>

<template>
  <Table>
    <TableHeader>
      <TableRow>
        <TableHead>Name</TableHead>
        <TableHead>Email</TableHead>
        <TableHead>Role</TableHead>
      </TableRow>
    </TableHeader>
    <TableBody>
      <TableRow v-for="user in users" :key="user.id">
        <TableCell class="font-medium">{{ user.name }}</TableCell>
        <TableCell>{{ user.email }}</TableCell>
        <TableCell>{{ user.role }}</TableCell>
      </TableRow>
    </TableBody>
  </Table>
</template>

State Management with Pinia

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)

  const isAuthenticated = computed(() => !!token.value)
  const displayName = computed(() => user.value?.name ?? 'Guest')

  async function login(credentials: Credentials) {
    const response = await api.login(credentials)
    user.value = response.user
    token.value = response.token
  }

  function logout() {
    user.value = null
    token.value = null
  }

  return { user, token, isAuthenticated, displayName, login, logout }
}, {
  persist: true // with pinia-plugin-persistedstate
})

Data Fetching with TanStack Query

<script setup lang="ts">
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'

const queryClient = useQueryClient()

// Fetch data
const { data: users, isLoading, error } = useQuery({
  queryKey: ['users'],
  queryFn: () => fetch('/api/users').then(r => r.json()),
})

// Mutation with optimistic updates
const { mutate: createUser } = useMutation({
  mutationFn: (newUser: CreateUserDto) => 
    fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(newUser),
    }).then(r => r.json()),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['users'] })
  },
})
</script>

<template>
  <div v-if="isLoading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <UserList v-else :users="users" />
</template>

Routing with Vue Router

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: () => import('@/pages/Home.vue') },
    { 
      path: '/dashboard',
      component: () => import('@/pages/Dashboard.vue'),
      meta: { requiresAuth: true }
    },
    { path: '/login', component: () => import('@/pages/Login.vue') },
  ]
})

// Navigation guard
router.beforeEach((to) => {
  const userStore = useUserStore()
  if (to.meta.requiresAuth && !userStore.isAuthenticated) {
    return '/login'
  }
})

export default router

Additional References

Install via CLI
npx skills add https://github.com/iqbaladinur/agent-skills --skill vue-shadcn-frontend
Repository Details
star Stars 1
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator