Slots
A Slot replaces one piece of a component's rendering — a select's option text, a list's empty state, an add button. In templates they are ordinary Vue slots, and because every Vueform component also accepts a slots prop carrying the same content, a schema-driven form keeps the full customization surface.
Template Slots
Fill a Slot the way you would on any Vue component — a #slotName template whose scope carries the Slot's props:
<script setup lang="ts">
const countries = [
{ id: 'us', name: 'United States', flag: '🇺🇸' },
{ id: 'de', name: 'Germany', flag: '🇩🇪' },
]
</script>
<template>
<VfSelect name="country" label="Country" :items="countries" item-label="name">
<template #option="{ item }">
{{ item.$raw.idnameflagflag }} {{ item.label }}
</template>
</VfSelect>
</template>item is the resolved item — value, label, disabled, key, and $raw, the original object. The element's generics type $raw from your items and value from itemValue, so the shape of your own data completes and type-checks.
Each component's reference page lists its Slots and their props under the Slots tab — see VfSelect's for the Slot above. The layout Slots every element shares (label, help, errors, …) are covered in Element Layout. And the template Slot replaces a component's entire rendering — see Templates.
What a Slot Replaces
Some Slots fill a node's content and leave the node itself in place. VfSelect's option fills the native <option>, which is why its content can only be text — the browser ignores markup inside an option.
Others replace a whole Part, node included — VfList's add, remove, and placeholder, or VfRadios' item. These Slots receive the Part's resolved bindings bag (data-* state markers, ARIA attributes, parts overrides); spread it with v-bind on your root node so the replacement keeps its state styling hooks and accessibility wiring. The bag's anatomy lives in Parts.
The slots Prop
In a schema there is no template to write a #slotName block in, so the slots prop maps Slot names to content. Every entry renders as a component, and the Slot props arrive as its props:
import Option from './Option.vue'
{
country: {
type: 'select',
label: 'Country',
items: [
{ id: 'us', name: 'United States', flag: '🇺🇸' },
{ id: 'de', name: 'Germany', flag: '🇩🇪' },
],
itemLabel: 'name',
slots: { option: Option },
},
}Option.vue declares the Slot props it uses and renders them:
<script setup lang="ts">
import type { ResolvedItem } from '@vueform/ui'
defineProps<{ item: ResolvedItem }>()
</script>
<template>
{{ item.$raw?.flag }} {{ item.label }}
</template>Declare the props you use. Vueform sets inheritAttrs: false on object Slot components automatically, so undeclared Slot props never land on your root node as HTML attributes — declaring inheritAttrs: true yourself opts back into the fallthrough.
Any component definition works — an SFC, a defineComponent with setup or render, or a template string, which needs Vue's runtime-compiler build. A plain function works as a functional component, receiving the Slot props and returning the content:
slots: { option: ({ item }) => item.label.toUpperCase() }A few rules govern how entries resolve:
- Keys may be kebab-case or camelCase —
addonBeforelands on theaddon-beforeSlot. - Slot names autocomplete in typed schemas, but entry values are only checked as "a component or a string" — typing the content is what
defineSlotadds. - Components inside a reactive schema — a
ref(), a Pinia store — are unwrapped automatically, nomarkRawneeded. - When an inline template Slot and a
slotsentry fill the same Slot, the inline one wins.
String Content
A string entry needs no component. Plain text renders as text, with Expressions resolved and kept live — 'Ships to {country}' follows the Model as it changes. Resolution honors no-expressions and the slots key of $expression.whitelist — see Restriction Levels.
A string shaped like a component name — PascalCase or kebab-case — that names a globally registered component resolves to that component. Anything else renders as text. Setting $security.allowComponents restricts which names may resolve — once set it is an allowlist, and a registered but unlisted name renders as text with a once-per-name dev warning.
HTML renders by default — to restrict this or set up sanitization, see HTML Sanitization.
The gates cover strings only, because strings are the only entry shape a schema from outside the app can carry — component objects and functions come from your own code and are never gated. What else a schema can reach, banning the slots key outright with banAttrs, and running without the security module are covered in Security.
App-Wide Slots
slots is a regular prop, so Defaults keyed by component name apply a Slot to every instance, and Presets to a named subset:
createVueform({
defaults: {
VfSelect: { slots: { option: Option } },
},
})An instance's own slots merge with the default per Slot name — the instance entry wins for its key while other default entries stay. Setting mergeObjectProps: false in the config makes the instance object replace the default as a whole.
defineSlot
defineSlot is Vue's defineComponent that knows the Slot's props. The first argument names the Slot as 'Component.slotName' — 'VfSelect.option', 'VfText.addon-before' — the props array completes to that Slot's prop names, and setup / render receive them typed:
import { h } from 'vue'
import { defineSlot } from '@vueform/ui'
const Option = defineSlot('VfSelect.option', {
props: ['itemitem'],
setup(props) {
return () => h('strong', props.item.valueanylabelstringdisabledbooleankeynumber$rawanylabel)
},
})It always sets inheritAttrs: false and marks the component raw, so the result drops straight into a schema's slots.
There are no generics here. A standalone definition has no element instance to infer from, so prop values carry the Slot's widest types — item.value and item.$raw are any above, whereas a template Slot narrows them from your items. Prop names still complete.
defineSlot works for every Slot, template included — see Templates and the VfField default pattern in Components. Slot props are all a Slot component receives; to reach the host component's context from inside it, see Context. The full signature lives on the defineSlot API page.