Skip to content

Input

Experimental Alpha

This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.

A flexible, accessible form input component that supports text inputs, textareas, various styling variants, validation states, and input addons.

Examples

Vue
Lit
React
Live Preview

Basic Input

Sizes

Shape Variants

States

Textarea

With Addons (Automatic Detection)

Addons are automatically detected when you provide slot content - no props needed!

.00

Addons with Style Variants

Addons work seamlessly with all input styling variants

USD

Interactive Event Handling

Demonstrates event handling with @input, @change, @focus, @blur, and v-model:value

Character count: 0 | Last input: (none)

Last confirmed value: (none)

Status: Not focused

Current value: (empty)

Words: 0
Characters: 0
Status: Ready

Label Positioning

Control label placement with the labelPosition prop: top (default), start, end, or bottom

CSS Shadow Parts Customization

Input can be customized using CSS Shadow Parts: ::part(ag-input), ::part(ag-textarea), ::part(ag-input-label), ::part(ag-input-error), ::part(ag-input-help)

View Vue Code
<template>
  <section>
    <div class="mbe4">
      <h2>Basic Input</h2>
    </div>
    <div class="stacked mbe4">
      <VueInput
        v-model:value="basicValue"
        label="Email"
        type="email"
        placeholder="you@example.com"
        class="mbe2"
      />
      <VueInput
        v-model:value="password"
        label="Password"
        type="password"
        placeholder="Enter password"
        class="mbe2"
      />
      <VueInput
        v-model:value="search"
        label="Search"
        type="search"
        placeholder="Search..."
        class="mbe2"
      />
    </div>

    <div class="mbe4">
      <h2>Sizes</h2>
    </div>
    <div class="stacked mbe4">
      <VueInput
        v-model:value="sizeSmall"
        label="Small Input"
        size="small"
        placeholder="Small size"
        class="mbe2"
      />
      <VueInput
        v-model:value="sizeDefault"
        label="Default Input"
        size="default"
        placeholder="Default size"
        class="mbe2"
      />
      <VueInput
        v-model:value="sizeLarge"
        label="Large Input"
        size="large"
        placeholder="Large size"
        class="mbe2"
      />
    </div>

    <div class="mbe4">
      <h2>Shape Variants</h2>
    </div>
    <div class="stacked mbe4">
      <VueInput
        v-model:value="shapeDefault"
        label="Default (rectangular)"
        placeholder="Default rectangular"
        class="mbe2"
      />
      <VueInput
        v-model:value="shapeRounded"
        label="Rounded"
        :rounded="true"
        placeholder="Rounded corners"
        class="mbe2"
      />
      <VueInput
        v-model:value="shapeCapsule"
        label="Capsule"
        :capsule="true"
        placeholder="Capsule shape"
        class="mbe2"
      />
      <VueInput
        v-model:value="shapeUnderlined"
        label="Underlined"
        :underlined="true"
        placeholder="Underlined only"
        class="mbe2"
      />
      <VueInput
        v-model:value="shapeUnderlinedBg"
        label="Underlined with Background"
        :underlined-with-background="true"
        placeholder="Underlined with background"
        class="mbe2"
      />
    </div>

    <div class="mbe4">
      <h2>States</h2>
    </div>
    <div class="stacked mbe4">
      <VueInput
        v-model:value="stateDefault"
        label="Normal"
        placeholder="Normal state"
        class="mbe2"
      />
      <VueInput
        v-model:value="stateRequired"
        label="Required"
        :required="true"
        placeholder="Required field"
        help-text="This field is required"
        class="mbe2"
      />
      <VueInput
        v-model:value="stateDisabled"
        label="Disabled"
        :disabled="true"
        placeholder="Disabled input"
        class="mbe2"
      />
      <VueInput
        v-model:value="stateReadonly"
        label="Readonly"
        :readonly="true"
        value="Read-only value"
        class="mbe2"
      />
      <VueInput
        v-model:value="stateInvalid"
        label="Invalid"
        :invalid="true"
        placeholder="Invalid input"
        error-message="This field has an error"
        class="mbe2"
      />
    </div>

    <div class="mbe4">
      <h2>Textarea</h2>
    </div>
    <div class="stacked mbe4">
      <VueInput
        v-model:value="textareaValue"
        label="Comments"
        type="textarea"
        placeholder="Enter your comments..."
        :rows="4"
        class="mbe2"
      />
      <VueInput
        v-model:value="textareaLarge"
        label="Description"
        type="textarea"
        placeholder="Enter description..."
        :rows="6"
        size="large"
        help-text="Provide a detailed description"
        class="mbe2"
      />
    </div>

    <div class="mbe4">
      <h2>With Addons (Automatic Detection)</h2>
      <p style="margin-top: 0.5rem; color: var(--ag-text-secondary); font-size: 0.875rem;">
        Addons are automatically detected when you provide slot content - no props needed!
      </p>
    </div>
    <div class="stacked mbe4">
      <VueInput
        v-model:value="addonLeft"
        label="Website URL"
        placeholder="example.com"
        class="mbe2"
      >
        <template #addon-left>
          <Globe
            :size="18"
            color="var(--ag-primary)"
          />
        </template>
      </VueInput>

      <VueInput
        v-model:value="addonRight"
        label="Price"
        placeholder="0.00"
        class="mbe2"
      >
        <template #addon-right>
          <DollarSign
            :size="18"
            color="var(--ag-success)"
          />
        </template>
      </VueInput>

      <VueInput
        v-model:value="addonBoth"
        label="Amount"
        placeholder="100"
        class="mbe2"
      >
        <template #addon-left>
          <DollarSign
            :size="18"
            color="var(--ag-success)"
          />
        </template>
        <template #addon-right>
          <span>.00</span>
        </template>
      </VueInput>

      <VueInput
        v-model:value="addonPercent"
        label="Discount"
        type="number"
        placeholder="10"
        class="mbe2"
      >
        <template #addon-right>
          <Percent :size="18" />
        </template>
      </VueInput>
    </div>

    <div class="mbe4">
      <h2>Addons with Style Variants</h2>
      <p style="margin-top: 0.5rem; color: var(--ag-text-secondary); font-size: 0.875rem;">
        Addons work seamlessly with all input styling variants
      </p>
    </div>
    <div class="stacked mbe4">
      <VueInput
        v-model:value="addonRounded"
        label="Rounded with Addons"
        type="number"
        placeholder="0.00"
        :rounded="true"
        class="mbe2"
      >
        <template #addon-left>
          <DollarSign
            :size="18"
            color="var(--ag-primary)"
          />
        </template>
        <template #addon-right>
          <span style="font-weight: 600;">USD</span>
        </template>
      </VueInput>

      <VueInput
        v-model:value="addonCapsule"
        label="Capsule with Addon"
        type="search"
        placeholder="Find products..."
        :capsule="true"
        class="mbe2"
      >
        <template #addon-left>
          <Search
            :size="18"
            color="var(--ag-secondary)"
          />
        </template>
      </VueInput>

      <VueInput
        v-model:value="addonUnderlined"
        label="Underlined with Addon"
        type="number"
        placeholder="10"
        :underlined="true"
        class="mbe2"
      >
        <template #addon-right>
          <Percent :size="18" />
        </template>
      </VueInput>

      <VueInput
        v-model:value="addonUnderlinedBg"
        label="Underlined with Background"
        type="text"
        placeholder="Enter username"
        :underlined-with-background="true"
        class="mbe2"
      >
        <template #addon-left>
          <User2
            :size="18"
            color="var(--ag-secondary)"
          />
        </template>
      </VueInput>
    </div>

    <div class="mbe4">
      <h2>Interactive Event Handling</h2>
      <p
        class="mbe2"
        style="color: var(--ag-text-secondary); font-size: 0.875rem;"
      >
        Demonstrates event handling with @input, @change, @focus, @blur, and v-model:value
      </p>
    </div>
    <div class="stacked mbe4">
      <!-- Pattern 1: @input event for real-time tracking -->
      <div>
        <VueInput
          v-model:value="interactiveEmail"
          label="Email (@input event)"
          type="email"
          placeholder="you@example.com"
          @input="handleInputEvent"
          class="mbe2"
        />
        <p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--ag-text-secondary);">
          Character count: <strong>{{ interactiveEmail.length }}</strong> | Last input: <strong>{{ lastInputTime }}</strong>
        </p>
      </div>

      <!-- Pattern 2: @change event for completed changes -->
      <div>
        <VueInput
          v-model:value="interactiveUsername"
          label="Username (@change event)"
          placeholder="Enter username"
          @change="handleChangeEvent"
          class="mbe2"
        />
        <p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--ag-text-secondary);">
          Last confirmed value: <strong>{{ confirmedUsername || '(none)' }}</strong>
        </p>
      </div>

      <!-- Pattern 3: Focus and Blur events -->
      <div>
        <VueInput
          v-model:value="interactiveFocus"
          label="Focus Tracking (@focus/@blur)"
          placeholder="Click to focus"
          @focus="handleFocus"
          @blur="handleBlur"
          class="mbe2"
        />
        <p style="margin-top: 0.5rem; font-size: 0.875rem;">
          Status: <strong :style="{ color: isFocused ? 'var(--ag-success)' : 'var(--ag-text-secondary)' }">
            {{ isFocused ? 'Focused' : 'Not focused' }}
          </strong>
          {{ focusCount > 0 ? `(focused ${focusCount} time${focusCount > 1 ? 's' : ''})` : '' }}
        </p>
      </div>

      <!-- Pattern 4: v-model:value with reactive updates -->
      <div>
        <VueInput
          v-model:value="interactiveReactive"
          label="Two-way Binding (v-model:value)"
          placeholder="Type here..."
          class="mbe2"
        />
        <p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--ag-text-secondary);">
          Current value: <strong>{{ interactiveReactive || '(empty)' }}</strong>
        </p>
        <button
          @click="interactiveReactive = 'Programmatically set!'"
          style="margin-top: 0.5rem; padding: 0.25rem 0.75rem; border: 1px solid var(--ag-border); border-radius: var(--ag-radius-sm); cursor: pointer;"
        >
          Set value programmatically
        </button>
      </div>

      <!-- Pattern 5: Textarea with all events -->
      <div>
        <VueInput
          v-model:value="interactiveTextarea"
          label="Textarea with Events"
          type="textarea"
          :rows="3"
          placeholder="Try typing, then click outside..."
          @input="handleTextareaInput"
          @change="handleTextareaChange"
          @focus="handleTextareaFocus"
          @blur="handleTextareaBlur"
          class="mbe2"
        />
        <div style="margin-top: 0.5rem; font-size: 0.875rem; padding: 0.5rem; background: var(--ag-background-secondary); border-radius: 4px;">
          <div>Words: <strong>{{ wordCount }}</strong></div>
          <div>Characters: <strong>{{ interactiveTextarea.length }}</strong></div>
          <div>Status: <strong>{{ textareaStatus }}</strong></div>
        </div>
      </div>
    </div>

    <div class="mbe4">
      <h2>Label Positioning</h2>
      <p style="margin-top: 0.5rem; color: var(--ag-text-secondary); font-size: 0.875rem;">
        Control label placement with the <code>labelPosition</code> prop: <code>top</code> (default), <code>start</code>, <code>end</code>, or <code>bottom</code>
      </p>
    </div>
    <div class="stacked mbe4">
      <VueInput
        v-model:value="labelTop"
        label="Top Position (Default)"
        label-position="top"
        placeholder="Label above input"
        help-text="Default vertical layout - best for mobile"
        class="mbe2"
      />
      <VueInput
        v-model:value="labelStart"
        label="Name:"
        label-position="start"
        placeholder="Enter name"
        help-text="Horizontal layout - label before input"
        class="mbe2"
      />
      <VueInput
        v-model:value="labelEnd"
        label="Email:"
        label-position="end"
        placeholder="you@example.com"
        help-text="Horizontal layout - label after input"
        class="mbe2"
      />
      <VueInput
        v-model:value="labelBottom"
        label="Bottom Position"
        label-position="bottom"
        placeholder="Enter value"
        help-text="Vertical layout - label below input"
        class="mbe2"
      />
    </div>

    <div class="mbe4">
      <h2>CSS Shadow Parts Customization</h2>
      <p style="margin-top: 0.5rem; margin-bottom: 1rem; color: var(--vp-c-text-2);">
        Input can be customized using CSS Shadow Parts:
        <code>::part(ag-input)</code>,
        <code>::part(ag-textarea)</code>,
        <code>::part(ag-input-label)</code>,
        <code>::part(ag-input-error)</code>,
        <code>::part(ag-input-help)</code>
      </p>
    </div>
    <div class="stacked mbe4">
      <VueInput
        v-model:value="customGradient"
        class="custom-gradient-input mbe2"
        label="Modern Gradient Border"
        placeholder="you@example.com"
        type="email"
      />

      <VueInput
        v-model:value="customMaterial"
        class="custom-material-input mbe2"
        label="Material Design Style"
        placeholder="John Doe"
      />

      <VueInput
        v-model:value="customTextarea"
        class="custom-textarea mbe2"
        label="Styled Textarea"
        type="textarea"
        :rows="4"
        placeholder="Paste your code here..."
        help-text="Monospace font with dashed border"
      />
    </div>
  </section>
