StackForm
Customisation

Slot System

Overview of the three-layer slot customisation system

Every StackForm field exposes a slot system with three independent override layers. They compose independently — use one, two, or all three together.

The three layers

LayerWhat it controlsMerge rule
slotsWhich component renders each slotFirst non-null wins: field → provider → core default
slotPropsExtra props passed to a slot componentField-level replaces provider-level per slot key (shallow replace)
classNamesTailwind classes applied to each slot elementAll three stack: cn(core, provider, field)

Merge semantics

// slots — first non-null wins
resolvedSlot = fieldSlots?.Label ?? providerSlots?.Label ?? DefaultLabel

// slotProps — field fully replaces provider per key
resolvedInputProps = fieldSlotProps?.input ?? providerSlotProps?.input

// classNames — all three merge via clsx
resolvedLabelClass = cn(coreClass, providerClass, fieldClass)

Overriding at provider level

Set a slot override once on StackFormProvider to apply it to every field in the form.

import { StackFormProvider } from '@stackform/ui'
import type { LabelSlotProps } from '@stackform/ui'

function BoldLabel({ htmlFor, children, required }: LabelSlotProps) {
  return (
    <label htmlFor={htmlFor} className="font-bold text-sm">
      {children}
      {required ? <span aria-hidden="true"> *</span> : null}
    </label>
  )
}

<StackFormProvider slots={{ Label: BoldLabel }}>
  {/* Every field in here uses BoldLabel */}
  <TextField name="name" label="Name" />
  <TextField name="email" label="Email" />
</StackFormProvider>

Overriding at field level

Pass slots directly to a single field to override just that instance. Field-level always wins over provider-level.

function InlineLabel({ htmlFor, children }: LabelSlotProps) {
  return (
    <label htmlFor={htmlFor} className="inline-flex items-center gap-2 text-xs">
      {children}
    </label>
  )
}

<TextField
  name="email"
  label="Email"
  slots={{ Label: InlineLabel }}
/>

When to use each layer

  • slots — Replace the markup entirely. Use when className overrides are not enough.
  • slotProps — Pass extra HTML attributes (e.g. data-testid, autoComplete) without replacing the component.
  • classNames — Adjust appearance only. The simplest option when the default markup is correct.