Usage
Wrap any form component with a FormField. Used in a Form, it provides validation and error handling.
Label
Use the label prop to set the label for the form control.
<template>
<B24FormField label="Email">
<B24Input placeholder="Enter your email" />
</B24FormField>
</template>
for attribute and the form control are associated with a unique id if not provided.When using the required prop, an asterisk is added next to the label.
<template>
<B24FormField label="Email" required>
<B24Input placeholder="Enter your email" />
</B24FormField>
</template>
Label slot
Use the #label slot when the label needs more than a string — a badge, a
counter, a short piece of state. The slot replaces the label's content, not
the <label> element around it, so whatever association the field already had
is unchanged.
Work makes a screen reader announce "Email Work". Mark decorative
content aria-hidden="true", as below. Avoid interactive content — a link or
button inside a <label> both activates itself and toggles the control.<template>
<B24FormField name="email">
<template #label>
Email
<B24Badge label="Work" size="xs" aria-hidden="true" />
</template>
<B24Input placeholder="Enter your email" />
</B24FormField>
</template>
The slot receives the label prop, so a wrapper can decorate the string
instead of replacing it — useful when the text itself comes from a schema or a
translation:
<B24FormField label="Email" name="email">
<template #label="{ label }">
{{ label }}
<B24Badge label="Work" size="xs" aria-hidden="true" />
</template>
<B24Input placeholder="Enter your email" />
</B24FormField>
hint, description, help and error take a slot the same way, and each
receives the prop it replaces — so setting the prop alongside the slot keeps
the text in one place. What is announced is what was
drawn covers what each block contributes
to the control's accessible description. The required asterisk is drawn on
the <label> element, so it survives a custom #label slot.
#error goes further than the others: the error block renders whenever an
#error slot exists, with or without an actual error, and takes help with
it. Error and help slots has the working pattern.Description
Use the description prop to provide additional information below the label.
We'll never share your email with anyone else.
<template>
<B24FormField label="Email" description="We'll never share your email with anyone else.">
<B24Input placeholder="Enter your email" class="w-full" />
</B24FormField>
</template>
Description slot
Use the #description slot when the description needs markup — a link to the
policy the field refers to, a piece of emphasis, an inline code sample. It
replaces the content of the same <p> the prop fills.
The slot on its own is enough to be announced — aria-describedby names the
blocks that were actually drawn, whichever of the two drew them. Setting the
prop as well is still worth it: the slot receives it, so decorating the value
costs one interpolation instead of restating it. See What is announced is what
was drawn.
We'll never share it. How we use your data
<template>
<B24FormField name="email" label="Email" description="We'll never share it.">
<template #description>
We'll never share it.
<a href="/docs/components/form/" class="underline">How we use your data</a>
</template>
<B24Input placeholder="Enter your email" class="w-full" />
</B24FormField>
</template>
A link is safe here in a way it is not in #label: the description sits
outside the <label> element, so clicking it does not toggle the control.
The slot receives the description prop, so the wrapper decorates the string
rather than restating it — which matters when the text comes from a schema or
a translation, and is what keeps the prop and the visible text in step:
<B24FormField label="Email" name="email" description="We'll never share it.">
<template #description="{ description }">
{{ description }}
<B24Link to="/docs/components/form/" raw class="underline">
How we use your data
</B24Link>
</template>
<B24Input placeholder="Enter your email" class="w-full" />
</B24FormField>
Hint
Use the hint prop to display a hint message next to the label.
<template>
<B24FormField label="Email" hint="Optional">
<B24Input placeholder="Enter your email" />
</B24FormField>
</template>
Hint slot
Use the #hint slot when the hint carries state rather than a word — a
character count, a badge, an icon. It renders inside the label row, next to
the label itself.
Pass hint as well as the slot, for the same reason as #description: the
announcement follows the prop, not the slot.
<template>
<B24FormField name="bio" label="Bio" hint="0 / 140">
<template #hint>
<B24Badge label="0 / 140" size="xs" color="air-secondary-accent-1" />
</template>
<B24Input placeholder="Tell us about yourself" class="w-full" />
</B24FormField>
</template>
<label>, not part of it, so its content does not
join the control's accessible name — unlike #label. It is announced through
aria-describedby instead.label or #label is present — so a hint on a label-less
field renders nothing at all, and nothing is announced either. Silent, but at
least consistent: until
#497 the control also
advertised aria-describedby="…-hint", pointing at an element that was never
drawn.Help
Use the help prop to display a help message below the form control. When used together with the error prop, the error prop takes precedence.
<template>
<B24FormField label="Email" help="Please enter a valid email address.">
<B24Input placeholder="Enter your email" class="w-full" />
</B24FormField>
</template>
Error
Use the error prop to display an error message below the form control. When used together with the help prop, the error prop takes precedence.
When used inside a Form, this is automatically set when a validation error occurs.
<template>
<B24FormField label="Email" error="Please enter a valid email address.">
<b24Input placeholder="Enter your email" class="w-full" />
</B24FormField>
</template>
What is announced is what was drawn
Every one of the five blocks renders from props.x || !!slots.x — the prop or
the slot will do. aria-describedby follows the same rule: it names the blocks
that ended up in the document, and only those.
Two conditions are worth knowing, because both hide a block you asked for:
hint lives in the label row and needs a label or #label to exist at all,
and help is the v-else-if of the error branch, so a rendered error takes
its place. In both cases nothing is drawn and nothing is announced.
nuxt/ui, which builds the attribute from
the props alone — so a slot with no prop went unannounced, and a prop whose
block never drew left the attribute naming an id that was not in the document.
Fixed here in #497 and recorded
as an invariant in .sync/PORTING.md §2, so a later port does not revert it.Error and help slots
#error is where the rule above bites hardest, because the error block has a
second condition on it:
v-if="props.error !== false && ((typeof error === 'string' && error) || !!slots.error)"
v-else-if="props.help || !!slots.help"
An #error slot alone satisfies that v-if whether or not there is an
error. So the block renders permanently, help — the v-else-if of the same
branch — never renders at all, and the control still reads
aria-invalid="false": that attribute reports the field's error state, and
there is none. The block is described, since it was drawn, but nothing tells
assistive technology the field is invalid:
<!-- Don't: the message is always visible, `help` never is, and a screen
reader is told the field is valid. -->
<B24FormField label="Email" name="email" help="We'll only use it to sign you in.">
<template #error>
<WarningIcon class="size-4" /> Please enter a valid email address.
</template>
<B24Input placeholder="Enter your email" />
</B24FormField>
Bind error to the message — or to false when there is none — and use the
slot for markup only. Then the branch behaves exactly as it does with the prop:
<script setup lang="ts">
import WarningIcon from '@bitrix24/b24icons-vue/main/WarningIcon'
const value = ref('')
const message = computed<string | false>(() =>
value.value.includes('@') ? false : 'Please enter a valid email address.'
)
</script>
<template>
<B24FormField
label="Email"
name="email"
:error="message"
help="We'll only use it to sign you in."
>
<template #error="{ error }">
<WarningIcon
class="size-4"
aria-hidden="true"
/> {{ error }}
</template>
<B24Input
v-model="value"
placeholder="Enter your email"
/>
</B24FormField>
</template>
help and error are mutually exclusive on screen and in the attribute
alike — whichever block drew is the one named.
error at all — the
Form sets it from the schema, and it is a string or undefined. undefined is
not false, so an #error slot still renders permanently there. Use #error
only on fields whose error you control, or use the prop and style the block
through b24ui.error.#help follows the general rule: pass help alongside it, or the help text
appears with nothing describing it. It disappears whenever an error is
showing, by design.
Error pattern
Use the error-pattern prop to match form errors with a regular expression. This is especially relevant for components with array values such as InputTags, where errors include array indices in their name (e.g. tags.0).
Size
Use the size prop to change the size of the FormField, the size is proxied to the form control.
We'll never share your email with anyone else.
<template>
<B24FormField
label="Email"
description="We'll never share your email with anyone else."
hint="Optional"
help="Please enter a valid email address."
size="lg"
>
<B24Input placeholder="Enter your email" class="w-full" />
</B24FormField>
</template>
Orientation
Use the orientation prop to change the layout of the FormField. Defaults to vertical.
<template>
<B24FormField
orientation="horizontal"
label="Email"
help="Please enter a valid email address."
class="w-72"
>
<B24Input placeholder="Enter your email" class="w-full" />
</B24FormField>
</template>
API
Props
Slots
Theme
export default {
slots: {
root: 'font-[family-name:var(--ui-font-family-system)] font-(--ui-font-weight-regular)',
wrapper: 'leading-(--ui-font-line-height-reset)',
labelWrapper: 'flex content-center items-center justify-between gap-1',
label: 'block text-label',
hint: 'text-description',
container: 'relative',
description: 'leading-(--ui-font-line-height-2xs) text-description',
error: 'text-(--ui-color-accent-main-alert)',
errorWrapper: 'flex flex-row flex-nowrap items-center gap-0.5',
errorIcon: 'size-4.5',
help: 'leading-(--ui-font-line-height-2xs) italic text-description'
},
variants: {
useDescription: {
true: {
wrapper: ''
},
false: {
wrapper: ''
}
},
size: {
xs: {
root: 'text-(length:--ui-font-size-xs)',
errorIcon: 'size-[16px]'
},
sm: {
root: 'text-(length:--ui-font-size-xs)',
errorIcon: 'size-[16px]'
},
md: {
root: 'text-(length:--ui-font-size-sm)',
errorIcon: 'size-4.5'
},
lg: {
root: 'text-(length:--ui-font-size-md)'
}
},
required: {
true: {
label: "after:content-['*'] after:ms-0.5 after:text-(--ui-color-accent-main-alert)"
}
},
orientation: {
vertical: {
container: '',
description: 'mt-[2px]',
error: 'mt-[4px]',
errorIcon: 'mt-[2px]',
help: 'mt-[6px]'
},
horizontal: {
root: 'flex justify-between place-items-baseline gap-2'
}
}
},
compoundVariants: [
{
useDescription: true,
orientation: 'vertical',
class: {
wrapper: 'mb-[6px]'
}
},
{
useDescription: false,
orientation: 'vertical',
class: {
wrapper: 'mb-[10px]'
}
},
{
orientation: 'horizontal',
class: {
wrapper: ''
}
}
],
defaultVariants: {
size: 'md'
}
}