</template>

<script>
import VueInput from "agnosticui-core/input/vue";
import { Globe, DollarSign, Percent, Search, User2 } from "lucide-vue-next";

export default {
  name: "InputExamples",
  components: {
    VueInput,
    Globe,
    DollarSign,
    Percent,
    User2,
    Search,
  },
  data() {
    return {
      // Basic
      basicValue: "",
      password: "",
      search: "",

      // Sizes
      sizeSmall: "",
      sizeDefault: "",
      sizeLarge: "",

      // Shapes
      shapeDefault: "",
      shapeRounded: "",
      shapeCapsule: "",
      shapeUnderlined: "",
      shapeUnderlinedBg: "",

      // States
      stateDefault: "",
      stateRequired: "",
      stateDisabled: "Disabled value",
      stateReadonly: "Read-only value",
      stateInvalid: "",

      // Textarea
      textareaValue: "",
      textareaLarge: "",

      // Addons
      addonLeft: "",
      addonRight: "",
      addonBoth: "",
      addonPercent: "",

      // Addons with style variants
      addonRounded: "",
      addonCapsule: "",
      addonUnderlined: "",
      addonUnderlinedBg: "",

      // Custom styles
      customGradient: "",
      customMaterial: "",
      customError: "",
      customTextarea: "",

      // Label positioning
      labelTop: "",
      labelStart: "",
      labelEnd: "",
      labelBottom: "",

      // Interactive event handling
      interactiveEmail: "",
      lastInputTime: "(none)",
      interactiveUsername: "",
      confirmedUsername: "",
      interactiveFocus: "",
      isFocused: false,
      focusCount: 0,
      interactiveReactive: "",
      interactiveTextarea: "",
      textareaStatus: "Ready",
    };
  },
  computed: {
    wordCount() {
      if (!this.interactiveTextarea.trim()) return 0;
      return this.interactiveTextarea.trim().split(/\s+/).length;
    },
  },
  methods: {
    handleInputEvent(event) {
      const now = new Date();
      this.lastInputTime = now.toLocaleTimeString();
      console.log("Input event:", event);
    },
    handleChangeEvent(event) {
      this.confirmedUsername = this.interactiveUsername;
      console.log("Change event:", event);
    },
    handleFocus(event) {
      this.isFocused = true;
      this.focusCount++;
      console.log("Focus event:", event);
    },
    handleBlur(event) {
      this.isFocused = false;
      console.log("Blur event:", event);
    },
    handleTextareaInput(event) {
      this.textareaStatus = "Typing...";
      console.log("Textarea input event:", event);
    },
    handleTextareaChange(event) {
      this.textareaStatus = "Changes saved";
      console.log("Textarea change event:", event);
    },
    handleTextareaFocus(event) {
      this.textareaStatus = "Focused";
      console.log("Textarea focus event:", event);
    },
    handleTextareaBlur(event) {
      this.textareaStatus = "Ready";
      console.log("Textarea blur event:", event);
    },
  },
};
</script>

