Skip to content

Vue ​

Learn how to use Galaxy UI with Vue 3.

Requirements ​

  • Vue: 3.3.0 or higher
  • TypeScript: 5.0.0 or higher (recommended)
  • Tailwind CSS: 3.4.0 or higher
  • Vite: 5.0.0 or higher (recommended)

Installation ​

Initialize Galaxy UI in your Vue project:

bash
npx @galaxy-stack/nebula-cli@latest init
bash
pnpm dlx @galaxy-stack/nebula-cli@latest init
bash
yarn dlx @galaxy-stack/nebula-cli@latest init
bash
bunx @galaxy-stack/nebula-cli@latest init

The CLI will:

  1. Detect that you're using Vue
  2. Install required dependencies (radix-vue, etc.)
  3. Create components.json
  4. Setup Tailwind CSS configuration
  5. Create utility functions

Project Setup ​

Create a new project with Vite:

bash
# Create project
npm create vite@latest my-app -- --template vue-ts

# Navigate to project
cd my-app

# Install dependencies
npm install

# Initialize Galaxy UI
npx @galaxy-stack/nebula-cli@latest init

Vite Configuration ​

Ensure your vite.config.ts has path aliases:

ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

TypeScript Configuration ​

Update tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src/**/*.ts", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

Adding Components ​

Add components to your project:

bash
npx @galaxy-stack/nebula-cli@latest add button

This will:

  1. Create src/components/ui/button/ directory
  2. Copy component files (Button.vue, variants.ts, index.ts)
  3. Install dependencies (radix-vue, class-variance-authority)
  4. Export Button component with props: variant (6 options), size (4 options), asChild, as, class, type, disabled

Usage ​

Basic Example ​

vue
<script setup lang="ts">
import { Button } from '@/components/ui/button'
</script>

<template>
  <div class="p-4">
    <Button>Click me</Button>
  </div>
</template>

With Variants ​

vue
<script setup lang="ts">
import { Button } from '@/components/ui/button'
</script>

<template>
  <div class="flex gap-2">
    <Button variant="default">Default</Button>
    <Button variant="secondary">Secondary</Button>
    <Button variant="destructive">Destructive</Button>
    <Button variant="outline">Outline</Button>
    <Button variant="ghost">Ghost</Button>
    <Button variant="link">Link</Button>
  </div>
</template>

With Events ​

vue
<script setup lang="ts">
import { Button } from '@/components/ui/button'

const handleClick = () => {
  console.log('Button clicked!')
}
</script>

<template>
  <Button @click="handleClick">
    Click me
  </Button>
</template>

Form Example ​

vue
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

const email = ref('')
const password = ref('')

const handleSubmit = () => {
  console.log({ email: email.value, password: password.value })
}
</script>

<template>
  <form @submit.prevent="handleSubmit" class="space-y-4 max-w-md">
    <div class="space-y-2">
      <Label for="email">Email</Label>
      <Input
        id="email"
        v-model="email"
        type="email"
        placeholder="Enter your email"
      />
    </div>

    <div class="space-y-2">
      <Label for="password">Password</Label>
      <Input
        id="password"
        v-model="password"
        type="password"
        placeholder="Enter your password"
      />
    </div>

    <Button type="submit" class="w-full">
      Sign In
    </Button>
  </form>
</template>

Dialog Example ​

vue
<script setup lang="ts">
import { ref } from 'vue'
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogFooter,
  DialogTitle,
  DialogDescription,
  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>
      <DialogHeader>
        <DialogTitle>Are you sure?</DialogTitle>
        <DialogDescription>
          This action cannot be undone. This will permanently delete your account.
        </DialogDescription>
      </DialogHeader>
      <DialogFooter>
        <Button variant="outline" @click="open = false">
          Cancel
        </Button>
        <Button variant="destructive" @click="open = false">
          Delete
        </Button>
      </DialogFooter>
    </DialogContent>
  </Dialog>
</template>

Composition API ​

Using Composables ​

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

const count = ref(0)
const doubled = computed(() => count.value * 2)

