Form Group

A versatile wrapper for various form components such as `Input`, `Textarea`, `Select`, and more. It offers a comprehensive set of features, including label, description, hint, message, status, and others.

Examples

Basic

PropDefaultTypeDescription
label-stringAdds a label to the form group.
id-stringSets the form group's id.
for-stringSets the label's for attribute.
Preview
Code

Required

PropDefaultTypeDescription
requiredfalsebooleanAdds * to the label.
Preview
Code

Description

PropDefaultTypeDescription
description-stringAdds a description to the form group.
Preview
Code
We'll never share your email with anyone else.

Hint

PropDefaultTypeDescription
hint-stringAdds a hint to the form group.
Preview
Code
Optional

Message

PropDefaultTypeDescription
message-stringSets the form group's message.
Preview
Code

We'll never share your email with anyone else.

Status

PropDefaultTypeDescription
status-info,success, warning, error, undefinedSets the form group's status.
Preview
Code

Your username is available.

This information will be visible to other users.

Your email is invalid

Your password is weak.

Counter

PropDefaultTypeDescription
counter-objectEnables the form group's counter.
CounterDefaultTypeDescription
value-stringSets the counter value.
max-numberSets the maximum counter number.
Preview
Code

Username has no length limit

0
0/10

Slots

NamePropsDescription
default-The default slot of the form group, refer Basic section.
top-The top section of the form group.
bottom-The bottom section of the form group.
label-The label slot of the form group.
description-The description slot of the form group.
hint-The hint slot of the form group.
message-The message slot of the form group.
counter-The counter slot of the form group.

Props

types/form-group.ts
import type { HTMLAttributes } from 'vue'
import type { NLabelProps } from './label'

export interface NFormGroupProps extends NLabelProps {
  class?: HTMLAttributes['class']
  /**
   * Update the form group status.
   *
   * @default null
   */
  status?: 'info' | 'success' | 'warning' | 'error'
  /**
   * Add a required indicator to the form group.
   *
   * @default false
   */
  required?: boolean
  /**
   * Manually set the id attribute.
   *
   * By default, the id attribute is generated randomly for accessibility reasons.
   *
   * @default randomId
   * @example
   * id="email"
   */
  id?: string
  /**
   * Label for the form group.
   *
   * @example
   * label="Email"
   */
  label?: string
  /**
   * Display `hint` message for the form group.
   *
   * @example
   * hint="Enter your email address"
   */
  hint?: any
  /**
   * Display `Description` message for the form group.
   *
   * @example
   * description="We will never share your email with anyone else."
   */
  description?: any
  /**
   * Display `Message` for the form group.
   * Useful for displaying validation errors.
   *
   * @example
   * message="Email is required"
   */
  message?: any

  /**
   * Display `counter` for the form group.
   * Useful for displaying character count.
   *
   * @example
   * counter="{ value: 0, max: 100 }"
   */
  counter?: {
    value: number
    max?: number
  }

  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/form-group.ts
   */
  una?: {
    formGroup?: HTMLAttributes['class']

    formGroupTopWrapper?: HTMLAttributes['class']
    formGroupTopWrapperInner?: HTMLAttributes['class']
    formGroupBottomWrapper?: HTMLAttributes['class']
    formGroupCounterWrapper?: HTMLAttributes['class']
    formGroupMessageWrapper?: HTMLAttributes['class']
    formGroupLabelWrapper?: HTMLAttributes['class']

    formGroupLabel?: HTMLAttributes['class']
    formGroupDescription?: HTMLAttributes['class']
    formGroupHint?: HTMLAttributes['class']
    formGroupMessage?: HTMLAttributes['class']

    formGroupLabelRequired?: HTMLAttributes['class']
  }
}

Presets

shortcuts/form-group.ts
type FormGroupPrefix = 'form-group'