<style scoped>
/* Modern gradient input style */
.custom-gradient-input::part(ag-input) {
  border: 2px solid transparent;
  background: linear-gradient(white, white) padding-box,
    linear-gradient(135deg, #667eea 0%, #764ba2 100%) border-box;
  border-radius: 12px;
  padding: 0.75rem 1rem;
  font-weight: 500;
  transition: all 0.3s ease;
}

.custom-gradient-input::part(ag-input):focus {
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
  transform: translateY(-1px);
}

.custom-gradient-input::part(ag-input-label) {
  font-weight: 700;
  color: var(--ag-text-secondary);
  text-transform: uppercase;
  font-size: 0.75rem;
  letter-spacing: 0.05em;
}

/* Material Design-inspired */
.custom-material-input::part(ag-input) {
  border: none;
  border-bottom: 2px solid #e5e7eb;
  border-radius: 0;
  padding: 0.5rem 0;
  background: transparent;
  transition: border-color 0.2s ease;
}

.custom-material-input::part(ag-input):focus {
  border-bottom-color: #667eea;
  outline: none;
}

.custom-material-input::part(ag-input-label) {
  font-size: 0.875rem;
  color: var(--ag-text-secondary);
  margin-bottom: 0.25rem;
}
/* Textarea customization */
.custom-textarea::part(ag-textarea) {
  border: 2px dashed #9ca3af;
  border-radius: 12px;
  background: #f9fafb;
  padding: 1rem;
  font-family: "Monaco", "Courier New", monospace;
  font-size: 0.875rem;
  line-height: 1.6;
  transition: all 0.3s ease;
}

.custom-textarea::part(ag-textarea):focus {
  border-style: solid;
  border-color: #667eea;
  background: white;
}

.custom-textarea::part(ag-input-label) {
  font-weight: 700;
  color: var(--ag-text-secondary);
}
</style>
Live Preview
View Lit / Web Component Code
import { LitElement, html, css } from 'lit';
import 'agnosticui-core/input';

export class InputLitExamples extends LitElement {
  static properties = {
    // Basic
    basicValue: { type: String },
    password: { type: String },
    search: { type: String },
    // Sizes
    sizeSmall: { type: String },
    sizeDefault: { type: String },
    sizeLarge: { type: String },
    // Shapes
    shapeDefault: { type: String },
    shapeRounded: { type: String },
    shapeCapsule: { type: String },
    shapeUnderlined: { type: String },
    shapeUnderlinedBg: { type: String },
    // States
    stateDefault: { type: String },
    stateRequired: { type: String },
    stateDisabled: { type: String },
    stateReadonly: { type: String },
    stateInvalid: { type: String },
    // Textarea
    textareaValue: { type: String },
    textareaLarge: { type: String },
    // Addons
    addonLeft: { type: String },
    addonRight: { type: String },
    addonBoth: { type: String },
    addonPercent: { type: String },
    // Addons with style variants
    addonRounded: { type: String },
    addonCapsule: { type: String },
    addonUnderlined: { type: String },
    addonUnderlinedBg: { type: String },
    // Custom styles
    customGradient: { type: String },
    customMaterial: { type: String },
    customTextarea: { type: String },
    // Label positioning
    labelTop: { type: String },
    labelStart: { type: String },
    labelEnd: { type: String },
    labelBottom: { type: String },
    // Interactive event handling
    interactiveEmail: { type: String },
    lastInputTime: { type: String },
    interactiveUsername: { type: String },
    confirmedUsername: { type: String },
    interactiveFocus: { type: String },
    isFocused: { type: Boolean },
    focusCount: { type: Number },
    interactiveReactive: { type: String },
    interactiveTextarea: { type: String },
    textareaStatus: { type: String },
  };

  static styles = css`
    /* Modern gradient input style */
    .custom-gradient-input::part(ag-input) {
      border: 2px solid transparent;
      background: linear-gradient(white, white) padding-box,
        linear-gradient(135deg, #667eea 0%, #764ba2 100%) border-box;
      border-radius: 12px;
      padding: 0.75rem 1rem;
      font-weight: 500;
      transition: all 0.3s ease;
    }

    .custom-gradient-input::part(ag-input):focus {
      box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
      transform: translateY(-1px);
    }

    .custom-gradient-input::part(ag-input-label) {
      font-weight: 700;
      color: var(--ag-text-secondary);
      text-transform: uppercase;
      font-size: 0.75rem;
      letter-spacing: 0.05em;
    }

    /* Material Design-inspired */
    .custom-material-input::part(ag-input) {
      border: none;
      border-bottom: 2px solid #e5e7eb;
      border-radius: 0;
      padding: 0.5rem 0;
      background: transparent;
      transition: border-color 0.2s ease;
    }

    .custom-material-input::part(ag-input):focus {
      border-bottom-color: #667eea;
      outline: none;
    }

    .custom-material-input::part(ag-input-label) {
      font-size: 0.875rem;
      color: var(--ag-text-secondary);
      margin-bottom: 0.25rem;
    }

    /* Textarea customization */
    .custom-textarea::part(ag-textarea) {
      border: 2px dashed #9ca3af;
      border-radius: 12px;
      background: #f9fafb;
      padding: 1rem;
      font-family: "Monaco", "Courier New", monospace;
      font-size: 0.875rem;
      line-height: 1.6;
      transition: all 0.3s ease;
    }

    .custom-textarea::part(ag-textarea):focus {
      border-style: solid;
      border-color: #667eea;
      background: white;
    }

    .custom-textarea::part(ag-input-label) {
      font-weight: 700;
      color: var(--ag-text-secondary);
    }
  `;

  constructor() {
    super();
    this.basicValue = '';
    this.password = '';
    this.search = '';
    this.sizeSmall = '';
    this.sizeDefault = '';
    this.sizeLarge = '';
    this.shapeDefault = '';
    this.shapeRounded = '';
    this.shapeCapsule = '';
    this.shapeUnderlined = '';
    this.shapeUnderlinedBg = '';
    this.stateDefault = '';
    this.stateRequired = '';
    this.stateDisabled = 'Disabled value';
    this.stateReadonly = 'Read-only value';
    this.stateInvalid = '';
    this.textareaValue = '';
    this.textareaLarge = '';
    this.addonLeft = '';
    this.addonRight = '';
    this.addonBoth = '';
    this.addonPercent = '';
    this.addonRounded = '';
    this.addonCapsule = '';
    this.addonUnderlined = '';
    this.addonUnderlinedBg = '';
    this.customGradient = '';
    this.customMaterial = '';
    this.customTextarea = '';
    this.labelTop = '';
    this.labelStart = '';
    this.labelEnd = '';
    this.labelBottom = '';
    this.interactiveEmail = '';
    this.lastInputTime = '(none)';
    this.interactiveUsername = '';
    this.confirmedUsername = '';
    this.interactiveFocus = '';
    this.isFocused = false;
    this.focusCount = 0;
    this.interactiveReactive = '';
    this.interactiveTextarea = '';
    this.textareaStatus = 'Ready';
  }

  get wordCount() {
    if (!this.interactiveTextarea.trim()) return 0;
    return this.interactiveTextarea.trim().split(/\s+/).length;
  }

  handleInputEvent(e) {
    this.interactiveEmail = e.target.value;
    const now = new Date();
    this.lastInputTime = now.toLocaleTimeString();
  }

  handleChangeEvent(e) {
    this.interactiveUsername = e.target.value;
    this.confirmedUsername = e.target.value;
  }

  handleFocus() {
    this.isFocused = true;
    this.focusCount++;
  }

  handleBlur() {
    this.isFocused = false;
  }

  handleTextareaInput(e) {
    this.interactiveTextarea = e.target.value;
    this.textareaStatus = 'Typing...';
  }

  handleTextareaChange() {
    this.textareaStatus = 'Changes saved';
  }

  handleTextareaFocus() {
    this.textareaStatus = 'Focused';
  }

  handleTextareaBlur() {
    this.textareaStatus = 'Ready';
  }

  setProgrammatically() {
    this.interactiveReactive = 'Programmatically set!';
  }

  // Render in light DOM to access global utility classes
  createRenderRoot() {
    return this;
  }

  render() {
    return html`
      <section>
        <div class="mbe4">
          <h2>Basic Input</h2>
        </div>
        <div class="stacked mbe4">
          <ag-input
            .value=${this.basicValue}
            @input=${(e) => this.basicValue = e.target.value}
            label="Email"
            type="email"
            placeholder="you@example.com"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.password}
            @input=${(e) => this.password = e.target.value}
            label="Password"
            type="password"
            placeholder="Enter password"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.search}
            @input=${(e) => this.search = e.target.value}
            label="Search"
            type="search"
            placeholder="Search..."
            class="mbe2"
          ></ag-input>
        </div>

        <div class="mbe4">
          <h2>Sizes</h2>
        </div>
        <div class="stacked mbe4">
          <ag-input
            .value=${this.sizeSmall}
            @input=${(e) => this.sizeSmall = e.target.value}
            label="Small Input"
            size="small"
            placeholder="Small size"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.sizeDefault}
            @input=${(e) => this.sizeDefault = e.target.value}
            label="Default Input"
            size="default"
            placeholder="Default size"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.sizeLarge}
            @input=${(e) => this.sizeLarge = e.target.value}
            label="Large Input"
            size="large"
            placeholder="Large size"
            class="mbe2"
          ></ag-input>
        </div>

        <div class="mbe4">
          <h2>Shape Variants</h2>
        </div>
        <div class="stacked mbe4">
          <ag-input
            .value=${this.shapeDefault}
            @input=${(e) => this.shapeDefault = e.target.value}
            label="Default (rectangular)"
            placeholder="Default rectangular"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.shapeRounded}
            @input=${(e) => this.shapeRounded = e.target.value}
            label="Rounded"
            rounded
            placeholder="Rounded corners"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.shapeCapsule}
            @input=${(e) => this.shapeCapsule = e.target.value}
            label="Capsule"
            capsule
            placeholder="Capsule shape"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.shapeUnderlined}
            @input=${(e) => this.shapeUnderlined = e.target.value}
            label="Underlined"
            underlined
            placeholder="Underlined only"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.shapeUnderlinedBg}
            @input=${(e) => this.shapeUnderlinedBg = e.target.value}
            label="Underlined with Background"
            underlined-with-background
            placeholder="Underlined with background"
            class="mbe2"
          ></ag-input>
        </div>

        <div class="mbe4">
          <h2>States</h2>
        </div>
        <div class="stacked mbe4">
          <ag-input
            .value=${this.stateDefault}
            @input=${(e) => this.stateDefault = e.target.value}
            label="Normal"
            placeholder="Normal state"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.stateRequired}
            @input=${(e) => this.stateRequired = e.target.value}
            label="Required"
            required
            placeholder="Required field"
            help-text="This field is required"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.stateDisabled}
            label="Disabled"
            disabled
            placeholder="Disabled input"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.stateReadonly}
            label="Readonly"
            readonly
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.stateInvalid}
            @input=${(e) => this.stateInvalid = e.target.value}
            label="Invalid"
            invalid
            placeholder="Invalid input"
            error-message="This field has an error"
            class="mbe2"
          ></ag-input>
        </div>

        <div class="mbe4">
          <h2>Textarea</h2>
        </div>
        <div class="stacked mbe4">
          <ag-input
            .value=${this.textareaValue}
            @input=${(e) => this.textareaValue = e.target.value}
            label="Comments"
            type="textarea"
            placeholder="Enter your comments..."
            .rows=${4}
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.textareaLarge}
            @input=${(e) => this.textareaLarge = e.target.value}
            label="Description"
            type="textarea"
            placeholder="Enter description..."
            .rows=${6}
            size="large"
            help-text="Provide a detailed description"
            class="mbe2"
          ></ag-input>
        </div>

        <div class="mbe4">
          <h2>With Addons (Automatic Detection)</h2>
          <p style="margin-top: 0.5rem; color: var(--ag-text-secondary); font-size: 0.875rem;">
            Addons are automatically detected when you provide slot content - no props needed!
          </p>
        </div>
        <div class="stacked mbe4">
          <ag-input
            .value=${this.addonLeft}
            @input=${(e) => this.addonLeft = e.target.value}
            label="Website URL"
            placeholder="example.com"
            class="mbe2"
          >
            <svg slot="addon-left" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="var(--ag-primary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <circle cx="12" cy="12" r="10"></circle>
              <line x1="2" y1="12" x2="22" y2="12"></line>
              <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
            </svg>
          </ag-input>

          <ag-input
            .value=${this.addonRight}
            @input=${(e) => this.addonRight = e.target.value}
            label="Price"
            placeholder="0.00"
            class="mbe2"
          >
            <svg slot="addon-right" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="var(--ag-success)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <line x1="12" y1="1" x2="12" y2="23"></line>
              <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
            </svg>
          </ag-input>

          <ag-input
            .value=${this.addonBoth}
            @input=${(e) => this.addonBoth = e.target.value}
            label="Amount"
            placeholder="100"
            class="mbe2"
          >
            <svg slot="addon-left" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="var(--ag-success)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <line x1="12" y1="1" x2="12" y2="23"></line>
              <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
            </svg>
            <span slot="addon-right">.00</span>
          </ag-input>

          <ag-input
            .value=${this.addonPercent}
            @input=${(e) => this.addonPercent = e.target.value}
            label="Discount"
            type="number"
            placeholder="10"
            class="mbe2"
          >
            <svg slot="addon-right" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <line x1="19" y1="5" x2="5" y2="19"></line>
              <circle cx="6.5" cy="6.5" r="2.5"></circle>
              <circle cx="17.5" cy="17.5" r="2.5"></circle>
            </svg>
          </ag-input>
        </div>

        <div class="mbe4">
          <h2>Addons with Style Variants</h2>
          <p style="margin-top: 0.5rem; color: var(--ag-text-secondary); font-size: 0.875rem;">
            Addons work seamlessly with all input styling variants
          </p>
        </div>
        <div class="stacked mbe4">
          <ag-input
            .value=${this.addonRounded}
            @input=${(e) => this.addonRounded = e.target.value}
            label="Rounded with Addons"
            type="number"
            placeholder="0.00"
            rounded
            class="mbe2"
          >
            <svg slot="addon-left" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="var(--ag-primary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <line x1="12" y1="1" x2="12" y2="23"></line>
              <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
            </svg>
            <span slot="addon-right" style="font-weight: 600;">USD</span>
          </ag-input>

          <ag-input
            .value=${this.addonCapsule}
            @input=${(e) => this.addonCapsule = e.target.value}
            label="Capsule with Addon"
            type="search"
            placeholder="Find products..."
            capsule
            class="mbe2"
          >
            <svg slot="addon-left" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="var(--ag-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <circle cx="11" cy="11" r="8"></circle>
              <path d="m21 21-4.35-4.35"></path>
            </svg>
          </ag-input>

          <ag-input
            .value=${this.addonUnderlined}
            @input=${(e) => this.addonUnderlined = e.target.value}
            label="Underlined with Addon"
            type="number"
            placeholder="10"
            underlined
            class="mbe2"
          >
            <svg slot="addon-right" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <line x1="19" y1="5" x2="5" y2="19"></line>
              <circle cx="6.5" cy="6.5" r="2.5"></circle>
              <circle cx="17.5" cy="17.5" r="2.5"></circle>
            </svg>
          </ag-input>

          <ag-input
            .value=${this.addonUnderlinedBg}
            @input=${(e) => this.addonUnderlinedBg = e.target.value}
            label="Underlined with Background"
            type="text"
            placeholder="Enter username"
            underlined-with-background
            class="mbe2"
          >
            <svg slot="addon-left" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="var(--ag-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
              <circle cx="12" cy="7" r="4"></circle>
            </svg>
          </ag-input>
        </div>

        <div class="mbe4">
          <h2>Interactive Event Handling</h2>
          <p class="mbe2" style="color: var(--ag-text-secondary); font-size: 0.875rem;">
            Demonstrates event handling with @input, @change, @focus, @blur
          </p>
        </div>
        <div class="stacked mbe4">
          <div>
            <ag-input
              .value=${this.interactiveEmail}
              @input=${this.handleInputEvent}
              label="Email (@input event)"
              type="email"
              placeholder="you@example.com"
              class="mbe2"
            ></ag-input>
            <p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--ag-text-secondary);">
              Character count: <strong>${this.interactiveEmail.length}</strong> | Last input: <strong>${this.lastInputTime}</strong>
            </p>
          </div>

          <div>
            <ag-input
              .value=${this.interactiveUsername}
              @change=${this.handleChangeEvent}
              label="Username (@change event)"
              placeholder="Enter username"
              class="mbe2"
            ></ag-input>
            <p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--ag-text-secondary);">
              Last confirmed value: <strong>${this.confirmedUsername || '(none)'}</strong>
            </p>
          </div>

          <div>
            <ag-input
              .value=${this.interactiveFocus}
              @input=${(e) => this.interactiveFocus = e.target.value}
              @focus=${this.handleFocus}
              @blur=${this.handleBlur}
              label="Focus Tracking (@focus/@blur)"
              placeholder="Click to focus"
              class="mbe2"
            ></ag-input>
            <p style="margin-top: 0.5rem; font-size: 0.875rem;">
              Status: <strong style="color: ${this.isFocused ? 'var(--ag-success)' : 'var(--ag-text-secondary)'}">
                ${this.isFocused ? 'Focused' : 'Not focused'}
              </strong>
              ${this.focusCount > 0 ? `(focused ${this.focusCount} time${this.focusCount > 1 ? 's' : ''})` : ''}
            </p>
          </div>

          <div>
            <ag-input
              .value=${this.interactiveReactive}
              @input=${(e) => this.interactiveReactive = e.target.value}
              label="Two-way Binding"
              placeholder="Type here..."
              class="mbe2"
            ></ag-input>
            <p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--ag-text-secondary);">
              Current value: <strong>${this.interactiveReactive || '(empty)'}</strong>
            </p>
            <button
              @click=${this.setProgrammatically}
              style="margin-top: 0.5rem; padding: 0.25rem 0.75rem; border: 1px solid var(--ag-border); border-radius: var(--ag-radius-sm); cursor: pointer;"
            >
              Set value programmatically
            </button>
          </div>

          <div>
            <ag-input
              .value=${this.interactiveTextarea}
              @input=${this.handleTextareaInput}
              @change=${this.handleTextareaChange}
              @focus=${this.handleTextareaFocus}
              @blur=${this.handleTextareaBlur}
              label="Textarea with Events"
              type="textarea"
              .rows=${3}
              placeholder="Try typing, then click outside..."
              class="mbe2"
            ></ag-input>
            <div style="margin-top: 0.5rem; font-size: 0.875rem; padding: 0.5rem; background: var(--ag-background-secondary); border-radius: 4px;">
              <div>Words: <strong>${this.wordCount}</strong></div>
              <div>Characters: <strong>${this.interactiveTextarea.length}</strong></div>
              <div>Status: <strong>${this.textareaStatus}</strong></div>
            </div>
          </div>
        </div>

        <div class="mbe4">
          <h2>Label Positioning</h2>
          <p style="margin-top: 0.5rem; color: var(--ag-text-secondary); font-size: 0.875rem;">
            Control label placement with the <code>labelPosition</code> attribute: <code>top</code> (default), <code>start</code>, <code>end</code>, or <code>bottom</code>
          </p>
        </div>
        <div class="stacked mbe4">
          <ag-input
            .value=${this.labelTop}
            @input=${(e) => this.labelTop = e.target.value}
            label="Top Position (Default)"
            label-position="top"
            placeholder="Label above input"
            help-text="Default vertical layout - best for mobile"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.labelStart}
            @input=${(e) => this.labelStart = e.target.value}
            label="Name:"
            label-position="start"
            placeholder="Enter name"
            help-text="Horizontal layout - label before input"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.labelEnd}
            @input=${(e) => this.labelEnd = e.target.value}
            label="Email:"
            label-position="end"
            placeholder="you@example.com"
            help-text="Horizontal layout - label after input"
            class="mbe2"
          ></ag-input>
          <ag-input
            .value=${this.labelBottom}
            @input=${(e) => this.labelBottom = e.target.value}
            label="Bottom Position"
            label-position="bottom"
            placeholder="Enter value"
            help-text="Vertical layout - label below input"
            class="mbe2"
          ></ag-input>
        </div>

        <div class="mbe4">
          <h2>CSS Shadow Parts Customization</h2>
          <p style="margin-top: 0.5rem; margin-bottom: 1rem; color: var(--vp-c-text-2);">
            Input can be customized using CSS Shadow Parts:
            <code>::part(ag-input)</code>,
            <code>::part(ag-textarea)</code>,
            <code>::part(ag-input-label)</code>,
            <code>::part(ag-input-error)</code>,
            <code>::part(ag-input-help)</code>
          </p>
        </div>
        <div class="stacked mbe4">
          <ag-input
            .value=${this.customGradient}
            @input=${(e) => this.customGradient = e.target.value}
            class="custom-gradient-input mbe2"
            label="Modern Gradient Border"
            placeholder="you@example.com"
            type="email"
          ></ag-input>

          <ag-input
            .value=${this.customMaterial}
            @input=${(e) => this.customMaterial = e.target.value}
            class="custom-material-input mbe2"
            label="Material Design Style"
            placeholder="John Doe"
          ></ag-input>

          <ag-input
            .value=${this.customTextarea}
            @input=${(e) => this.customTextarea = e.target.value}
            class="custom-textarea mbe2"
            label="Styled Textarea"
            type="textarea"
            .rows=${4}
            placeholder="Paste your code here..."
            help-text="Monospace font with dashed border"
          ></ag-input>
        </div>
      </section>
    `;
  }
}

// Register the custom element
customElements.define('input-lit-examples', InputLitExamples);

Interactive Preview: Click the "Open in StackBlitz" button below to see this example running live in an interactive playground.

View React Code
import { useState, useMemo } from "react";
import { ReactInput } from "agnosticui-core/input/react";
import { Globe, DollarSign, Percent, Search, User2 } from "lucide-react";

export default function InputReactExamples() {
  // Basic
  const [basicValue, setBasicValue] = useState("");
  const [password, setPassword] = useState("");
  const [search, setSearch] = useState("");

  // Sizes
  const [sizeSmall, setSizeSmall] = useState("");
  const [sizeDefault, setSizeDefault] = useState("");
  const [sizeLarge, setSizeLarge] = useState("");

  // Shapes
  const [shapeDefault, setShapeDefault] = useState("");
  const [shapeRounded, setShapeRounded] = useState("");
  const [shapeCapsule, setShapeCapsule] = useState("");
  const [shapeUnderlined, setShapeUnderlined] = useState("");
  const [shapeUnderlinedBg, setShapeUnderlinedBg] = useState("");

  // States
  const [stateDefault, setStateDefault] = useState("");
  const [stateRequired, setStateRequired] = useState("");
  const [stateDisabled] = useState("Disabled value");
  const [stateReadonly] = useState("Read-only value");
  const [stateInvalid, setStateInvalid] = useState("");

  // Textarea
  const [textareaValue, setTextareaValue] = useState("");
  const [textareaLarge, setTextareaLarge] = useState("");

  // Addons
  const [addonLeft, setAddonLeft] = useState("");
  const [addonRight, setAddonRight] = useState("");
  const [addonBoth, setAddonBoth] = useState("");
  const [addonPercent, setAddonPercent] = useState("");

  // Addons with style variants
  const [addonRounded, setAddonRounded] = useState("");
  const [addonCapsule, setAddonCapsule] = useState("");
  const [addonUnderlined, setAddonUnderlined] = useState("");
  const [addonUnderlinedBg, setAddonUnderlinedBg] = useState("");

  // Custom styles
  const [customGradient, setCustomGradient] = useState("");
  const [customMaterial, setCustomMaterial] = useState("");
  const [customTextarea, setCustomTextarea] = useState("");

  // Label positioning
  const [labelTop, setLabelTop] = useState("");
  const [labelStart, setLabelStart] = useState("");
  const [labelEnd, setLabelEnd] = useState("");
  const [labelBottom, setLabelBottom] = useState("");

  // Interactive event handling
  const [interactiveEmail, setInteractiveEmail] = useState("");
  const [lastInputTime, setLastInputTime] = useState("(none)");
  const [interactiveUsername, setInteractiveUsername] = useState("");
  const [confirmedUsername, setConfirmedUsername] = useState("");
  const [interactiveFocus, setInteractiveFocus] = useState("");
  const [isFocused, setIsFocused] = useState(false);
  const [focusCount, setFocusCount] = useState(0);
  const [interactiveReactive, setInteractiveReactive] = useState("");
  const [interactiveTextarea, setInteractiveTextarea] = useState("");
  const [textareaStatus, setTextareaStatus] = useState("Ready");

  // Computed word count
  const wordCount = useMemo(() => {
    if (!interactiveTextarea.trim()) return 0;
    return interactiveTextarea.trim().split(/\s+/).length;
  }, [interactiveTextarea]);

  // Event handlers
  const handleInputEvent = (event) => {
    const now = new Date();
    setLastInputTime(now.toLocaleTimeString());
    console.log("Input event:", event);
  };

  const handleChangeEvent = (event) => {
    setConfirmedUsername(interactiveUsername);
    console.log("Change event:", event);
  };

  const handleFocus = (event) => {
    setIsFocused(true);
    setFocusCount((prev) => prev + 1);
    console.log("Focus event:", event);
  };

  const handleBlur = (event) => {
    setIsFocused(false);
    console.log("Blur event:", event);
  };

  const handleTextareaInput = (event) => {
    setTextareaStatus("Typing...");
    console.log("Textarea input event:", event);
  };

  const handleTextareaChange = (event) => {
    setTextareaStatus("Changes saved");
    console.log("Textarea change event:", event);
  };

  const handleTextareaFocus = (event) => {
    setTextareaStatus("Focused");
    console.log("Textarea focus event:", event);
  };

  const handleTextareaBlur = (event) => {
    setTextareaStatus("Ready");
    console.log("Textarea blur event:", event);
  };

  return (
    <section>
      <style>{`
        /* Modern gradient input style */
        .custom-gradient-input::part(ag-input) {
          border: 2px solid transparent;
          background: linear-gradient(white, white) padding-box,
            linear-gradient(135deg, #667eea 0%, #764ba2 100%) border-box;
          border-radius: 12px;
          padding: 0.75rem 1rem;
          font-weight: 500;
          transition: all 0.3s ease;
        }

        .custom-gradient-input::part(ag-input):focus {
          box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
          transform: translateY(-1px);
        }

        .custom-gradient-input::part(ag-input-label) {
          font-weight: 700;
          color: var(--ag-text-secondary);
          text-transform: uppercase;
          font-size: 0.75rem;
          letter-spacing: 0.05em;
        }

        /* Material Design-inspired */
        .custom-material-input::part(ag-input) {
          border: none;
          border-bottom: 2px solid #e5e7eb;
          border-radius: 0;
          padding: 0.5rem 0;
          background: transparent;
          transition: border-color 0.2s ease;
        }

        .custom-material-input::part(ag-input):focus {
          border-bottom-color: #667eea;
          outline: none;
        }

        .custom-material-input::part(ag-input-label) {
          font-size: 0.875rem;
          color: var(--ag-text-secondary);
          margin-bottom: 0.25rem;
        }

        /* Textarea customization */
        .custom-textarea::part(ag-textarea) {
          border: 2px dashed #9ca3af;
          border-radius: 12px;
          background: #f9fafb;
          padding: 1rem;
          font-family: "Monaco", "Courier New", monospace;
          font-size: 0.875rem;
          line-height: 1.6;
          transition: all 0.3s ease;
        }

        .custom-textarea::part(ag-textarea):focus {
          border-style: solid;
          border-color: #667eea;
          background: white;
        }

        .custom-textarea::part(ag-input-label) {
          font-weight: 700;
          color: var(--ag-text-secondary);
        }
      `}</style>

      <div className="mbe4">
        <h2>Basic Input</h2>
      </div>
      <div className="stacked mbe4">
        <ReactInput
          value={basicValue}
          onChange={(e) => setBasicValue(e.target.value)}
          label="Email"
          type="email"
          placeholder="you@example.com"
          className="mbe2"
        />
        <ReactInput
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          label="Password"
          type="password"
          placeholder="Enter password"
          className="mbe2"
        />
        <ReactInput
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          label="Search"
          type="search"
          placeholder="Search..."
          className="mbe2"
        />
      </div>

      <div className="mbe4">
        <h2>Sizes</h2>
      </div>
      <div className="stacked mbe4">
        <ReactInput
          value={sizeSmall}
          onChange={(e) => setSizeSmall(e.target.value)}
          label="Small Input"
          size="small"
          placeholder="Small size"
          className="mbe2"
        />
        <ReactInput
          value={sizeDefault}
          onChange={(e) => setSizeDefault(e.target.value)}
          label="Default Input"
          size="default"
          placeholder="Default size"
          className="mbe2"
        />
        <ReactInput
          value={sizeLarge}
          onChange={(e) => setSizeLarge(e.target.value)}
          label="Large Input"
          size="large"
          placeholder="Large size"
          className="mbe2"
        />
      </div>

      <div className="mbe4">
        <h2>Shape Variants</h2>
      </div>
      <div className="stacked mbe4">
        <ReactInput
          value={shapeDefault}
          onChange={(e) => setShapeDefault(e.target.value)}
          label="Default (rectangular)"
          placeholder="Default rectangular"
          className="mbe2"
        />
        <ReactInput
          value={shapeRounded}
          onChange={(e) => setShapeRounded(e.target.value)}
          label="Rounded"
          rounded={true}
          placeholder="Rounded corners"
          className="mbe2"
        />
        <ReactInput
          value={shapeCapsule}
          onChange={(e) => setShapeCapsule(e.target.value)}
          label="Capsule"
          capsule={true}
          placeholder="Capsule shape"
          className="mbe2"
        />
        <ReactInput
          value={shapeUnderlined}
          onChange={(e) => setShapeUnderlined(e.target.value)}
          label="Underlined"
          underlined={true}
          placeholder="Underlined only"
          className="mbe2"
        />
        <ReactInput
          value={shapeUnderlinedBg}
          onChange={(e) => setShapeUnderlinedBg(e.target.value)}
          label="Underlined with Background"
          underlinedWithBackground={true}
          placeholder="Underlined with background"
          className="mbe2"
        />
      </div>

      <div className="mbe4">
        <h2>States</h2>
      </div>
      <div className="stacked mbe4">
        <ReactInput
          value={stateDefault}
          onChange={(e) => setStateDefault(e.target.value)}
          label="Normal"
          placeholder="Normal state"
          className="mbe2"
        />
        <ReactInput
          value={stateRequired}
          onChange={(e) => setStateRequired(e.target.value)}
          label="Required"
          required={true}
          placeholder="Required field"
          helpText="This field is required"
          className="mbe2"
        />
        <ReactInput
          value={stateDisabled}
          label="Disabled"
          disabled={true}
          placeholder="Disabled input"
          className="mbe2"
        />
        <ReactInput
          value={stateReadonly}
          label="Readonly"
          readonly={true}
          className="mbe2"
        />
        <ReactInput
          value={stateInvalid}
          onChange={(e) => setStateInvalid(e.target.value)}
          label="Invalid"
          invalid={true}
          placeholder="Invalid input"
          errorMessage="This field has an error"
          className="mbe2"
        />
      </div>

      <div className="mbe4">
        <h2>Textarea</h2>
      </div>
      <div className="stacked mbe4">
        <ReactInput
          value={textareaValue}
          onChange={(e) => setTextareaValue(e.target.value)}
          label="Comments"
          type="textarea"
          placeholder="Enter your comments..."
          rows={4}
          className="mbe2"
        />
        <ReactInput
          value={textareaLarge}
          onChange={(e) => setTextareaLarge(e.target.value)}
          label="Description"
          type="textarea"
          placeholder="Enter description..."
          rows={6}
          size="large"
          helpText="Provide a detailed description"
          className="mbe2"
        />
      </div>

      <div className="mbe4">
        <h2>With Addons (Automatic Detection)</h2>
        <p
          style={{
            marginTop: "0.5rem",
            color: "var(--ag-text-secondary)",
            fontSize: "0.875rem",
          }}
        >
          Addons are automatically detected when you provide slot content - no
          props needed!
        </p>
      </div>
      <div className="stacked mbe4">
        <ReactInput
          value={addonLeft}
          onChange={(e) => setAddonLeft(e.target.value)}
          label="Website URL"
          placeholder="example.com"
          className="mbe2"
        >
          <div slot="addon-left">
            <Globe size={18} color="var(--ag-primary)" />
          </div>
        </ReactInput>

        <ReactInput
          value={addonRight}
          onChange={(e) => setAddonRight(e.target.value)}
          label="Price"
          placeholder="0.00"
          className="mbe2"
        >
          <div slot="addon-right">
            <DollarSign size={18} color="var(--ag-success)" />
          </div>
        </ReactInput>

        <ReactInput
          value={addonBoth}
          onChange={(e) => setAddonBoth(e.target.value)}
          label="Amount"
          placeholder="100"
          className="mbe2"
        >
          <div slot="addon-left">
            <DollarSign size={18} color="var(--ag-success)" />
          </div>
          <div slot="addon-right">
            <span>.00</span>
          </div>
        </ReactInput>

        <ReactInput
          value={addonPercent}
          onChange={(e) => setAddonPercent(e.target.value)}
          label="Discount"
          type="number"
          placeholder="10"
          className="mbe2"
        >
          <div slot="addon-right">
            <Percent size={18} />
          </div>
        </ReactInput>
      </div>

      <div className="mbe4">
        <h2>Addons with Style Variants</h2>
        <p
          style={{
            marginTop: "0.5rem",
            color: "var(--ag-text-secondary)",
            fontSize: "0.875rem",
          }}
        >
          Addons work seamlessly with all input styling variants
        </p>
      </div>
      <div className="stacked mbe4">
        <ReactInput
          value={addonRounded}
          onChange={(e) => setAddonRounded(e.target.value)}
          label="Rounded with Addons"
          type="number"
          placeholder="0.00"
          rounded={true}
          className="mbe2"
        >
          <div slot="addon-left">
            <DollarSign size={18} color="var(--ag-primary)" />
          </div>
          <div slot="addon-right">
            <span style={{ fontWeight: 600 }}>USD</span>
          </div>
        </ReactInput>

        <ReactInput
          value={addonCapsule}
          onChange={(e) => setAddonCapsule(e.target.value)}
          label="Capsule with Addon"
          type="search"
          placeholder="Find products..."
          capsule={true}
          className="mbe2"
        >
          <div slot="addon-left">
            <Search size={18} color="var(--ag-secondary)" />
          </div>
        </ReactInput>

        <ReactInput
          value={addonUnderlined}
          onChange={(e) => setAddonUnderlined(e.target.value)}
          label="Underlined with Addon"
          type="number"
          placeholder="10"
          underlined={true}
          className="mbe2"
        >
          <div slot="addon-right">
            <Percent size={18} />
          </div>
        </ReactInput>

        <ReactInput
          value={addonUnderlinedBg}
          onChange={(e) => setAddonUnderlinedBg(e.target.value)}
          label="Underlined with Background"
          type="text"
          placeholder="Enter username"
          underlinedWithBackground={true}
          className="mbe2"
        >
          <div slot="addon-left">
            <User2 size={18} color="var(--ag-secondary)" />
          </div>
        </ReactInput>
      </div>

      <div className="mbe4">
        <h2>Interactive Event Handling</h2>
        <p
          className="mbe2"
          style={{ color: "var(--ag-text-secondary)", fontSize: "0.875rem" }}
        >
          Demonstrates event handling with onInput, onChange, onFocus, onBlur,
          and controlled value
        </p>
      </div>
      <div className="stacked mbe4">
        {/* Pattern 1: onInput event for real-time tracking */}
        <div>
          <ReactInput
            value={interactiveEmail}
            onChange={(e) => setInteractiveEmail(e.target.value)}
            onInput={handleInputEvent}
            label="Email (onInput event)"
            type="email"
            placeholder="you@example.com"
            className="mbe2"
          />
          <p
            style={{
              marginTop: "0.5rem",
              fontSize: "0.875rem",
              color: "var(--ag-text-secondary)",
            }}
          >
            Character count: <strong>{interactiveEmail.length}</strong> | Last
            input: <strong>{lastInputTime}</strong>
          </p>
        </div>

        {/* Pattern 2: onChange event for completed changes */}
        <div>
          <ReactInput
            value={interactiveUsername}
            onChange={(e) => {
              setInteractiveUsername(e.target.value);
              handleChangeEvent(e);
            }}
            label="Username (onChange event)"
            placeholder="Enter username"
            className="mbe2"
          />
          <p
            style={{
              marginTop: "0.5rem",
              fontSize: "0.875rem",
              color: "var(--ag-text-secondary)",
            }}
          >
            Last confirmed value:{" "}
            <strong>{confirmedUsername || "(none)"}</strong>
          </p>
        </div>

        {/* Pattern 3: Focus and Blur events */}
        <div>
          <ReactInput
            value={interactiveFocus}
            onChange={(e) => setInteractiveFocus(e.target.value)}
            onFocus={handleFocus}
            onBlur={handleBlur}
            label="Focus Tracking (onFocus/onBlur)"
            placeholder="Click to focus"
            className="mbe2"
          />
          <p style={{ marginTop: "0.5rem", fontSize: "0.875rem" }}>
            Status:{" "}
            <strong
              style={{
                color: isFocused
                  ? "var(--ag-success)"
                  : "var(--ag-text-secondary)",
              }}
            >
              {isFocused ? "Focused" : "Not focused"}
            </strong>
            {focusCount > 0
              ? ` (focused ${focusCount} time${focusCount > 1 ? "s" : ""})`
              : ""}
          </p>
        </div>

        {/* Pattern 4: Controlled value with reactive updates */}
        <div>
          <ReactInput
            value={interactiveReactive}
            onChange={(e) => setInteractiveReactive(e.target.value)}
            label="Two-way Binding (controlled value)"
            placeholder="Type here..."
            className="mbe2"
          />
          <p
            style={{
              marginTop: "0.5rem",
              fontSize: "0.875rem",
              color: "var(--ag-text-secondary)",
            }}
          >
            Current value: <strong>{interactiveReactive || "(empty)"}</strong>
          </p>
          <button
            onClick={() => setInteractiveReactive("Programmatically set!")}
            style={{
              marginTop: "0.5rem",
              padding: "0.25rem 0.75rem",
              border: "1px solid var(--ag-border)",
              borderRadius: "var(--ag-radius-sm)",
              cursor: "pointer",
            }}
          >
            Set value programmatically
          </button>
        </div>

        {/* Pattern 5: Textarea with all events */}
        <div>
          <ReactInput
            value={interactiveTextarea}
            onChange={(e) => setInteractiveTextarea(e.target.value)}
            onInput={handleTextareaInput}
            onFocus={handleTextareaFocus}
            onBlur={handleTextareaBlur}
            label="Textarea with Events"
            type="textarea"
            rows={3}
            placeholder="Try typing, then click outside..."
            className="mbe2"
          />
          <div
            style={{
              marginTop: "0.5rem",
              fontSize: "0.875rem",
              padding: "0.5rem",
              background: "var(--ag-background-secondary)",
              borderRadius: "4px",
            }}
          >
            <div>
              Words: <strong>{wordCount}</strong>
            </div>
            <div>
              Characters: <strong>{interactiveTextarea.length}</strong>
            </div>
            <div>
              Status: <strong>{textareaStatus}</strong>
            </div>
          </div>
        </div>
      </div>

      <div className="mbe4">
        <h2>Label Positioning</h2>
        <p
          style={{
            marginTop: "0.5rem",
            color: "var(--ag-text-secondary)",
            fontSize: "0.875rem",
          }}
        >
          Control label placement with the <code>labelPosition</code> prop:{" "}
          <code>top</code> (default), <code>start</code>, <code>end</code>, or{" "}
          <code>bottom</code>
        </p>
      </div>
      <div className="stacked mbe4">
        <ReactInput
          value={labelTop}
          onChange={(e) => setLabelTop(e.target.value)}
          label="Top Position (Default)"
          labelPosition="top"
          placeholder="Label above input"
          helpText="Default vertical layout - best for mobile"
          className="mbe2"
        />
        <ReactInput
          value={labelStart}
          onChange={(e) => setLabelStart(e.target.value)}
          label="Name:"
          labelPosition="start"
          placeholder="Enter name"
          helpText="Horizontal layout - label before input"
          className="mbe2"
        />
        <ReactInput
          value={labelEnd}
          onChange={(e) => setLabelEnd(e.target.value)}
          label="Email:"
          labelPosition="end"
          placeholder="you@example.com"
          helpText="Horizontal layout - label after input"
          className="mbe2"
        />
        <ReactInput
          value={labelBottom}
          onChange={(e) => setLabelBottom(e.target.value)}
          label="Bottom Position"
          labelPosition="bottom"
          placeholder="Enter value"
          helpText="Vertical layout - label below input"
          className="mbe2"
        />
      </div>

      <div className="mbe4">
        <h2>CSS Shadow Parts Customization</h2>
        <p
          style={{
            marginTop: "0.5rem",
            marginBottom: "1rem",
            color: "var(--vp-c-text-2)",
          }}
        >
          Input can be customized using CSS Shadow Parts:{" "}
          <code>::part(ag-input)</code>, <code>::part(ag-textarea)</code>,{" "}
          <code>::part(ag-input-label)</code>,{" "}
          <code>::part(ag-input-error)</code>,{" "}
          <code>::part(ag-input-help)</code>
        </p>
      </div>
      <div className="stacked mbe4">
        <ReactInput
          value={customGradient}
          onChange={(e) => setCustomGradient(e.target.value)}
          className="custom-gradient-input mbe2"
          label="Modern Gradient Border"
          placeholder="you@example.com"
          type="email"
        />

        <ReactInput
          value={customMaterial}
          onChange={(e) => setCustomMaterial(e.target.value)}
          className="custom-material-input mbe2"
          label="Material Design Style"
          placeholder="John Doe"
        />

        <ReactInput
          value={customTextarea}
          onChange={(e) => setCustomTextarea(e.target.value)}
          className="custom-textarea mbe2"
          label="Styled Textarea"
          type="textarea"
          rows={4}
          placeholder="Paste your code here..."
          helpText="Monospace font with dashed border"
        />
      </div>
    </section>
  );
}
Open in StackBlitz

Usage

TIP

The framework examples below import AgnosticUI as an npm package. Alternatively, you can use the CLI for complete control, AI/LLM visibility, and full code ownership:

bash
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add Input

The CLI copies source code directly into your project, giving you full visibility and control. After running npx ag add, you'll receive exact import instructions.

Vue
vue
<template>
  <section>
    <VueInput
      v-model:value="email"
      label="Email"
      type="email"
      placeholder="you@example.com"
    />

    <VueInput
      v-model:value="username"
      label="Username"
      :required="true"
      :invalid="isInvalid"
      error-message="Username is required"
      help-text="Choose a unique username"
    />

    <VueInput
      v-model:value="message"
      label="Message"
      type="textarea"
      :rows="4"
      placeholder="Enter your message..."
    />

    <VueInput v-model:value="price" label="Price">
      <template #addon-left>
        <DollarSign :size="18" />
      </template>
    </VueInput>

    <VueInput
      v-model:value="small"
      label="Small Input"
      size="small"
      placeholder="Small size"
    />

    <VueInput
      v-model="rounded"
      label="Rounded"
      :rounded="true"
      placeholder="Rounded corners"
    />

    <VueInput
      v-model:value="disabled"
      label="Disabled"
      :disabled="true"
      value="Cannot edit"
    />
  </section>
</template>

<script>
import VueInput from "agnosticui-core/input/vue";
import { DollarSign } from "lucide-vue-next";

export default {
  components: {
    VueInput,
    DollarSign,
  },
  data() {
    return {
      email: "",
      username: "",
      isInvalid: false,
      message: "",
      price: "",
      small: "",
      rounded: "",
      disabled: "Cannot edit",
    };
  },
};
</script>
React
tsx
import { useState } from "react";
import { ReactInput } from "agnosticui-core/input/react";
import { ReactButton } from "agnosticui-core/button/react";

export default function InputExample() {
  const [email, setEmail] = useState("");
  const [username, setUsername] = useState("");
  const [isInvalid, setIsInvalid] = useState(false);
  const [message, setMessage] = useState("");
  const [price, setPrice] = useState("");
  const [small, setSmall] = useState("");
  const [rounded, setRounded] = useState("");

  return (
    <section>
      <ReactInput
        value={email}
        onChange={(e) => setEmail((e.target as HTMLInputElement).value)}
        label="Email"
        type="email"
        placeholder="you@example.com"
      />

      <ReactInput
        value={username}
        onChange={(e) => setUsername((e.target as HTMLInputElement).value)}
        label="Username"
        rounded
        required
        invalid={isInvalid}
        errorMessage="Username is required"
        helpText="Choose a unique username"
      />
      <ReactButton
        size="xl"
        shape="rounded"
        variant="monochrome"
        onClick={() => setIsInvalid(!isInvalid)}
      >
        Toggle Invalid
      </ReactButton>

      <ReactInput
        value={message}
        onChange={(e) => setMessage(e.target as HTMLInputElement).value)}
        label="Message"
        type="textarea"
        rows={4}
        placeholder="Enter your message..."
      />

      <ReactInput
        value={price}
        onChange={(e) => setPrice(e.target as HTMLInputElement).value)}
        label="Price"
      >
        <span slot="addon-left">$</span>
      </ReactInput>

      <ReactInput
        value={small}
        onChange={(e) => setSmall(e.target as HTMLInputElement).value)}
        label="Small Input"
        size="small"
        placeholder="Small size"
      />

      <ReactInput
        value={rounded}
        onChange={(e) => setRounded(e.target as HTMLInputElement).value)}
        label="Rounded"
        rounded
        placeholder="Rounded corners"
      />

      <ReactInput value="Cannot edit" label="Disabled" disabled />
    </section>
  );
}
Lit (Web Components)
typescript
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators.js";
import "agnosticui-core/input";

