Appearance
FormInput
FormInput
is a form input wrapper component that allows you to easily add a label and error text to a form input and mark it as required. Additionally, it sets for
, id
, aria-required
, aria-labelledby
and aria-describedby
attributes automatically.
- See also
Component
vue
<template>
<div
class="form-input"
:class="{ 'form-input--error': errorMessage }">
<div class="form-input__prepend-label-container">
<slot name="prepend-label" />
</div>
<div
class="form-input__label-container"
:class="{
'form-input__label-container--inline-input':
$slots['prepend-input'] || $slots['append-input'],
}">
<div
v-if="$slots['prepend-input']"
ref="inputContainer"
class="form-input__input-container">
<slot name="prepend-input" />
</div>
<slot
:input-id="inputId"
name="label">
<label :for="inputId">{{ labelText }}</label>
</slot>
<div
v-if="$slots['append-input']"
ref="inputContainer"
class="form-input__input-container">
<slot name="append-input" />
</div>
</div>
<div
v-if="$slots.default"
ref="inputContainer"
class="form-input__input-container">
<slot :input-id="inputId" />
</div>
<div class="form-input__error-message-container">
<slot name="error-message" :error-id="errorId">
<small
v-if="errorMessage"
:id="errorId"
class="form-input__error-message"
>{{ errorMessage }}</small
>
</slot>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, useId, watchEffect } from 'vue'
const props = defineProps<{
errorMessage?: string
required?: boolean
label?: string
}>()
const inputContainer = ref<HTMLElement | null>(null)
const inputId = useId()
const errorId = useId()
const labelText = computed(() => {
return props.required ? `${props.label} *` : props.label
})
const updateAriaAttributes = (element: HTMLElement, isNativeInput: boolean) => {
if (props.required) {
element.setAttribute('aria-required', 'true')
} else {
element.removeAttribute('aria-required')
}
if (props.errorMessage) {
element.setAttribute('aria-describedby', errorId)
} else {
element.removeAttribute('aria-describedby')
}
if (!isNativeInput) {
element.setAttribute('aria-labelledby', inputId)
}
}
onMounted(() => {
if (!inputContainer.value || Array.isArray(inputContainer.value)) return
// Try to find a native input element first
const inputElement = inputContainer.value.querySelector('input, textarea, select')
if (inputElement) {
// For native input elements, set the id
inputElement.id = inputId
// Watch for prop changes and update aria attributes
watchEffect(() => {
updateAriaAttributes(inputElement as HTMLElement, true)
})
} else {
// For non-native elements (like PrimeVue components without native inputs),
// find the root component element and set aria attributes
const componentElement = inputContainer.value.querySelector('[class*="p-"]')
if (componentElement) {
// Watch for prop changes and update aria attributes
watchEffect(() => {
updateAriaAttributes(componentElement as HTMLElement, false)
})
}
}
})
</script>
<style scoped lang="scss">
.form-input {
display: flex;
flex-direction: column;
gap: 5px;
&--error {
:deep(.form-input__input-container) {
.p-inputtext,
.p-cascadeselect,
.p-checkbox-box,
.p-colorpicker,
.p-datepicker,
.p-editor,
.p-inputnumber-input,
.p-inputotp-input,
.p-listbox,
.p-multiselect,
.p-radiobutton-input,
.p-select,
.p-selectbutton,
.p-textarea,
.p-togglebutton,
.p-toggleswitch .p-toggleswitch-slider,
.p-treeselect {
border: 1px solid var(--p-red-500);
}
.p-slider .p-slider-handle,
.p-slider .p-slider-handle::before {
background-color: var(--p-red-500);
}
.p-knob svg .p-knob-range {
stroke: var(--p-red-500);
}
.p-knob svg .p-knob-text,
.p-rating svg path {
fill: var(--p-red-500);
}
}
}
.form-input__label-container {
display: flex;
justify-content: flex-start;
gap: 5px;
flex-direction: row;
&--inline-input {
gap: 10px;
}
}
.form-input__input-container {
> :not(.p-toggleswitch) {
width: 100%;
}
}
.form-input__error-message-container {
display: flex;
align-items: flex-start;
gap: 5px;
flex-direction: column;
.form-input__error-message {
color: var(--p-red-500);
}
}
}
</style>
Props
label
- Type:
string
- Required:
false
- Description: The label to display for the input.
required
- Type:
boolean
- Default:
false
- Description: A flag that indicates if the input is required. Will add a
*
to the label and setaria-required="true"
on the input.
errorMessage
- Type:
string
- Default:
null
- Description: The error message to display for the input. Adds
aria-describedby
to the input element and a custom id to error message.
Slots
default
- Description: The input element/component (e.g.,
<input>
,<InputText>
, etc.) should be placed in the default slot. It will be wrapped and styled by the component. - Slot Props:
input-id
(string
): The generated unique ID for the input element.
label
- Description: Customizes the label area.
- Slot Props:
input-id
(string
): The generated unique ID for the input element.
- Default: Renders a
<label>
with the providedlabel
prop.
prepend-label
- Description: Content to display before the label (e.g., an icon or text). Renders inside a container before the label.
prepend-input
- Description: Slot to use to position your
input
before thelabel
inline.
append-input
- Description: Slot to use to position your
input
after thelabel
inline.
error-message
- Description: Customizes the error message area. By default, displays the
errorMessage
prop if present. You can override this slot to provide custom error content. - Slot Props:
error-id
(string
): The generated unique ID for the error message element.
Accessibility
The FormInput
component includes built-in accessibility features:
- Automatically generates a unique ID for the input element and associates it with the label via the
for
attribute. - Adds
aria-required="true"
to the input element when therequired
prop is set totrue
. - Adds
aria-describedby
to the input element when an error message is present, linking it to the error message element. - Supports
aria-labelledby
for components that require it (e.g., PrimeVueSelect
,ToggleButton
,Slider
,Knob
,ListBox
,SelectButton
).