Custom Components for Vue
This guide explains how to configure custom Vue components in Locofy using the locofy.config.json file. This allows you to map your existing Vue components to Figma designs, enabling UIPro to generate code using your actual component library.
Configuration File: locofy.config.json
The locofy.config.json file defines how your Vue components map to Figma components. It's automatically generated by UIPro when you run: "Create UIPro config for all components"
File Location
your-project/
├── src/
│ └── components/
├── locofy.config.json ← Configuration file
└── package.jsonVue Component Examples
Example 1: Button Component (Composition API)
Component Code:
<script setup lang="ts">
interface ButtonProps {
label: string;
size?: 'small' | 'medium' | 'large';
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
const props = withDefaults(defineProps<ButtonProps>(), {
size: 'medium',
variant: 'primary',
disabled: false
});
const emit = defineEmits<{
click: []
}>();
</script>
<template>
<button
:class="['btn', `btn-${size}`, `btn-${variant}`]"
:disabled="disabled"
@click="emit('click')"
>
{{ label }}
</button>
</template>
<style scoped>
.btn {
border-radius: 8px;
font-weight: 600;
cursor: pointer;
border: none;
}
.btn-small {
padding: 6px 12px;
font-size: 14px;
}
.btn-medium {
padding: 10px 16px;
font-size: 16px;
}
.btn-large {
padding: 14px 24px;
font-size: 18px;
}
.btn-primary {
background-color: #007AFF;
color: white;
}
.btn-secondary {
background-color: #5856D6;
color: white;
}
</style>locofy.config.json Configuration:
{
"components": [
{
"path": "./src/components/Button.vue",
"name": "Button",
"props": [
{
"name": "label",
"dataType": "string",
"propType": 1,
"isOptional": false,
"config": {
"layerName": "[children]"
}
},
{
"name": "size",
"dataType": "enum",
"propType": 1,
"isOptional": true,
"expectValues": ["small", "medium", "large"],
"config": {
"layerProp": "Size"
}
},
{
"name": "variant",
"dataType": "enum",
"propType": 1,
"isOptional": true,
"expectValues": ["primary", "secondary"],
"config": {
"layerProp": "Variant"
}
},
{
"name": "disabled",
"dataType": "boolean",
"propType": 4,
"isOptional": true
}
],
"config": {
"layerName": "Button"
}
}
],
"projectId": "your-project-id",
"projectName": "Your Vue App"
}Example 2: Card Component
Component Code:
<script setup lang="ts">
interface CardProps {
showHeader?: boolean;
showFooter?: boolean;
className?: string;
}
const props = withDefaults(defineProps<CardProps>(), {
showHeader: true,
showFooter: true,
className: ''
});
</script>
<template>
<div :class="['card', className]">
<div v-if="showHeader" class="card-header">
<slot name="header"></slot>
</div>
<div class="card-content">
<slot></slot>
</div>
<div v-if="showFooter" class="card-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<style scoped>
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
padding: 16px;
border-bottom: 1px solid #E5E5EA;
}
.card-content {
padding: 16px;
}
.card-footer {
padding: 16px;
border-top: 1px solid #E5E5EA;
}
</style>locofy.config.json Configuration:
{
"components": [
{
"path": "./src/components/Card.vue",
"name": "Card",
"props": [
{
"name": "showHeader",
"dataType": "boolean",
"propType": 4,
"isOptional": true,
"config": {
"layerProp": "Header"
}
},
{
"name": "showFooter",
"dataType": "boolean",
"propType": 4,
"isOptional": true,
"config": {
"layerProp": "Footer"
}
},
{
"name": "className",
"dataType": "string",
"propType": 1,
"isOptional": true
}
],
"config": {
"layerName": "Card"
}
}
]
}Example 3: Input Component
Component Code:
<script setup lang="ts">
interface InputProps {
modelValue?: string;
type?: 'text' | 'email' | 'password' | 'number';
placeholder?: string;
label?: string;
error?: string;
disabled?: boolean;
size?: 'sm' | 'md' | 'lg';
}
const props = withDefaults(defineProps<InputProps>(), {
modelValue: '',
type: 'text',
placeholder: '',
disabled: false,
size: 'md'
});
const emit = defineEmits<{
'update:modelValue': [value: string]
}>();
</script>
<template>
<div class="input-container">
<label v-if="label" class="input-label">{{ label }}</label>
<input
:type="type"
:value="modelValue"
:placeholder="placeholder"
:disabled="disabled"
:class="['input', `input-${size}`, { 'input-error': error }]"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
<span v-if="error" class="error-text">{{ error }}</span>
</div>
</template>
<style scoped>
.input-container {
margin-bottom: 16px;
}
.input-label {
display: block;
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
}
.input {
width: 100%;
background-color: #F2F2F7;
border-radius: 8px;
border: 1px solid transparent;
}
.input-sm {
padding: 8px 12px;
font-size: 14px;
}
.input-md {
padding: 12px 16px;
font-size: 16px;
}
.input-lg {
padding: 16px 20px;
font-size: 18px;
}
.input-error {
border-color: #FF3B30;
}
.error-text {
font-size: 12px;
color: #FF3B30;
margin-top: 4px;
}
</style>locofy.config.json Configuration:
{
"components": [
{
"path": "./src/components/Input.vue",
"name": "Input",
"props": [
{
"name": "type",
"dataType": "enum",
"propType": 1,
"isOptional": true,
"expectValues": ["text", "email", "password", "number"]
},
{
"name": "placeholder",
"dataType": "string",
"propType": 1,
"isOptional": true
},
{
"name": "label",
"dataType": "string",
"propType": 1,
"isOptional": true,
"config": {
"layerName": "Label"
}
},
{
"name": "error",
"dataType": "string",
"propType": 1,
"isOptional": true
},
{
"name": "disabled",
"dataType": "boolean",
"propType": 4,
"isOptional": true
},
{
"name": "size",
"dataType": "enum",
"propType": 1,
"isOptional": true,
"expectValues": ["sm", "md", "lg"],
"config": {
"layerProp": "Size",
"valueMapping": {
"sm": ["small"],
"md": ["medium"],
"lg": ["large"]
}
}
}
],
"config": {
"layerName": "Input"
}
}
]
}Example 4: Avatar Component
Component Code:
<script setup lang="ts">
interface AvatarProps {
src: string;
alt?: string;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
shape?: 'circle' | 'square';
showBadge?: boolean;
}
const props = withDefaults(defineProps<AvatarProps>(), {
alt: 'Avatar',
size: 'md',
shape: 'circle',
showBadge: false
});
const sizeMap = {
xs: '24px',
sm: '32px',
md: '48px',
lg: '64px',
xl: '96px'
};
</script>
<template>
<div class="avatar-container">
<img
:src="src"
:alt="alt"
:class="['avatar', `avatar-${shape}`]"
:style="{ width: sizeMap[size], height: sizeMap[size] }"
/>
<span v-if="showBadge" class="badge"></span>
</div>
</template>
<style scoped>
.avatar-container {
position: relative;
display: inline-block;
}
.avatar {
object-fit: cover;
background-color: #E5E5EA;
}
.avatar-circle {
border-radius: 50%;
}
.avatar-square {
border-radius: 8px;
}
.badge {
position: absolute;
bottom: 0;
right: 0;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #34C759;
border: 2px solid white;
}
</style>locofy.config.json Configuration:
{
"components": [
{
"path": "./src/components/Avatar.vue",
"name": "Avatar",
"props": [
{
"name": "src",
"dataType": "string",
"propType": 1,
"isOptional": false,
"attr": "src"
},
{
"name": "alt",
"dataType": "string",
"propType": 1,
"isOptional": true,
"attr": "alt"
},
{
"name": "size",
"dataType": "enum",
"propType": 1,
"isOptional": true,
"expectValues": ["xs", "sm", "md", "lg", "xl"],
"config": {
"layerProp": "Size"
}
},
{
"name": "shape",
"dataType": "enum",
"propType": 1,
"isOptional": true,
"expectValues": ["circle", "square"],
"config": {
"layerProp": "Shape"
}
},
{
"name": "showBadge",
"dataType": "boolean",
"propType": 4,
"isOptional": true
}
],
"config": {
"layerName": "Avatar"
}
}
]
}Example 5: Icon Button (Options API)
Component Code:
<script>
export default {
name: 'IconButton',
props: {
icon: {
type: String,
required: true
},
label: {
type: String,
required: true
},
size: {
type: String,
default: 'medium',
validator: (value) => ['small', 'medium', 'large'].includes(value)
}
},
emits: ['click']
}
</script>
<template>
<button :class="['icon-btn', `icon-btn-${size}`]" @click="$emit('click')">
<span class="icon-btn-icon">{{ icon }}</span>
<span class="icon-btn-label">{{ label }}</span>
</button>
</template>
<style scoped>
.icon-btn {
display: inline-flex;
align-items: center;
gap: 8px;
border: none;
border-radius: 8px;
background-color: #007AFF;
color: white;
cursor: pointer;
}
.icon-btn-small {
padding: 6px 12px;
font-size: 14px;
}
.icon-btn-medium {
padding: 10px 16px;
font-size: 16px;
}
.icon-btn-large {
padding: 14px 24px;
font-size: 18px;
}
</style>locofy.config.json Configuration:
{
"components": [
{
"path": "./src/components/IconButton.vue",
"name": "IconButton",
"props": [
{
"name": "icon",
"dataType": "string",
"propType": 1,
"isOptional": false,
"config": {
"nodeField": "name"
}
},
{
"name": "label",
"dataType": "string",
"propType": 1,
"isOptional": false,
"config": {
"layerName": "[label]"
}
},
{
"name": "size",
"dataType": "enum",
"propType": 1,
"isOptional": true,
"expectValues": ["small", "medium", "large"]
}
],
"config": {
"layerName": "IconButton"
}
}
]
}Complete locofy.config.json Example
{
"components": [
{
"path": "./src/components/Button.vue",
"name": "Button",
"props": [
{
"name": "label",
"dataType": "string",
"propType": 1,
"isOptional": false
},
{
"name": "size",
"dataType": "enum",
"propType": 1,
"isOptional": true,
"expectValues": ["small", "medium", "large"],
"config": {
"layerProp": "Size"
}
},
{
"name": "variant",
"dataType": "enum",
"propType": 1,
"isOptional": true,
"expectValues": ["primary", "secondary"],
"config": {
"layerProp": "Variant"
}
}
],
"config": {
"layerName": "Button"
}
},
{
"path": "./src/components/Card.vue",
"name": "Card",
"props": [
{
"name": "showHeader",
"dataType": "boolean",
"propType": 4,
"isOptional": true
},
{
"name": "showFooter",
"dataType": "boolean",
"propType": 4,
"isOptional": true
}
],
"config": {
"layerName": "Card"
}
},
{
"path": "./src/components/Input.vue",
"name": "Input",
"props": [
{
"name": "placeholder",
"dataType": "string",
"propType": 1,
"isOptional": true
},
{
"name": "label",
"dataType": "string",
"propType": 1,
"isOptional": true
},
{
"name": "size",
"dataType": "enum",
"propType": 1,
"isOptional": true,
"expectValues": ["sm", "md", "lg"],
"config": {
"layerProp": "Size",
"valueMapping": {
"sm": ["small"],
"md": ["medium"],
"lg": ["large"]
}
}
}
],
"config": {
"layerName": "Input"
}
}
],
"projectId": "abc123xyz",
"projectName": "My Vue App"
}Configuration Properties Reference
| Property | Type | Description |
|---|---|---|
path | string | Relative path to .vue file |
name | string | Component name |
props | array | Array of prop definitions |
config | object | Component-level configuration |
targetTags | array | Replace native HTML tags |
Vue-Specific Considerations
1. Slots
Map Vue slots as node-type props:
{
"name": "header",
"dataType": "node",
"propType": 5,
"config": {
"layerName": "Header"
}
}2. v-model
The modelValue prop is handled by Vue's v-model and typically doesn't need mapping.
3. Composition API vs Options API
Both are supported. UIPro detects the component structure automatically.
Advanced Features
Target Tags
{
"components": [
{
"path": "./src/components/Container.vue",
"name": "Container",
"targetTags": ["div", "section"],
"props": []
}
]
}Wrapper Component
{
"components": [
{
"path": "./src/components/Input.vue",
"name": "Input",
"wrapperComponent": {
"component": "FormField",
"props": [
{
"name": "label",
"config": {
"layerName": "Label"
}
}
]
},
"props": []
}
]
}Workflow
- Create Vue components with TypeScript
- Run UIPro configuration:
"Create UIPro config for all components" - UIPro generates
locofy.config.json - Review configuration
- Open Figma and map components
- Generate code
Best Practices
- ✅ Use TypeScript for better type inference
- ✅ Use Composition API with
<script setup>for cleaner code - ✅ Use scoped styles to avoid conflicts
- ✅ Use
valueMappingwhen Figma differs from code - ✅ Document complex mappings
Next Steps
- Check the main Custom Components guide
- Explore other guides: React, Angular, React Native
- Join our Discord community (opens in a new tab)