@customElement("input-example")
export class InputExample extends LitElement {
  static styles = css`
    :host {
      display: block;
    }
    section {
      display: flex;
      flex-direction: column;
      gap: 1rem;
    }
  `;

  firstUpdated() {
    // Set up event listeners for inputs in the shadow DOM
    const emailInput = this.shadowRoot?.querySelector("#email-input");
    const usernameInput = this.shadowRoot?.querySelector(
      "#username-input"
    ) as any;

    emailInput?.addEventListener("input", (e: Event) => {
      const target = e.target as HTMLInputElement;
      console.log("Email:", target.value);
    });

    usernameInput?.addEventListener("blur", (e: Event) => {
      const target = e.target as HTMLInputElement;
      if (!target.value) {
        usernameInput.invalid = true;
      } else {
        usernameInput.invalid = false;
      }
    });
  }

  render() {
    return html`
      <section>
        <ag-input
          id="email-input"
          label="Email"
          type="email"
          placeholder="you@example.com"
        ></ag-input>

        <ag-input
          id="username-input"
          label="Username"
          required
          error-message="Username is required"
          help-text="Choose a unique username"
        ></ag-input>

        <ag-input
          id="message-input"
          label="Message"
          type="textarea"
          rows="4"
          placeholder="Enter your message..."
        ></ag-input>

        <ag-input id="price-input" label="Price">
          <span slot="addon-left">$</span>
        </ag-input>

        <ag-input
          label="Small Input"
          size="small"
          placeholder="Small size"
        ></ag-input>

        <ag-input
          label="Rounded"
          rounded
          placeholder="Rounded corners"
        ></ag-input>

        <ag-input label="Disabled" disabled value="Cannot edit"></ag-input>
      </section>
    `;
  }
}

