Input
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
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!
Addons with Style Variants
Addons work seamlessly with all input styling variants
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)
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>
);
}
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:
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add InputThe 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
<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
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)
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
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | '' | The input value (use with v-model for two-way binding) |
label | string | '' | Label text for the input. Best practice is to always provide a visible label. |
type | string | 'text' | Input type (text, email, password, search, tel, url, number, date, time, color, or 'textarea' for textarea element) |
placeholder | string | '' | Placeholder text shown when input is empty |
size | 'small' | 'default' | 'large' | 'default' | Size variant of the input |
name | string | '' | Name attribute for form submission |
required | boolean | false | Marks the field as required. Displays an asterisk (*) after the label |
disabled | boolean | false | Disables the input, preventing interaction |
readonly | boolean | false | Makes the input read-only but still focusable |
invalid | boolean | false | Marks the input as invalid. Changes border color to red and sets aria-invalid |
helpText | string | '' | Helper text displayed below the input |
errorMessage | string | '' | Error message displayed when invalid. Linked via aria-describedby |
rounded | boolean | false | Applies rounded corners (border-radius: md) |
capsule | boolean | false | Applies capsule shape (fully rounded ends) |
underlined | boolean | false | Shows only bottom border (underlined style) |
underlinedWithBackground | boolean | false | Underlined style with subtle background color |
inline | boolean | false | Changes 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) |
labelHidden | boolean | false | Visually hides the label while keeping it accessible to screen readers |
noLabel | boolean | false | Completely removes the label element. Use with ariaLabel for accessibility |
ariaLabel | string | '' | ARIA label for accessibility when label is not visible |
hasLeftAddon | boolean | false | Deprecated: Addons are now automatically detected. Simply use the addon-left slot. |
hasRightAddon | boolean | false | Deprecated: Addons are now automatically detected. Simply use the addon-right slot. |
rows | number | 4 | Number of rows for textarea (only applies when type="textarea") |
cols | number | 50 | Number 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:
| Event | Payload | Description |
|---|---|---|
click | MouseEvent | Native click event on the input |
input | InputEvent | Native input event, fired on every keystroke |
change | Event | Native change event, fired when input loses focus after value changed |
focus | FocusEvent | Fired when input receives focus (re-dispatched from host for cross-shadow-DOM access) |
blur | FocusEvent | Fired when input loses focus (re-dispatched from host for cross-shadow-DOM access) |
Framework-Specific Event Usage
Vue:
- Use
v-model:valuefor two-way binding - Listen to events with
@event-namesyntax (e.g.,@input,@change,@focus,@blur) - The
update:valueemit is automatically fired on input for v-model support
<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
<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
addEventListenerand callback properties - Focus and blur events bubble through shadow DOM
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
| Slot | Description |
|---|---|
addon-left | Content to display on the left side of the input (automatically detected when provided) |
addon-right | Content 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 properforattribute association - ARIA Support: Implements
aria-invalid,aria-required,aria-describedbyfor validation states - Error Messaging: Error messages are properly linked via
aria-describedbyfor 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-describedbyfor context
Label Requirements
Always provide a label for accessibility. The component supports multiple label patterns:
Visible label (recommended):
<VueInput v-model:value="email" label="Email Address" />Visually hidden label (when design requires it):
<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:
<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!
<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
<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:
<!-- 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:
<!-- 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 usabilitystart: Compact horizontal layouts, admin panels, settings formsend: 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:
<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
- Always provide a label - Essential for accessibility and usability
- Use appropriate input types - Triggers correct mobile keyboards and built-in validation
- Provide helpful error messages - Tell users what went wrong and how to fix it
- Use help text for context - Explain format requirements or provide examples
- Mark required fields - Use the
requiredprop to show the asterisk indicator - Validate on blur - Check input validity when user leaves the field
- Keep labels above inputs - Follows BBC GEL guidelines for better mobile UX
- 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:
| Part | Description |
|---|---|
ag-input | The input element itself |
ag-textarea | The textarea element (when type="textarea") |
ag-input-wrapper | Main wrapper div around all elements |
ag-input-label | The label element |
ag-input-required | Required indicator asterisk (*) |
ag-input-help | Help text div below the input |
ag-input-error | Error message div |
ag-input-field-wrapper | Wrapper div for input with addons |
ag-input-addon-left | Left addon container |
ag-input-addon-right | Right addon container |
Customization Examples
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