Developing Custom Interfaces in Directus
Interface Extension in Directus is a Vue 3 component displayed as a field in the admin panel. Used for non-standard input types: color picker, map, multi-stepper, custom editor. Data is stored in standard database columns.
Creating an Interface Extension
npx create-directus-extension@latest
# Type: interface
# Name: color-picker
cd directus-extension-interface-color-picker
npm install
npm run dev # development mode with hot reload
Color Picker Interface
<!-- src/interface.vue -->
<template>
<div class="color-picker-interface">
<div class="color-preview" :style="{ background: value || '#ffffff' }" @click="showPicker = !showPicker" />
<v-input
:model-value="value"
type="text"
:placeholder="placeholder || '#000000'"
@update:model-value="$emit('input', $event)"
/>
<div v-if="showPicker" class="color-grid">
<div
v-for="color in presetColors"
:key="color"
class="color-swatch"
:style="{ background: color }"
:class="{ active: value === color }"
@click="selectColor(color)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
value: string | null
placeholder?: string
presetColors?: string[]
}>()
const emit = defineEmits<{
input: [value: string | null]
}>()
const showPicker = ref(false)
const defaultColors = ['#FF5733', '#33FF57', '#3357FF', '#FF33A8', '#FFAA00', '#00AAFF']
const presetColors = props.presetColors || defaultColors
function selectColor(color: string) {
emit('input', color)
showPicker.value = false
}
</script>
<style scoped>
.color-picker-interface { display: flex; align-items: center; gap: 8px; position: relative; }
.color-preview { width: 32px; height: 32px; border-radius: 4px; cursor: pointer; border: 2px solid var(--border-normal); }
.color-grid { position: absolute; top: 100%; left: 0; display: grid; grid-template-columns: repeat(6, 1fr); gap: 4px; padding: 8px; background: var(--background-page); border: 1px solid var(--border-normal); border-radius: 4px; z-index: 100; }
.color-swatch { width: 24px; height: 24px; border-radius: 4px; cursor: pointer; }
.color-swatch.active { outline: 2px solid var(--primary); }
</style>
Interface Registration
// src/index.ts
import InterfaceComponent from './interface.vue'
export default {
id: 'color-picker',
name: 'Color Picker',
icon: 'palette',
description: 'Select color from palette',
component: InterfaceComponent,
types: ['string'], // field types it applies to
options: [
{
field: 'presetColors',
name: 'Preset Colors',
type: 'json',
meta: {
interface: 'tags',
options: { placeholder: '#FF5733' },
},
},
{
field: 'placeholder',
name: 'Placeholder',
type: 'string',
meta: { interface: 'input' },
},
],
}
Star Rating Interface
<!-- Star rating component -->
<template>
<div class="star-rating">
<span
v-for="star in maxStars"
:key="star"
class="star"
:class="{ filled: star <= (hoveredStar || value || 0) }"
@click="$emit('input', star)"
@mouseenter="hoveredStar = star"
@mouseleave="hoveredStar = null"
>★</span>
<span v-if="value" class="value-label">{{ value }}/{{ maxStars }}</span>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
value: number | null
maxStars?: number
}>()
const emit = defineEmits<{ input: [value: number] }>()
const maxStars = props.maxStars || 5
const hoveredStar = ref<number | null>(null)
</script>
Attaching Custom Interface to a Field
After building and restarting Directus, in the admin panel when creating a field, select type String, then Interface = Color Picker.
Or programmatically via API:
// Create field with custom interface
await directus.request(
createField('products', {
field: 'brand_color',
type: 'string',
meta: {
interface: 'color-picker',
options: { presetColors: ['#FF5733', '#3357FF'] },
},
})
)
Timeline
Development of one custom interface with options — 1–2 days.