Note: When using input components within a custom element's shadow DOM, set up event listeners in the component's lifecycle (e.g., firstUpdated()) rather than using DOMContentLoaded, as document.querySelector() cannot access elements inside shadow DOM. Use this.shadowRoot.querySelector() instead.

Props

PropTypeDefaultDescription
modelValuestring''The input value (use with v-model for two-way binding)
labelstring''Label text for the input. Best practice is to always provide a visible label.
typestring'text'Input type (text, email, password, search, tel, url, number, date, time, color, or 'textarea' for textarea element)
placeholderstring''Placeholder text shown when input is empty
size'small' | 'default' | 'large''default'Size variant of the input
namestring''Name attribute for form submission
requiredbooleanfalseMarks the field as required. Displays an asterisk (*) after the label
disabledbooleanfalseDisables the input, preventing interaction
readonlybooleanfalseMakes the input read-only but still focusable
invalidbooleanfalseMarks the input as invalid. Changes border color to red and sets aria-invalid
helpTextstring''Helper text displayed below the input
errorMessagestring''Error message displayed when invalid. Linked via aria-describedby
roundedbooleanfalseApplies rounded corners (border-radius: md)
capsulebooleanfalseApplies capsule shape (fully rounded ends)
underlinedbooleanfalseShows only bottom border (underlined style)
underlinedWithBackgroundbooleanfalseUnderlined style with subtle background color
inlinebooleanfalseChanges display to inline-block for inline layouts
labelPosition'top' | 'start' | 'end' | 'bottom''top'Controls label placement. top/bottom for vertical layout, start/end for horizontal (respects RTL)
labelHiddenbooleanfalseVisually hides the label while keeping it accessible to screen readers
noLabelbooleanfalseCompletely removes the label element. Use with ariaLabel for accessibility
ariaLabelstring''ARIA label for accessibility when label is not visible
hasLeftAddonbooleanfalseDeprecated: Addons are now automatically detected. Simply use the addon-left slot.
hasRightAddonbooleanfalseDeprecated: Addons are now automatically detected. Simply use the addon-right slot.
rowsnumber4Number of rows for textarea (only applies when type="textarea")
colsnumber50Number of columns for textarea (only applies when type="textarea")