const increment = () => count.value++
</script>

<template>
  <div class="space-y-4">
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <Button @click="increment">Increment</Button>
  </div>
</template>

With Reactive ​

vue
<script setup lang="ts">
import { reactive } from 'vue'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'

const form = reactive({
  name: '',
  email: '',
})

const handleSubmit = () => {
  console.log(form)
}
</script>

<template>
  <form @submit.prevent="handleSubmit" class="space-y-4">
    <Input v-model="form.name" placeholder="Name" />
    <Input v-model="form.email" type="email" placeholder="Email" />
    <Button type="submit">Submit</Button>
  </form>
</template>

Vue Router Integration ​

With Vue Router v4 ​

vue
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import { Button } from '@/components/ui/button'
</script>

<template>
  <div>
    <nav class="flex gap-2 p-4">
      <Button as-child variant="ghost">
        <RouterLink to="/">Home</RouterLink>
      </Button>
      <Button as-child variant="ghost">
        <RouterLink to="/about">About</RouterLink>
      </Button>
    </nav>

    <RouterView />
  </div>
</template>

Programmatic Navigation ​

vue
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { Button } from '@/components/ui/button'

const router = useRouter()

const navigateToAbout = () => {
  router.push('/about')
}
</script>

<template>
  <Button @click="navigateToAbout">
    Go to About
  </Button>
</template>

State Management ​

With Pinia ​

ts
// stores/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    }
  }
})
vue
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
import { Button } from '@/components/ui/button'

const counter = useCounterStore()
</script>

<template>
  <div class="flex items-center gap-4">
    <Button @click="counter.decrement">-</Button>
    <span class="text-2xl font-bold">{{ counter.count }}</span>
    <Button @click="counter.increment">+</Button>
  </div>
</template>

TypeScript Support ​

All components are fully typed with TypeScript:

vue
<script setup lang="ts">
import { Button, type ButtonProps } from '@/components/ui/button'
import type { HTMLAttributes } from 'vue'

interface CustomButtonProps extends ButtonProps {
  loading?: boolean
}

defineProps<CustomButtonProps>()
</script>

<template>
  <Button :disabled="loading">
    {{ loading ? 'Loading...' : $slots.default }}
  </Button>
</template>

Best Practices ​

Component Composition ​

vue
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'

defineProps<{
  loading?: boolean
}>()
</script>

<template>
  <Button :disabled="loading">
    <Spinner v-if="loading" size="sm" class="mr-2" />
    <slot />
  </Button>
</template>

Using as-child ​

vue
<!-- ✅ Good: Use as-child for custom elements -->
<Button as-child>
  <a href="/link">Link Button</a>
</Button>

<!-- ❌ Bad: Nesting buttons -->
<Button>
  <a href="/link">Link Button</a>
</Button>

Handling Events ​

vue
<script setup lang="ts">
const handleClick = (event: MouseEvent) => {
  event.preventDefault()
  // Your logic
}
</script>

<template>
  <Button @click="handleClick">
    Click me
  </Button>
</template>

Script Setup ​

All examples use <script setup> syntax which is the recommended way in Vue 3:

vue
<script setup lang="ts">
// Direct imports and declarations
import { ref } from 'vue'
import { Button } from '@/components/ui/button'

// Reactive state
const count = ref(0)

// Functions
const increment = () => count.value++
</script>

Troubleshooting ​

Module not found errors ​

bash
# Clear cache and reinstall
rm -rf node_modules .vite
npm install

TypeScript errors ​

  1. Check tsconfig.json has correct paths configuration
  2. Restart Volar server in VS Code: Cmd+Shift+P → "Volar: Restart Vue Server"

Styles not applying ​

  1. Verify Tailwind CSS is configured correctly
  2. Check tailwind.config.js content paths include components
  3. Restart dev server

Radix Vue warnings ​

If you see warnings about Radix Vue primitives, ensure you have the latest version:

bash
npm install radix-vue@latest

Next Steps ​

Released under the MIT License.