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.json

Vue 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

PropertyTypeDescription
pathstringRelative path to .vue file
namestringComponent name
propsarrayArray of prop definitions
configobjectComponent-level configuration
targetTagsarrayReplace 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

  1. Create Vue components with TypeScript
  2. Run UIPro configuration: "Create UIPro config for all components"
  3. UIPro generates locofy.config.json
  4. Review configuration
  5. Open Figma and map components
  6. Generate code

Best Practices

  1. ✅ Use TypeScript for better type inference
  2. ✅ Use Composition API with <script setup> for cleaner code
  3. ✅ Use scoped styles to avoid conflicts
  4. ✅ Use valueMapping when Figma differs from code
  5. ✅ Document complex mappings

Next Steps