Events

The Input component follows AgnosticUI v2 event conventions for native events. All events work consistently across Lit, React, and Vue:

EventPayloadDescription
clickMouseEventNative click event on the input
inputInputEventNative input event, fired on every keystroke
changeEventNative change event, fired when input loses focus after value changed
focusFocusEventFired when input receives focus (re-dispatched from host for cross-shadow-DOM access)
blurFocusEventFired when input loses focus (re-dispatched from host for cross-shadow-DOM access)

Framework-Specific Event Usage

Vue:

  • Use v-model:value for two-way binding
  • Listen to events with @event-name syntax (e.g., @input, @change, @focus, @blur)
  • The update:value emit is automatically fired on input for v-model support
vue
<VueInput
  v-model:value="email"
  label="Email"
  @input="handleInput"
  @change="handleChange"
  @focus="handleFocus"
  @blur="handleBlur"
/>

React:

  • All native events work automatically through callback props
  • Use onInput, onChange, onFocus, onBlur, onClick
tsx
<ReactInput
  label="Email"
  onInput={(e) => console.log("input:", e.target.value)}
  onChange={(e) => console.log("change:", e.target.value)}
  onFocus={(e) => console.log("focused")}
  onBlur={(e) => console.log("blurred")}
/>

Lit/Web Components:

  • Use both addEventListener and callback properties
  • Focus and blur events bubble through shadow DOM
