InputField UI Elements
Formulier input met label, helper tekst, error state, iconen en password toggle. Textarea variant inbegrepen.
src/components/ui/InputField.astro
---
/**
* InputField
* Styled form input: text, email, password, textarea.
* Label, helper text, error state, disabled, leading/trailing icon.
*/
interface Props {
id?: string;
name?: string;
label?: string;
type?: 'text' | 'email' | 'password' | 'tel' | 'url' | 'search' | 'number' | 'textarea';
placeholder?: string;
value?: string;
helper?: string;
error?: string;
required?: boolean;
disabled?: boolean;
rows?: number;
leadingIcon?: string;
trailingIcon?: string;
size?: 'sm' | 'md' | 'lg';
}
const {
id,
name,
label,
type = 'text',
placeholder,
value,
helper,
error,
required = false,
disabled = false,
rows = 4,
leadingIcon,
trailingIcon,
size = 'md',
} = Astro.props;
const inputId = id ?? `input-${Math.random().toString(36).slice(2, 7)}`;
const hasError = !!error;
---
<div class:list={['if', `if--${size}`, { 'if--error': hasError, 'if--disabled': disabled }]} data-component="input-field">
{label && (
<label class="if__label" for={inputId}>
{label}
{required && <span class="if__required" aria-hidden="true">*</span>}
</label>
)}
<div class="if__wrap">
{leadingIcon && (
<span class="if__icon if__icon--lead" aria-hidden="true" set:html={leadingIcon} />
)}
{type === 'textarea' ? (
<textarea
id={inputId}
name={name}
class="if__input if__textarea"
placeholder={placeholder}
rows={rows}
required={required}
disabled={disabled}
aria-invalid={hasError ? 'true' : undefined}
aria-describedby={error ? `${inputId}-error` : helper ? `${inputId}-helper` : undefined}
>{value}</textarea>
) : (
<input
id={inputId}
name={name}
type={type}
class="if__input"
placeholder={placeholder}
value={value}
required={required}
disabled={disabled}
aria-invalid={hasError ? 'true' : undefined}
aria-describedby={error ? `${inputId}-error` : helper ? `${inputId}-helper` : undefined}
/>
)}
{trailingIcon && (
<span class="if__icon if__icon--trail" aria-hidden="true" set:html={trailingIcon} />
)}
{type === 'password' && (
<button type="button" class="if__toggle-pw" aria-label="Toon/verberg wachtwoord">
<svg class="if__pw-show" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
)}
</div>
{error && <p class="if__message if__message--error" id={`${inputId}-error`} role="alert">{error}</p>}
{!error && helper && <p class="if__message" id={`${inputId}-helper`}>{helper}</p>}
</div>
{type === 'password' && (
<script>
document.querySelectorAll('[data-component="input-field"]').forEach(field => {
const btn = field.querySelector<HTMLButtonElement>('.if__toggle-pw');
const input = field.querySelector<HTMLInputElement>('.if__input');
if (btn && input) {
btn.addEventListener('click', () => {
input.type = input.type === 'password' ? 'text' : 'password';
});
}
});
</script>
)}
<style>
.if { display: flex; flex-direction: column; gap: 0.375rem; }
.if__label {
font-size: 0.875rem;
font-weight: 600;
color: var(--color-primary, #0a0a0a);
display: flex;
align-items: center;
gap: 0.25rem;
}
.if--sm .if__label { font-size: 0.8125rem; }
.if--lg .if__label { font-size: 0.9375rem; }
.if__required { color: #ef4444; }
.if__wrap {
position: relative;
display: flex;
align-items: center;
}
.if__input {
width: 100%;
background: var(--color-bg, #fff);
border: 1.5px solid rgba(0,0,0,0.14);
border-radius: var(--radius, 0.5rem);
font-size: 0.9375rem;
font-family: inherit;
color: var(--color-primary, #0a0a0a);
padding: 0.6875rem 0.875rem;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
appearance: none;
-webkit-appearance: none;
}
.if--sm .if__input { font-size: 0.8125rem; padding: 0.5rem 0.75rem; }
.if--lg .if__input { font-size: 1rem; padding: 0.875rem 1rem; }
.if__input::placeholder { color: rgba(0,0,0,0.3); }
.if__input:focus {
border-color: var(--color-accent, #6366f1);
box-shadow: 0 0 0 3px rgba(99,102,241,0.12);
}
.if--error .if__input {
border-color: #ef4444;
}
.if--error .if__input:focus {
box-shadow: 0 0 0 3px rgba(239,68,68,0.12);
}
.if--disabled .if__input {
background: rgba(0,0,0,0.04);
color: rgba(0,0,0,0.4);
cursor: not-allowed;
}
/* Icons */
.if__icon {
position: absolute;
display: flex;
align-items: center;
color: rgba(0,0,0,0.4);
pointer-events: none;
}
.if__icon--lead { left: 0.75rem; }
.if__icon--trail { right: 0.75rem; }
.if__wrap:has(.if__icon--lead) .if__input { padding-left: 2.5rem; }
.if__wrap:has(.if__icon--trail) .if__input { padding-right: 2.5rem; }
/* Textarea */
.if__textarea {
resize: vertical;
align-self: stretch;
min-height: 100px;
}
/* Password toggle */
.if__toggle-pw {
position: absolute;
right: 0.75rem;
background: none;
border: none;
cursor: pointer;
color: rgba(0,0,0,0.4);
padding: 0.25rem;
display: flex;
align-items: center;
transition: color 0.15s;
}
.if__toggle-pw:hover { color: var(--color-primary, #0a0a0a); }
/* Messages */
.if__message {
font-size: 0.8125rem;
color: var(--color-muted, #6b7280);
line-height: 1.4;
}
.if__message--error { color: #ef4444; }
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
label | string | — | Veld label |
type | 'text' | 'email' | 'password' | 'tel' | 'url' | 'textarea' | 'text' | Input type |
placeholder | string | — | Placeholder tekst |
value | string | — | Standaard waarde |
helper | string | — | Hulptekst onder het veld |
error | string | — | Foutmelding (toont rode staat) |
required | boolean | false | Verplicht veld |
disabled | boolean | false | Uitgeschakeld |
size | 'sm' | 'md' | 'lg' | 'md' | Grootte |
leadingIcon | string | — | SVG HTML voor icoon links |
trailingIcon | string | — | SVG HTML voor icoon rechts |
rows | number | 4 | Aantal rijen (type=textarea) |
* = verplicht