Parts
Every Vueform component renders named internal pieces — Parts — the <input> of VfText, an element's label, a list's remove button, the tab bar of a tabbed form. A Part override merges attributes onto one of them — or props, when the Part is itself a component — and leaves the rest of the rendering untouched, which makes it the lightest tool for a test ID, an ARIA tweak, an extra listener, or a button variant. To replace what a Part renders instead of adjusting it, reach for Slots and Templates.
Targeting a Part
The parts prop maps Part names to attribute objects:
{
email: {
type: 'text',
label: 'Email',
parts: {
input: { 'data-testid': 'email-input' },
},
},
}Each value merges with what the component renders on that Part:
classandstylecombine with the built-in ones.on*keys (onFocus,onKeydown) become listeners that run alongside the component's own handlers.- Every other key replaces the built-in value of the same name — the computed ARIA attributes included, which is exactly how they're overridden.
Part keys are typed per component — the parts object autocompletes the component's Part names, and each value is typed as its target tag's attribute set. Before any Part value reaches the DOM it is filtered — innerHTML, outerHTML, and prototype keys are always dropped, and anything else can be banned through banAttrs.
The part: Shorthand
part:<name> targets a single Part directly:
<template>
<VfText name="email" :part:input="{ 'data-testid': 'email-input' }" />
</template>Both forms mix freely, but for the same Part part:<name> replaces the parts entry wholesale — the two are never merged. And because part:<name> is a namespaced prop, Defaults and Presets can set it — a test ID on every VfText input, say — with the config, preset, and local values combining per Merging Behavior. A part:<name> coming from defaults or presets also outranks a local parts.<name>, so to adjust an inherited override locally, use part:<name>.
The Root Part
Every component has a root Part — its outermost element. For elements it's the layout wrapper (a <div> or <fieldset>, see Anatomy), for VfForm the <form> tag, and for the rest their own outer element.
root matters because Vueform components don't inherit attributes — class, style, data-*, and listeners written on an element's or the form's tag are dropped silently. part:root is how they reach the wrapper:
<template>
<VfText name="email" :part:root="{ 'data-testid': 'email-field' }" />
</template>The exception is VfButton — it forwards tag attributes onto its <button> (or <a>), and so do VfIcon and VfAddon. Everywhere else the wrapper keeps its own classes, visibility handling, and fieldset wiring, and your root value merges in by the same rules as any other Part.
Without a Layout
With layout: false the element renders no wrapper at all, so there is nothing to carry a root override. Setting one brings a wrapper back:
<template>
<VfText name="search" :layout="false" :part:root="{ 'data-testid': 'search-field' }" />
</template>The element renders a <div class="vf-passthrough"> carrying your attributes, styled display: contents so it stays invisible to layout.
Function Values
Any Part value can be a function returning the attribute object. It receives the component's Context and re-runs whenever the reactive state it reads changes:
{
email: {
type: 'text',
label: 'Email',
parts: {
input: (el$) => ({ 'data-dirty': el$.dirty ? '' : undefined }),
},
},
}Function values are code, so they are available in code-authored schemas but can't come from a JSON one.
Callable Parts
Parts rendered once per item take per-instance arguments — VfList's item receives its drag state, remove and sort the item's index, an element's addonBefore and addonAfter the addon and its index, VfTabs' wrapper the page it belongs to. A function value receives those arguments first, with the context appended last:
{
guests: {
type: 'list',
schema: { name: { type: 'text' } },
parts: {
remove: (index) => ({ 'data-testid': `remove-guest-${index}` }),
},
},
}A static object works on a callable Part too and applies to every instance. Each callable Part's arguments are spelled out by its signature in the component's Vf*Parts type, so autocomplete shows them as you type the function.
Component Parts
When a Part renders a Vueform component instead of a plain tag, its value is forwarded as that component's props — typed PartOf<'VfButton'> and the like, and listed on the reference page with the component as the Part's element. This covers the add, remove, and sort buttons of VfList and VfMatrix and the add and remove of VfFile, VfNav's back, next, and finish, addon Parts (VfAddon), icon Parts (VfIcon), and VfForm's tabs, steps, nav, elements, and languages:
{
tags: {
type: 'list',
schema: { tag: { type: 'text' } },
parts: {
add: { variant: 'outlined' },
},
},
}Because the forwarded props include the target's own parts, overrides nest as deep as the component tree goes — note that the key is the Part's name (tabs), not the component's:
<template>
<VfForm tabbed :parts="{ tabs: { parts: { tab: { 'data-testid': 'form-tab' } } } }">
<!-- pages -->
</VfForm>
</template>Anything else the target component accepts works the same way — classes, slots, and listeners included.
The Bindings Bag
For every Part the component merges its State Attributes and ARIA attributes and puts your override on top — the result is the Part's Bindings Bag, and your override always wins. Classes are never in the bag — they resolve separately, through the Class Override Chain.
The bag matters once you replace a Part's markup. A slot that replaces a Part receives the resolved bag as its bindings prop — per-item Parts included — and spreading it keeps the user's Part overrides, the state attributes, and the ARIA wiring alive in your markup:
<template>
<VfList name="guests" :schema="{ name: { type: 'text' } }">
<template #item="{ bindings }">
<div v-bind="bindings">
<!-- custom row markup -->
</div>
</template>
</VfList>
</template>Since the bag carries no classes, the replacement markup brings its own. Custom templates consume the same bags through the Context, calling one per Part — bindings.input(), bindings.item(…) — as Templates shows.
Finding a Component's Parts
Every component's reference page has a Parts tab listing each Part's tag or component and its classes, with a live viewer of the Part tree — see VfText, VfList, or VfForm. Adapters render their own structure, so Part names can differ between adapters. To introduce Parts in markup of your own, see Typing New Keys in templates and Components.