js
const input = document.querySelector("ag-input");
input.addEventListener("input", (e) => console.log(e.target.value));
input.addEventListener("focus", (e) => console.log("focused"));

input.onInput = (e) => console.log(e.target.value);
input.onFocus = (e) => console.log("focused");

Slots

SlotDescription
addon-leftContent to display on the left side of the input (automatically detected when provided)
addon-rightContent to display on the right side of the input (automatically detected when provided)

Accessibility

The Input component follows BBC GEL Form Guidelines and WCAG 2.1 Level AA:

  • Label Above Input: Labels are positioned above inputs by default (BBC GEL best practice) for better mobile usability and internationalization
  • Semantic HTML: Uses native <input> and <label> elements with proper for attribute association
  • ARIA Support: Implements aria-invalid, aria-required, aria-describedby for validation states
  • Error Messaging: Error messages are properly linked via aria-describedby for screen reader announcement
  • Focus Management: Clear focus indicators using design system tokens
  • Required Fields: Visual asterisk (*) indicator with proper ARIA markup
  • Help Text: Associated with input via aria-describedby for context

Label Requirements

Always provide a label for accessibility. The component supports multiple label patterns:

Visible label (recommended):

vue
<VueInput v-model:value="email" label="Email Address" />

Visually hidden label (when design requires it):

