Skip to content

Vue Input Component

A styled input component with the label positioned inside the border outline. Wraps a native <input> with v-model support, forwards focus/blur events, and exposes focus()/blur() methods via defineExpose.

When to use

Use this component for any text input that needs a floating label inside the border outline. It wraps a native <input> with consistent styling and exposes programmatic focus()/blur() methods for parent components.

The pattern

Bind the Input component with v-model and pass props for label, type, and name. Additional attributes forward to the underlying <input> element.

Basic text input

The component accepts standard input attributes and forwards them through to the native element.

vue
<Input
  type="text"
  name="username"
  label="Username"
  v-model="form.username"
  autocomplete="off"
/>

Programmatic focus from parent

Access focus() and blur() methods through a template ref on the component.

vue
<script setup>
  import { ref } from "vue";
  const inputRef = ref(null);
  
  function focusInput() {
    inputRef.value?.focus();
  }
</script>

<template>
  <Input ref="inputRef" label="Search" v-model="query" />
  <button @click="focusInput">Focus</button>
</template>

Implementing the Input component

Use defineModel() for two-way binding. useAttrs() combined with inheritAttrs: false controls which attributes land on the <input> vs. the wrapper <label>. defineExpose surfaces imperative methods.

vue
<template>
  <label
    class="InputContainer"
    v-bind="{ class: className, style }"
  >
    <span class="Label">{{ label }}</span>
    <input
      class="InputField"
      ref="input"
      v-model="model"
      :type="type"
      :name="name"
      :placeholder="placeholder"
      @focus="emit('focus', $event)"
      @blur="emit('blur', $event)"
      v-bind="$attrs"
    />
  </label>
</template>

<script setup>
  import { ref, useAttrs, unref } from "vue";
  
  const { class: className, style, ...$attrs } = useAttrs();
  
  defineOptions({ inheritAttrs: false });
  
  const props = defineProps({
    type: { type: String, default: "text" },
    name: { type: String, default: "" },
    label: { type: String, default: "" },
    placeholder: { type: String, default: "" },
  });
  
  const model = defineModel({ default: "" });
  
  const emit = defineEmits(["focus", "blur"]);
  
  defineExpose({ focus, blur });
  
  const input = ref(null);
  
  function focus() {
    unref(input)?.focus();
  }
  
  function blur() {
    unref(input)?.blur();
  }
</script>

Attribute forwarding with inheritAttrs: false

Setting inheritAttrs: false prevents Vue from auto-applying attributes to the root <label>. Instead, class and style go to the <label>, while remaining attributes ($attrs) go to the <input>. This gives precise control over which element receives ARIA attributes, data-* attributes, and other forwarded props.

Two-way binding with defineModel()

The defineModel() macro creates the two-way binding. Parents use v-model without needing to handle :modelValue + @update:modelValue manually.

Exposing methods with defineExpose

defineExpose({ focus, blur }) makes these methods callable from a parent via a template ref. Without this, the methods are private to the component.

Label hiding

When label is empty, the <span class="Label"> can be hidden with CSS (:empty { display: none }), and padding adjusts automatically.

Trade-offs

  • Label-in-border layout requires CSS that makes the label overlap the border. This is tightly coupled to the styling approach — changing the visual design means updating both the component template and CSS.
  • useAttrs() destructuring is evaluated once at setup time. Dynamic class or style changes from the parent after mount won't propagate through the destructured values. This is fine for static layouts but problematic if the parent toggles classes dynamically.
  • No built-in validation — This component only handles display and binding. Validation belongs in the form composable (useForm) or a schema validation layer, keeping the input component focused on rendering.