export const staticFormGroup: Record<`${FormGroupPrefix}-${string}` | FormGroupPrefix, string> = {
  // base
  'form-group': 'space-y-2',
  'form-group-description': 'text-0.8rem text-muted',
  'form-group-hint': 'text-sm leading-none text-muted',
  'form-group-message': 'text-0.8em transition-all duration-1000 ease-in-out',

  // wrappers
  'form-group-top-wrapper': 'flex flex-col space-y-1.5',
  'form-group-top-wrapper-inner': 'flex justify-between items-end space-x-1.5',
  'form-group-bottom-wrapper': 'flex space-x-1.5 justify-between items-start',
  'form-group-message-wrapper': '',

  // label
  'form-group-label-wrapper': 'flex',
  'form-group-label': 'block label-base',
  'form-group-label-required': 'after:content-[\'*\'] after:ms-0.5 after:text-error',

  // counter
  'form-group-counter-wrapper': 'text-0.8em',
  'form-group-counter-error': 'text-error',
  'form-group-counter-current': 'text-accent',
  'form-group-counter-separator': 'text-muted',
  'form-group-counter-max': 'text-muted',
}

export const formGroup = [
  staticFormGroup,
]

Components

FormGroup.vue
<script setup lang="ts">
import type { NFormGroupProps } from '../../types'
import { computed } from 'vue'
import { cn, randomId } from '../../utils'
import Label from '../elements/Label.vue'
import NFormGroupDefaultSlot from '../slots/FormGroupDefault'

const props = defineProps<NFormGroupProps>()

const id = computed(() => props.id ?? randomId('form-group'))

const statusClassVariants = computed(() => {
  const text = {
    info: 'text-info',
    success: 'text-success',
    warning: 'text-warning',
    error: 'text-error',
    default: 'text-muted',
  }

  return text[props.status ?? 'default']
})
</script>

<template>
  <div
    :class="cn(
      'form-group',
      props.class,
      una?.formGroup,
    )"
  >
    <slot name="top">
      <div
        form-group="message-wrapper"
        :class="una?.formGroupMessageWrapper"
      >
        <div
          v-if="label || hint || description"
          form-group="top-wrapper"
          :class="una?.formGroupTopWrapper"
        >
          <div
            v-if="label || hint"
            form-group="top-wrapper-inner"
            :class="una?.formGroupTopWrapperInner"
          >
            <slot name="label">
              <Label
                :for="props.for ?? id"
              >
                <div
                  form-group="label-wrapper"
                  :class="una?.formGroupLabelWrapper"
                >
                  <span
                    form-group="label"
                    :class="una?.formGroupLabel"
                  >
                    {{ label }}
                  </span>
                  <span
                    v-if="required"
                    form-group="label-required"
                    :class="una?.formGroupLabelRequired"
                  />
                </div>
              </Label>
            </slot>

            <slot name="hint">
              <span
                v-if="hint"
                form-group="hint"
                :class="una?.formGroupHint"
              >
                {{ hint }}
              </span>
            </slot>
          </div>

          <slot name="description">
            <span
              v-if="description"
              form-group="description"
              :class="una?.formGroupDescription"
            >
              {{ description }}
            </span>
          </slot>
        </div>
      </div>
    </slot>

    <NFormGroupDefaultSlot
      :id="id"
      :status="status"
    >
      <slot />
    </NFormGroupDefaultSlot>

    <slot name="bottom">
      <div
        v-if="message || counter"
        form-group="bottom-wrapper"
        :class="[
          { 'justify-end': !message && counter },
          una?.formGroupBottomWrapper,
        ]"
      >
        <slot name="message">
          <div
            v-if="message"
            form-group="message-wrapper"
            :class="una?.formGroupMessageWrapper"
          >
            <p
              form-group="message"
              :class="[
                una?.formGroupMessage,
                statusClassVariants,
              ]"
            >
              {{ message }}
            </p>
          </div>
        </slot>

        <slot name="counter">
          <div
            v-if="counter"
            form-group="counter-wrapper"
            :class="una?.formGroupCounterWrapper"
          >
            <span
              :class="`${counter?.value >= (counter?.max || 0) && counter?.max
                ? 'form-group-counter-error'
                : 'form-group-counter-current'}`"
            >
              {{ counter?.value }}
            </span>
            <span v-if="counter?.max" form-group="counter-separator">/</span>
            <span v-if="counter?.max" form-group="counter-max">{{ counter?.max }}</span>
          </div>
        </slot>
      </div>
    </slot>
  </div>
</template>