vue
<VueInput
  v-model:value="search"
  label="Search"
  :label-hidden="true"
  placeholder="Search..."
/>

Form Integration

Use v-model for two-way data binding with form data:

vue
<template>
  <form @submit.prevent="handleSubmit">
    <VueInput
      v-model:value="form.firstName"
      label="First Name"
      name="firstName"
      :required="true"
      :invalid="errors.firstName"
      :error-message="errors.firstName"
    />

    <VueInput
      v-model:value="form.email"
      label="Email"
      name="email"
      type="email"
      :required="true"
      :invalid="errors.email"
      :error-message="errors.email"
      @blur="validateEmail"
    />

    <VueInput
      v-model:value="form.message"
      label="Message"
      name="message"
      type="textarea"
      :rows="6"
      help-text="Optional: Tell us more"
    />

    <button type="submit">Submit</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      form: {
        firstName: "",
        email: "",
        message: "",
      },
      errors: {
        firstName: "",
        email: "",
      },
    };
  },
  methods: {
    validateEmail() {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(this.form.email) && this.form.email !== "") {
        this.errors.email = "Please enter a valid email address";
      } else {
        this.errors.email = "";
      }
    },
    handleSubmit() {
      // Validate all fields
      this.errors.firstName = this.form.firstName
        ? ""
        : "First name is required";
      this.validateEmail();

      if (!this.errors.firstName && !this.errors.email) {
        // Submit form
        console.log("Form submitted:", this.form);
      }
    },
  },
};
</script>

Input Addons

Add icons or text before or after the input using slots. Addons are automatically detected when you provide slot content - no props needed!

vue
<template>
  <VueInput v-model:value="url" label="Website URL" placeholder="example.com">
    <template #addon-left>
      <Globe :size="18" color="#12623e" />
    </template>
  </VueInput>

  <VueInput v-model:value="price" label="Price" placeholder="0.00">
    <template #addon-right>
      <DollarSign :size="18" color="#14854f" />
    </template>
  </VueInput>

  <VueInput v-model:value="amount" label="Amount" placeholder="100">
    <template #addon-left>
      <DollarSign :size="18" color="#14854f" />
    </template>
    <template #addon-right>
      <span>.00</span>
    </template>
  </VueInput>

  <VueInput
    v-model:value="discount"
    label="Discount"
    type="number"
    placeholder="10"
  >
    <template #addon-right>
      <span style="font-weight: bold;">%</span>
    </template>
  </VueInput>
</template>

<script>
import VueInput from "agnosticui-core/input/vue";
import { Globe, DollarSign } from "lucide-vue-next";

export default {
  components: { VueInput, Globe, DollarSign },
  data() {
    return { url: "", price: "", amount: "", discount: "" };
  },
};
</script>

Input Types

The component supports all HTML5 input types:

  • Text-based: text (default), email, password, search, tel, url
  • Numeric: number
  • Date/Time: date, time, datetime-local, month, week
  • Other: color, file, range
  • Textarea: Use type="textarea" for multi-line text input
vue
<VueInput v-model:value="email" label="Email" type="email" />
<VueInput v-model:value="password" label="Password" type="password" />
<VueInput v-model:value="date" label="Date" type="date" />
<VueInput v-model:value="message" label="Message" type="textarea" :rows="4" />

Shape Variants

Customize the input appearance with shape variants:

vue
<!-- Default: Rectangular -->
<VueInput v-model:value="value" label="Default" />

<!-- Rounded corners -->
<VueInput v-model:value="value" label="Rounded" :rounded="true" />

<!-- Capsule (fully rounded) -->
<VueInput v-model:value="value" label="Capsule" :capsule="true" />

<!-- Underlined only -->
<VueInput v-model:value="value" label="Underlined" :underlined="true" />

<!-- Underlined with background -->
<VueInput
  v-model:value="value"
  label="Underlined + BG"
  :underlined-with-background="true"
/>

Label Positioning

Control label placement with the labelPosition prop. Supports four directional values that work across all form controls:

vue
<!-- Top (default) - Label above input -->
<VueInput
  v-model:value="name"
  label="Full Name"
  label-position="top"
  placeholder="Enter your name"
/>

<!-- Start - Label before input (horizontal, respects RTL) -->
<VueInput
  v-model:value="age"
  label="Age:"
  label-position="start"
  placeholder="25"
/>

<!-- End - Label after input (horizontal, respects RTL) -->
<VueInput
  v-model:value="amount"
  label="USD"
  label-position="end"
  placeholder="100"
/>

<!-- Bottom - Label below input -->
<VueInput
  v-model:value="code"
  label="Verification Code"
  label-position="bottom"
  placeholder="Enter code"
/>

Use Cases:

  • top (default): Best for most forms - follows BBC GEL guidelines for mobile usability
  • start: Compact horizontal layouts, admin panels, settings forms
  • end: Less common, useful when label is a unit or suffix (e.g., "USD", "kg")
  • bottom: Rare, use sparingly for special design requirements

Progressive Enhancement: Horizontal layouts (start/end) use modern CSS field-sizing: content in Chrome 123+ and Safari TP for responsive input widths, with graceful fallback to fixed width in other browsers.

Validation

Show validation states and error messages:

vue
<template>
  <VueInput
    v-model:value="email"
    label="Email"
    type="email"
    :required="true"
    :invalid="emailError"
    :error-message="emailError ? 'Please enter a valid email' : ''"
    help-text="We'll never share your email"
    @blur="validateEmail"
  />
</template>

<script>
export default {
  data() {
    return {
      email: "",
      emailError: false,
    };
  },
  methods: {
    validateEmail() {
      const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      this.emailError = !regex.test(this.email) && this.email !== "";
    },
  },
};
</script>

Best Practices

  1. Always provide a label - Essential for accessibility and usability
  2. Use appropriate input types - Triggers correct mobile keyboards and built-in validation
  3. Provide helpful error messages - Tell users what went wrong and how to fix it
  4. Use help text for context - Explain format requirements or provide examples
  5. Mark required fields - Use the required prop to show the asterisk indicator
  6. Validate on blur - Check input validity when user leaves the field
  7. Keep labels above inputs - Follows BBC GEL guidelines for better mobile UX
  8. Consider placeholder text carefully - Don't rely on placeholders alone for instructions

CSS Shadow Parts

The Input component exposes the following CSS Shadow Parts for custom styling:

PartDescription
ag-inputThe input element itself
ag-textareaThe textarea element (when type="textarea")
ag-input-wrapperMain wrapper div around all elements
ag-input-labelThe label element
ag-input-requiredRequired indicator asterisk (*)
ag-input-helpHelp text div below the input
ag-input-errorError message div
ag-input-field-wrapperWrapper div for input with addons
ag-input-addon-leftLeft addon container
ag-input-addon-rightRight addon container

Customization Examples

css
ag-input::part(ag-input) {
  border: 2px solid transparent;
  background: linear-gradient(white, white) padding-box, linear-gradient(
        135deg,
        #667eea 0%,
        #764ba2 100%
      ) border-box;
  border-radius: 12px;
  padding: 0.75rem 1rem;
}

ag-input::part(ag-input):focus {
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}

ag-input::part(ag-input-label) {
  font-weight: 700;
  color: #667eea;
  text-transform: uppercase;
  font-size: 0.75rem;
}

ag-input::part(ag-input-error) {
  color: #dc2626;
  font-weight: 600;
  padding: 0.5rem;
  background: #fee2e2;
  border-left: 3px solid #dc2626;
  border-radius: 4px;
}

ag-input::part(ag-input) {
  border: none;
  border-bottom: 2px solid #e5e7eb;
  border-radius: 0;
  background: transparent;
  transition: border-color 0.2s ease;
}

ag-input::part(ag-input):focus {
  border-bottom-color: #667eea;
}

See the CSS Shadow Parts Customization section in the examples above for more styling demonstrations.

When to Use

Use Input when:

  • Collecting short, single-line text data (name, email, etc.)
  • Building forms for data entry
  • You need validation and error messaging
  • You need different input types (email, password, date, etc.)

Use Textarea (type="textarea") when:

  • Collecting longer, multi-line text (comments, descriptions, etc.)
  • User needs to see multiple lines of their input at once

Consider alternatives when:

  • You have a fixed set of options - use Select or Radio buttons instead
  • You need yes/no or on/off - use Toggle or Checkbox instead
  • You need date/time selection with a calendar - consider a DatePicker component