Inputs
Ionic’s ion-input, ion-textarea, ion-select, ion-toggle render native-feeling inputs across iOS, Android, and the web. Pair with reactive forms for validation + state.
Inputs, textarea, select, toggle, validation
EXAMPLE
<!-- Angular template -->
<form [formGroup]="form" (ngSubmit)="submit()">
<!-- 1) Text input — float label, helper, error states -->
<ion-item lines="inset">
<ion-label position="floating">Email</ion-label>
<ion-input
formControlName="email"
type="email"
inputmode="email"
autocomplete="email"
autocapitalize="off"
spellcheck="false"
clearInput
></ion-input>
</ion-item>
<ion-note color="danger" *ngIf="email.invalid && email.touched">
Please enter a valid email.
</ion-note>
<!-- 2) Password input — clear button + toggle visibility -->
<ion-item>
<ion-label position="floating">Password</ion-label>
<ion-input
formControlName="password"
[type]="showPassword ? 'text' : 'password'"
autocomplete="current-password"
></ion-input>
<ion-button fill="clear" slot="end" (click)="showPassword = !showPassword">
<ion-icon [name]="showPassword ? 'eye-off' : 'eye'" slot="icon-only"></ion-icon>
</ion-button>
</ion-item>
<!-- 3) Number input — keyboard + range -->
<ion-item>
<ion-label position="floating">Age</ion-label>
<ion-input
formControlName="age"
type="number"
inputmode="numeric"
min="0"
max="120"
></ion-input>
</ion-item>
<!-- 4) Phone -->
<ion-item>
<ion-label position="floating">Phone</ion-label>
<ion-input
formControlName="phone"
type="tel"
inputmode="tel"
autocomplete="tel"
placeholder="+61 4xx xxx xxx"
></ion-input>
</ion-item>
<!-- 5) Textarea — auto-grow optional -->
<ion-item>
<ion-label position="floating">Bio</ion-label>
<ion-textarea
formControlName="bio"
rows="4"
autoGrow="true"
maxlength="500"
counter="true"
></ion-textarea>
</ion-item>
<!-- 6) Select — single -->
<ion-item>
<ion-label>Country</ion-label>
<ion-select
formControlName="country"
interface="action-sheet"
placeholder="Choose a country"
>
<ion-select-option value="au">Australia</ion-select-option>
<ion-select-option value="us">United States</ion-select-option>
<ion-select-option value="uk">United Kingdom</ion-select-option>
<ion-select-option value="de">Germany</ion-select-option>
</ion-select>
</ion-item>
<!-- 7) Select — multiple -->
<ion-item>
<ion-label>Languages</ion-label>
<ion-select formControlName="languages" multiple="true">
<ion-select-option value="en">English</ion-select-option>
<ion-select-option value="fr">French</ion-select-option>
<ion-select-option value="es">Spanish</ion-select-option>
<ion-select-option value="de">German</ion-select-option>
</ion-select>
</ion-item>
<!-- 8) Toggle (switch) -->
<ion-item>
<ion-label>Notifications</ion-label>
<ion-toggle formControlName="notifications" slot="end"></ion-toggle>
</ion-item>
<!-- 9) Checkbox -->
<ion-item>
<ion-label>I accept the terms</ion-label>
<ion-checkbox formControlName="terms" slot="start"></ion-checkbox>
</ion-item>
<!-- 10) Radio group -->
<ion-radio-group formControlName="plan">
<ion-list-header><ion-label>Plan</ion-label></ion-list-header>
<ion-item>
<ion-label>Free</ion-label>
<ion-radio slot="start" value="free"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Pro</ion-label>
<ion-radio slot="start" value="pro"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Enterprise</ion-label>
<ion-radio slot="start" value="enterprise"></ion-radio>
</ion-item>
</ion-radio-group>
<!-- 11) Range -->
<ion-item>
<ion-label>Volume</ion-label>
<ion-range formControlName="volume" min="0" max="100" step="5" pin="true">
<ion-icon slot="start" name="volume-low"></ion-icon>
<ion-icon slot="end" name="volume-high"></ion-icon>
</ion-range>
</ion-item>
<!-- 12) DateTime input — modal picker -->
<ion-item button id="open-datetime">
<ion-label>Birth date</ion-label>
<ion-text slot="end">{{ form.value.birthDate | date }}</ion-text>
</ion-item>
<ion-modal trigger="open-datetime">
<ng-template>
<ion-datetime
formControlName="birthDate"
presentation="date"
[max]="today"
[min]="'1900-01-01'"
></ion-datetime>
</ng-template>
</ion-modal>
<!-- 13) Submit -->
<div class="ion-padding">
<ion-button expand="block" type="submit" [disabled]="form.invalid || busy">
<ion-spinner *ngIf="busy" slot="start"></ion-spinner>
{{ busy ? 'Saving…' : 'Sign up' }}
</ion-button>
</div>
</form>
<!-- ====================== TypeScript ====================== -->
// signup.page.ts
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { ToastController } from '@ionic/angular';
@@Component({ selector: 'app-signup', templateUrl: './signup.page.html' })
export class SignupPage {
showPassword = false;
busy = false;
today = new Date().toISOString();
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
age: [0, [Validators.min(0), Validators.max(120)]],
phone: [''],
bio: [''],
country: [null, Validators.required],
languages: [[]],
notifications: [true],
terms: [false, Validators.requiredTrue],
plan: ['free'],
volume: [50],
birthDate: [null],
});
constructor(private fb: FormBuilder, private toast: ToastController) {}
get email() { return this.form.controls.email; }
get password() { return this.form.controls.password; }
async submit() {
if (this.form.invalid) return;
this.busy = true;
try {
await this.api.signup(this.form.getRawValue());
(await this.toast.create({ message: 'Account created', duration: 2000 })).present();
} catch (e: any) {
(await this.toast.create({ message: e.message, color: 'danger', duration: 3000 })).present();
} finally {
this.busy = false;
}
}
}
<!-- ====================== React (Ionic React) ====================== -->
import { useState } from 'react';
import { IonInput, IonItem, IonLabel, IonNote, IonButton } from '@ionic/react';
function SignupForm() {
const [email, setEmail] = useState('');
const [touched, setTouched] = useState(false);
const error = touched && !/\S+@@\S+\.\S+/.test(email) ? 'Invalid email' : '';
return (
<>
<IonItem>
<IonLabel position="floating">Email</IonLabel>
<IonInput
value={email}
onIonInput={(e) => setEmail(e.detail.value!)}
onIonBlur={() => setTouched(true)}
type="email"
autocomplete="email"
/>
</IonItem>
{error && <IonNote color="danger">{error}</IonNote>}
<IonButton expand="block" disabled={!!error || !email}>Sign up</IonButton>
</>
);
}
<!-- ====================== Best practices ====================== -->
<!-- ✅ inputmode + type for the right native keyboard -->
<!-- type="email" → email keyboard -->
<!-- type="tel" → number pad -->
<!-- inputmode="decimal" → numeric with dot for prices -->
<!-- type="search" → keyboard with 'search' button -->
<!-- ✅ autocomplete + autocapitalize hints for OS suggestions / autofill -->
<!-- autocomplete="email | password | new-password | tel | given-name | ..." -->
<!-- autocapitalize="off" on email + passwords -->
<!-- ✅ Validation -->
<!-- Pair Reactive Forms with native input validators -->
<!-- Show errors only after blur (touched) — not on first render -->
<!-- ✅ Accessibility -->
<!-- ion-label for every input -->
<!-- aria-describedby for error notes -->
<!-- aria-invalid when applicable -->
<!-- Sufficient color contrast on errors -->
<!-- ✅ Loading + disabled states -->
<!-- Disable submit during await -->
<!-- Spinner inside the button -->
<!-- ❌ Common bugs -->
<!-- Wrong type → wrong keyboard -->
<!-- Missing autocomplete → no autofill -->
<!-- Forgetting ion-label → screen reader silent -->
<!-- Showing errors immediately → bad UX on first render -->
<!-- Capturing onIonChange instead of onIonInput → 1-update delay -->
Why it matters
Match type + inputmode + autocomplete to the data — users get the right keyboard, the right autofill, and the right autocapitalisation. Reactive Forms + show-errors-after-blur is the standard Angular validation pattern.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
<ion-item>
<ion-label>Name</ion-label>
<ion-input [(ngModel)]="name"></ion-input>
</ion-item>
<ion-toggle [(ngModel)]="darkMode">Dark mode</ion-toggle>
Try it Yourself »
Discussion
Loading…