Bindings & Classes
Themes and Tokens change the values behind Vueform's CSS; Bindings and Classes are what the markup itself carries. Bindings are the attributes components render by default — ARIA wiring plus data-* state flags your CSS can target — while the class layer changes which classes render at all: Tailwind utilities on an input, or your own names in place of the Theme's. Both target the same named spots of a component's markup — its Parts — and every class resolves through one fixed chain of defaults and overrides, so it can be set for a single instance, a group of instances, or the whole app.
Parts
Every Vueform component renders a tree of named Parts — the label, the input, the error list — each a stable spot in the markup regardless of Theme or Template. Select one below to see the element it renders, its default classes, and its bindings:
Bindings and Classes both attach to a Part by its name — they're the Bindings and Classes rows of the panel above — and the names are typed per component, so a VfText accepts only VfText's own Parts and a typo surfaces as a type error. They aren't all a Part name addresses — Parts covers the rest — and each component's full Part tree is on its reference page under Parts, VfText's for example.
Bindings
Every Part carries a typed bag of default attributes — its Bindings. They hold the Part's ARIA wiring (aria-invalid, aria-describedby, the id/for associations) and its data-* state flags, and the built-in templates bind them for you. What lands on the markup follows the component's live state — VfText's input carries aria-invalid and data-invalid while the element has an error, and drops both the moment it validates.
Bindings are defaults, not a fixed surface — the parts prop overrides or extends any Part's attributes, as Parts covers. The ARIA side, and why it stays separate from the styling attributes, belongs to ARIA vs. Styling Attributes.
State Attributes
The data-* members reflect component state for styling — data-invalid on an errored input, data-selected on the active tab wrapper, data-dragging on a list row being dragged, data-variant="outlined" on a button. Boolean states are presence attributes (data-loading="" while loading, absent otherwise) and enumerated ones carry their value (data-align="right"), so CSS reaches them with attribute selectors — the built-in stylesheets style states exactly this way:
The state changes, the attribute follows, and no class ever has to change. Each component's state attributes are typed on its Vf*Bindings interface (VfTextBindings, VfListBindings) — which plugins extend by augmentation — and listed with its Parts on the reference page.
Adding Classes
The classes prop takes an object keyed by Part, and each value is merged with the classes the Part already has:
<template>
<VfForm>
<VfText
name="email"
label="Email"
:classes="{
input: 'font-mono tracking-wide',
label: 'font-semibold',
}"
/>
</VfForm>
</template>The input now renders vf-input vf-text-input font-mono tracking-wide — the defaults stay, the additions land after them.
Each element has one flat Part map. Its own Parts (VfText's input) sit next to the shared Parts every element gets from the Element Layout — root, label, elementGroup, errors, and the rest — and both kinds go in the same classes object, as the example above already does. root is a shared Part the element also defines for itself, so its own value (vf-text) merges with the layout's (vf-element plus the spacing classes).
A component rendered inside another one — the VfTabs of a tabbed form — takes its classes through the host's parts prop, which forwards to nested components (see Component Parts).
Class Values
A class value is anything Vue's :class accepts — a string, an array, a conditional object, or a mix of them. Refs are accepted anywhere inside the value — as the whole value, an array member, or a condition in a conditional object — and stay reactive, which matters in a schema, where there's no template to unwrap them:
const highlighted = ref(false)
const schema = {
email: {
type: 'text',
label: 'Email',
classes: {
input: ['font-mono', { 'ring-2': highlighted }],
},
},
}Toggling highlighted adds and removes ring-2 on the input without touching the schema.
Replacing Classes
When the default classes are in the way rather than a starting point, reclass replaces the Part's base classes instead of merging with them:
email: {
type: 'text',
label: 'Email',
reclass: { input: 'ticket-input' },
},Only the base is replaced — classes for the same Part still merges on top, so a reclass of ticket-input plus a classes of extra renders ticket-input extra. To remove a Part's classes entirely, replace them with an empty array (reclass: { input: [] }).
Shorthands
Both props have a single-Part attribute form, typed per component so only real Part names are accepted:
<template>
<VfText name="email" label="Email" class:input="font-mono" />
<VfText name="code" label="Code" reclass:input="code-input" />
</template>class: merges after the classes prop. reclass: only applies when no reclass object — local, from Defaults, or from Presets — sets that Part.
The plain Vue class prop works on Vueform components too, merging into the root Parts (every Part whose name starts with root). Unlike classes, it doesn't merge across defaults, presets, and local values — the closest one wins, as Override Priority details.
With layout: false the element renders without its wrapper, so there's no node for root classes to land on — see Removing the Layout.
Contextual Classes
Any class value can be a function of the component's Context, re-resolved as the state it reads changes:
email: {
type: 'text',
label: 'Email',
classes: {
input: (el$) => (el$.hasError ? 'border-red-500' : 'border-slate-300'),
},
},Contextual functions work the same in classes, reclass, the shorthands, and the global reclass config.
For state-dependent styling, check the component's state attributes first — plain CSS on a data-* flag usually beats a contextual class. Reach for a function when the class itself must change, as with utility frameworks.
Callable Parts
A few Parts' class values are functions the template calls with per-render arguments:
| Part | Arguments |
|---|---|
root (every element) | The element's resolved props, which drive the spacing and divider classes. |
labelGroup | The element's heading prop. |
addonBefore / addonAfter | The addon and its index. |
cell (VfGrid) | { colHeader, rowHeader, schema } of the cell. |
Overriding such a Part takes either a static value — merged into every call's result through classes, or replacing it through reclass — or a function with the same arguments, which receives the Context appended as the last one:
addonBefore: ['$', '€'],
classes: {
addonBefore: (addon, index, el$) => (index === 0 ? 'rounded-l-md' : null),
},The trap to know about is root — it's callable on every element, so a function there receives the element's props first and the Context only second, unlike on non-callable Parts where the Context is the first and only argument.
Global Classes
The reclass config option replaces classes app-wide, keyed by component name, '*element' (every element), or '*' (every component):
createVueform({
reclass: {
VfText: { input: 'ticket-input' },
'*element': { label: 'ticket-label' },
'*': { root: 'ticket-root' },
},
})Per Part, the most specific target wins — a VfText entry beats '*element', which beats '*' — and entries don't combine.
Adding classes app-wide or per group is classes set through Defaults or Presets. A reclass coming from Defaults or Presets resolves like the local prop, so it also beats the global reclass config.
Layout Classes
The layoutClasses config option swaps the Element Layout's shared classes app-wide, per Part, at the very bottom of the chain:
createVueform({
layoutClasses: {
label: 'ticket-label',
errors: 'ticket-errors',
},
})The difference from reclass['*element'] is scope. layoutClasses replaces only the layout's contribution — an element's own root value (vf-text) still merges in, and a static value on a callable Part stays callable — while reclass replaces the Part's entire base value. The option reference is at Configuration.
Override Priority
The chain below governs Classes only — Bindings resolve separately, with the parts prop as their override surface. Per Part, classes resolve from the bottom up:
- Base — the layout's shared classes (after
layoutClasses) merged with the component's own; a custom template'suseClassesreplaces the component's own value per Part (see Templates). - Global
reclassconfig — replaces the base. reclassprop — Defaults, Presets, and local values resolve into one object (local beating Presets beating Defaults per Part) that replaces again; thereclass:shorthand fills in only for Parts none of them set.classesprop — merged on top; Defaults, Presets, and local values all merge rather than override (see Mergingclassandclasses).class:shorthand — merged afterclasses.classprop — resolves closer-wins like any plain prop (a localclassbeats Presets, which beat Defaults, and a component-specific Default beats'*'), and only that single winning value merges into the root Parts at render.
The resolved map is readable on any Context as classes — form$.find('email')?.classes.input — which is also what custom Templates bind.
Class Helpers
With classHelpers on, every non-empty class value gets a marker prepended — [__VfText.input__] — tracing any class in the DOM back to its component and Part right in the element inspector. The markers are inactive whenever NODE_ENV is production, so leaving the option on is safe. classHelper customizes the marker and receives the Component.part path:
createVueform({
classHelpers: true,
classHelper: (key) => `[${key}]`, // default: (key) => `[__${key}__]`
})Both options are documented in Configuration.