Data input
Input
A native text input in a field with a label, help text and error message.
| Class name | Type | Description |
|---|---|---|
inputplf:input | Component | A native text input |
fieldplf:field | Component | A label, a control and its messages |
field-labelplf:field-label | Part | The control's label |
field-legendplf:field-legend | Part | The fieldset's legend |
field-helpplf:field-help | Part | Help text |
field-errorplf:field-error | Part | An error message |
field-requiredplf:field-required | Part | Marks a required field in the label |
input-smplf:input-sm | Size | Small |
input-lgplf:input-lg | Size | Large |
field-groupplf:field-group | Modifier | A fieldset of fields, without its border |
Default
The same field parts compose Textarea, native selects, Checkbox, Radio and Switch. Keep a visible label: a placeholder disappears as soon as someone types.
<div class="field">
<label class="field-label" for="input-website">Website</label>
<input class="input" id="input-website" name="website" type="url" dir="ltr" autocomplete="url" placeholder="https://example.com">
</div><div class="plf:field">
<label class="plf:field-label" for="input-website">Website</label>
<input class="plf:input" id="input-website" name="website" type="url" dir="ltr" autocomplete="url" placeholder="https://example.com">
</div>import { Field, Input } from '@pelatform/react';
export function Example() {
return (
<Field label="Website">
<Input name="website" type="url" dir="ltr" autoComplete="url" placeholder="https://example.com" />
</Field>
);
}import { Field, Input } from '@pelatform/react/prefixed';
export function Example() {
return (
<Field label="Website">
<Input name="website" type="url" dir="ltr" autoComplete="url" placeholder="https://example.com" />
</Field>
);
}Help text
Put the help text’s id in the input’s aria-describedby.
Example: https://example.test/workspaces/international-customer-success
<div class="field">
<label class="field-label" for="input-workspace">Workspace URL</label>
<input class="input" id="input-workspace" type="url" dir="ltr" value="https://example.test/workspaces/customer-success" aria-describedby="input-workspace-help">
<p class="field-help" id="input-workspace-help">Example: https://example.test/workspaces/international-customer-success</p>
</div><div class="plf:field">
<label class="plf:field-label" for="input-workspace">Workspace URL</label>
<input class="plf:input" id="input-workspace" type="url" dir="ltr" value="https://example.test/workspaces/customer-success" aria-describedby="input-workspace-help">
<p class="plf:field-help" id="input-workspace-help">Example: https://example.test/workspaces/international-customer-success</p>
</div>import { Field, Input } from '@pelatform/react';
export function Example() {
return (
<Field label="Workspace URL" help="Example: https://example.test/workspaces/international-customer-success">
<Input type="url" dir="ltr" defaultValue="https://example.test/workspaces/customer-success" />
</Field>
);
}import { Field, Input } from '@pelatform/react/prefixed';
export function Example() {
return (
<Field label="Workspace URL" help="Example: https://example.test/workspaces/international-customer-success">
<Input type="url" dir="ltr" defaultValue="https://example.test/workspaces/customer-success" />
</Field>
);
}Required
field-required only marks the label; the native required attribute makes the field required.
Shown alongside your contributions.
<div class="field">
<label class="field-label" for="input-name">Display name <span class="field-required">(required)</span></label>
<input class="input" id="input-name" name="display-name" type="text" autocomplete="name" required placeholder="Your name" aria-describedby="input-name-help">
<p class="field-help" id="input-name-help">Shown alongside your contributions.</p>
</div><div class="plf:field">
<label class="plf:field-label" for="input-name">Display name <span class="plf:field-required">(required)</span></label>
<input class="plf:input" id="input-name" name="display-name" type="text" autocomplete="name" required placeholder="Your name" aria-describedby="input-name-help">
<p class="plf:field-help" id="input-name-help">Shown alongside your contributions.</p>
</div>import { Field, Input } from '@pelatform/react';
export function Example() {
return (
<Field label="Display name" help="Shown alongside your contributions." required>
<Input name="display-name" type="text" autoComplete="name" placeholder="Your name" />
</Field>
);
}import { Field, Input } from '@pelatform/react/prefixed';
export function Example() {
return (
<Field label="Display name" help="Shown alongside your contributions." required>
<Input name="display-name" type="text" autoComplete="name" placeholder="Your name" />
</Field>
);
}Invalid
Your application adds and removes aria-invalid and the error message, whose id follows the help id in aria-describedby; start the message with “Error:” so the state does not rely on color alone.
Use an address you can access.
Error: enter a valid email address.
<div class="field">
<label class="field-label" for="input-email">Email</label>
<input class="input" id="input-email" name="email" type="email" dir="ltr" autocomplete="email" value="not-an-email" aria-invalid="true" aria-describedby="input-email-help input-email-error">
<p class="field-help" id="input-email-help">Use an address you can access.</p>
<p class="field-error" id="input-email-error">Error: enter a valid email address.</p>
</div><div class="plf:field">
<label class="plf:field-label" for="input-email">Email</label>
<input class="plf:input" id="input-email" name="email" type="email" dir="ltr" autocomplete="email" value="not-an-email" aria-invalid="true" aria-describedby="input-email-help input-email-error">
<p class="plf:field-help" id="input-email-help">Use an address you can access.</p>
<p class="plf:field-error" id="input-email-error">Error: enter a valid email address.</p>
</div>import { Field, Input } from '@pelatform/react';
export function Example() {
return (
<Field label="Email" help="Use an address you can access." error="Error: enter a valid email address.">
<Input name="email" type="email" dir="ltr" autoComplete="email" defaultValue="not-an-email" />
</Field>
);
}import { Field, Input } from '@pelatform/react/prefixed';
export function Example() {
return (
<Field label="Email" help="Use an address you can access." error="Error: enter a valid email address.">
<Input name="email" type="email" dir="ltr" autoComplete="email" defaultValue="not-an-email" />
</Field>
);
}Errors from your code
PLF.Field.error(control, message) shows your message, such as one your server returns, in the element the control’s aria-errormessage names, until the next edit.
<form class="flex flex-col gap-4 max-w-sm" id="input-signup">
<div class="field">
<label class="field-label" for="input-signup-email">Email</label>
<input class="input" id="input-signup-email" name="email" type="email" dir="ltr" autocomplete="email" value="taken@example.com" required aria-describedby="input-signup-email-help" aria-errormessage="input-signup-email-error">
<p class="field-help" id="input-signup-email-help">Use an address you can access.</p>
<p class="field-error" id="input-signup-email-error" hidden></p>
</div>
<button class="btn btn-primary self-start">
<span class="btn-label">Create account</span>
<span class="btn-loading">
<span class="btn-icon btn-icon-loading"></span>
Creating…
</span>
</button>
</form>
<script>
const form = document.getElementById('input-signup');
const email = document.getElementById('input-signup-email');
form.addEventListener('submit', async event => {
event.preventDefault();
// Your request; the button shows its loading part until it settles.
const response = await PLF.Button.loading(event.submitter, fetch('/signup', { method: 'POST', body: new FormData(form) }));
if (response.status === 409) {
PLF.Field.error(email, 'Error: this email already has an account.');
email.focus();
}
});
</script><form class="plf:flex plf:flex-col plf:gap-4 plf:max-w-sm" id="input-signup">
<div class="plf:field">
<label class="plf:field-label" for="input-signup-email">Email</label>
<input class="plf:input" id="input-signup-email" name="email" type="email" dir="ltr" autocomplete="email" value="taken@example.com" required aria-describedby="input-signup-email-help" aria-errormessage="input-signup-email-error">
<p class="plf:field-help" id="input-signup-email-help">Use an address you can access.</p>
<p class="plf:field-error" id="input-signup-email-error" hidden></p>
</div>
<button class="plf:btn plf:btn-primary plf:self-start">
<span class="plf:btn-label">Create account</span>
<span class="plf:btn-loading">
<span class="plf:btn-icon plf:btn-icon-loading"></span>
Creating…
</span>
</button>
</form>
<script>
const form = document.getElementById('input-signup');
const email = document.getElementById('input-signup-email');
form.addEventListener('submit', async event => {
event.preventDefault();
// Your request; the button shows its loading part until it settles.
const response = await PLF.Button.loading(event.submitter, fetch('/signup', { method: 'POST', body: new FormData(form) }));
if (response.status === 409) {
PLF.Field.error(email, 'Error: this email already has an account.');
email.focus();
}
});
</script>'use client';
import { useEffect, useRef, useState, type FormEvent } from 'react';
import { Button, Field, Input } from '@pelatform/react';
export function Example() {
const email = useRef<HTMLInputElement>(null);
const [error, setError] = useState('');
const [saving, setSaving] = useState(false);
// The field takes focus once its error shows, so the error is read with it.
useEffect(() => {
if (error) email.current?.focus();
}, [error]);
async function signUp(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setSaving(true);
try {
// Your request; the button shows its loading part until it settles.
const response = await fetch('/signup', { method: 'POST', body: new FormData(event.currentTarget) });
if (response.status === 409) setError('Error: this email already has an account.');
} finally {
setSaving(false);
}
}
return (
<form className="flex flex-col gap-4 max-w-sm" onSubmit={signUp}>
<Field label="Email" help="Use an address you can access." error={error}>
<Input ref={email} name="email" type="email" dir="ltr" autoComplete="email" defaultValue="taken@example.com" required onChange={() => setError('')} />
</Field>
<Button color="primary" className="self-start" loading={saving} loadingText="Creating…">
Create account
</Button>
</form>
);
}'use client';
import { useEffect, useRef, useState, type FormEvent } from 'react';
import { Button, Field, Input } from '@pelatform/react/prefixed';
export function Example() {
const email = useRef<HTMLInputElement>(null);
const [error, setError] = useState('');
const [saving, setSaving] = useState(false);
// The field takes focus once its error shows, so the error is read with it.
useEffect(() => {
if (error) email.current?.focus();
}, [error]);
async function signUp(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setSaving(true);
try {
// Your request; the button shows its loading part until it settles.
const response = await fetch('/signup', { method: 'POST', body: new FormData(event.currentTarget) });
if (response.status === 409) setError('Error: this email already has an account.');
} finally {
setSaving(false);
}
}
return (
<form className="plf:flex plf:flex-col plf:gap-4 plf:max-w-sm" onSubmit={signUp}>
<Field label="Email" help="Use an address you can access." error={error}>
<Input ref={email} name="email" type="email" dir="ltr" autoComplete="email" defaultValue="taken@example.com" required onChange={() => setError('')} />
</Field>
<Button color="primary" className="plf:self-start" loading={saving} loadingText="Creating…">
Create account
</Button>
</form>
);
}Field group
A disabled fieldset disables every control inside it.
<fieldset class="field field-group" disabled>
<legend class="field-legend">Managed settings</legend>
<div class="field">
<label class="field-label" for="input-account">Account ID</label>
<input class="input" id="input-account" name="account-id" value="organization-managed" aria-describedby="input-account-help">
<p class="field-help" id="input-account-help">A disabled fieldset disables its inputs through native HTML.</p>
</div>
</fieldset><fieldset class="plf:field plf:field-group" disabled>
<legend class="plf:field-legend">Managed settings</legend>
<div class="plf:field">
<label class="plf:field-label" for="input-account">Account ID</label>
<input class="plf:input" id="input-account" name="account-id" value="organization-managed" aria-describedby="input-account-help">
<p class="plf:field-help" id="input-account-help">A disabled fieldset disables its inputs through native HTML.</p>
</div>
</fieldset>import { Field, FieldGroup, Input } from '@pelatform/react';
export function Example() {
return (
<FieldGroup legend="Managed settings" disabled>
<Field label="Account ID" help="A disabled fieldset disables its inputs through native HTML.">
<Input name="account-id" defaultValue="organization-managed" />
</Field>
</FieldGroup>
);
}import { Field, FieldGroup, Input } from '@pelatform/react/prefixed';
export function Example() {
return (
<FieldGroup legend="Managed settings" disabled>
<Field label="Account ID" help="A disabled fieldset disables its inputs through native HTML.">
<Input name="account-id" defaultValue="organization-managed" />
</Field>
</FieldGroup>
);
}Types
Set dir="ltr" on email, URL and phone values so they stay readable on right-to-left pages.
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="field">
<label class="field-label" for="input-phone">Phone</label>
<input class="input" id="input-phone" name="phone" type="tel" dir="ltr" autocomplete="tel" placeholder="+62 812 3456 7890">
</div>
<div class="field">
<label class="field-label" for="input-search">Search</label>
<input class="input" id="input-search" name="query" type="search" placeholder="Find something…">
</div>
<div class="field">
<label class="field-label" for="input-password">Password</label>
<input class="input" id="input-password" name="password" type="password" autocomplete="new-password" placeholder="Enter a password">
</div>
<div class="field">
<label class="field-label" for="input-quantity">Quantity</label>
<input class="input" id="input-quantity" name="quantity" type="number" min="0" value="1">
</div>
</div><div class="plf:grid plf:grid-cols-1 plf:md:grid-cols-2 plf:gap-4">
<div class="plf:field">
<label class="plf:field-label" for="input-phone">Phone</label>
<input class="plf:input" id="input-phone" name="phone" type="tel" dir="ltr" autocomplete="tel" placeholder="+62 812 3456 7890">
</div>
<div class="plf:field">
<label class="plf:field-label" for="input-search">Search</label>
<input class="plf:input" id="input-search" name="query" type="search" placeholder="Find something…">
</div>
<div class="plf:field">
<label class="plf:field-label" for="input-password">Password</label>
<input class="plf:input" id="input-password" name="password" type="password" autocomplete="new-password" placeholder="Enter a password">
</div>
<div class="plf:field">
<label class="plf:field-label" for="input-quantity">Quantity</label>
<input class="plf:input" id="input-quantity" name="quantity" type="number" min="0" value="1">
</div>
</div>import { Field, Input } from '@pelatform/react';
export function Example() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Field label="Phone">
<Input name="phone" type="tel" dir="ltr" autoComplete="tel" placeholder="+62 812 3456 7890" />
</Field>
<Field label="Search">
<Input name="query" type="search" placeholder="Find something…" />
</Field>
<Field label="Password">
<Input name="password" type="password" autoComplete="new-password" placeholder="Enter a password" />
</Field>
<Field label="Quantity">
<Input name="quantity" type="number" min={0} defaultValue={1} />
</Field>
</div>
);
}import { Field, Input } from '@pelatform/react/prefixed';
export function Example() {
return (
<div className="plf:grid plf:grid-cols-1 plf:md:grid-cols-2 plf:gap-4">
<Field label="Phone">
<Input name="phone" type="tel" dir="ltr" autoComplete="tel" placeholder="+62 812 3456 7890" />
</Field>
<Field label="Search">
<Input name="query" type="search" placeholder="Find something…" />
</Field>
<Field label="Password">
<Input name="password" type="password" autoComplete="new-password" placeholder="Enter a password" />
</Field>
<Field label="Quantity">
<Input name="quantity" type="number" min={0} defaultValue={1} />
</Field>
</div>
);
}<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="field">
<label class="field-label" for="input-small">Small</label>
<input class="input input-sm" id="input-small" placeholder="Text input">
</div>
<div class="field">
<label class="field-label" for="input-default">Default</label>
<input class="input" id="input-default" placeholder="Text input">
</div>
<div class="field">
<label class="field-label" for="input-large">Large</label>
<input class="input input-lg" id="input-large" placeholder="Text input">
</div>
</div><div class="plf:grid plf:grid-cols-1 plf:md:grid-cols-3 plf:gap-4">
<div class="plf:field">
<label class="plf:field-label" for="input-small">Small</label>
<input class="plf:input plf:input-sm" id="input-small" placeholder="Text input">
</div>
<div class="plf:field">
<label class="plf:field-label" for="input-default">Default</label>
<input class="plf:input" id="input-default" placeholder="Text input">
</div>
<div class="plf:field">
<label class="plf:field-label" for="input-large">Large</label>
<input class="plf:input plf:input-lg" id="input-large" placeholder="Text input">
</div>
</div>import { Field, Input } from '@pelatform/react';
export function Example() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Field label="Small">
<Input size="sm" placeholder="Text input" />
</Field>
<Field label="Default">
<Input placeholder="Text input" />
</Field>
<Field label="Large">
<Input size="lg" placeholder="Text input" />
</Field>
</div>
);
}import { Field, Input } from '@pelatform/react/prefixed';
export function Example() {
return (
<div className="plf:grid plf:grid-cols-1 plf:md:grid-cols-3 plf:gap-4">
<Field label="Small">
<Input size="sm" placeholder="Text input" />
</Field>
<Field label="Default">
<Input placeholder="Text input" />
</Field>
<Field label="Large">
<Input size="lg" placeholder="Text input" />
</Field>
</div>
);
}Read-only
Unlike disabled, readonly keeps the value in the form data.
You can focus and copy this value.
<div class="field">
<label class="field-label" for="input-readonly">Readonly</label>
<input class="input" id="input-readonly" name="username" type="text" value="contributor" readonly aria-describedby="input-readonly-help">
<p class="field-help" id="input-readonly-help">You can focus and copy this value.</p>
</div><div class="plf:field">
<label class="plf:field-label" for="input-readonly">Readonly</label>
<input class="plf:input" id="input-readonly" name="username" type="text" value="contributor" readonly aria-describedby="input-readonly-help">
<p class="plf:field-help" id="input-readonly-help">You can focus and copy this value.</p>
</div>import { Field, Input } from '@pelatform/react';
export function Example() {
return (
<Field label="Readonly" help="You can focus and copy this value.">
<Input name="username" type="text" defaultValue="contributor" readOnly />
</Field>
);
}import { Field, Input } from '@pelatform/react/prefixed';
export function Example() {
return (
<Field label="Readonly" help="You can focus and copy this value.">
<Input name="username" type="text" defaultValue="contributor" readOnly />
</Field>
);
}Unavailable values are excluded from form data.
<div class="field">
<label class="field-label" for="input-disabled">Disabled</label>
<input class="input" id="input-disabled" name="unavailable" type="text" value="Unavailable" disabled aria-describedby="input-disabled-help">
<p class="field-help" id="input-disabled-help">Unavailable values are excluded from form data.</p>
</div><div class="plf:field">
<label class="plf:field-label" for="input-disabled">Disabled</label>
<input class="plf:input" id="input-disabled" name="unavailable" type="text" value="Unavailable" disabled aria-describedby="input-disabled-help">
<p class="plf:field-help" id="input-disabled-help">Unavailable values are excluded from form data.</p>
</div>import { Field, Input } from '@pelatform/react';
export function Example() {
return (
<Field label="Disabled" help="Unavailable values are excluded from form data.">
<Input name="unavailable" type="text" defaultValue="Unavailable" disabled />
</Field>
);
}import { Field, Input } from '@pelatform/react/prefixed';
export function Example() {
return (
<Field label="Disabled" help="Unavailable values are excluded from form data.">
<Input name="unavailable" type="text" defaultValue="Unavailable" disabled />
</Field>
);
}JavaScript
Input itself needs no JavaScript; its examples use Button’s loading state and the error helper, which needs no init(). The Full script (pelatform-ui.min.js) starts every component on page load, with no code of your own; with ESM imports, call init() yourself, as in Installation. For markup you add later, see Dynamic content.
<link rel="stylesheet" href="pelatform-ui.min.css">
<script src="pelatform-ui.min.js" defer></script><link rel="stylesheet" href="core/pelatform-ui-core.min.css">
<link rel="stylesheet" href="components/button.min.css">
<link rel="stylesheet" href="components/form.min.css">
<script src="core/pelatform-ui-core.min.js" defer></script>
<script src="components/button.min.js" defer></script>
<script src="components/field.min.js" defer></script>
<script>document.addEventListener('DOMContentLoaded', () => PLF.init());</script><link rel="stylesheet" href="pelatform-ui.prefixed.min.css">
<script src="pelatform-ui.min.js" defer></script><link rel="stylesheet" href="core/pelatform-ui-core.prefixed.min.css">
<link rel="stylesheet" href="components/button.prefixed.min.css">
<link rel="stylesheet" href="components/form.prefixed.min.css">
<script src="core/pelatform-ui-core.min.js" defer></script>
<script src="components/button.min.js" defer></script>
<script src="components/field.min.js" defer></script>
<script>document.addEventListener('DOMContentLoaded', () => PLF.init());</script>| Method | Behavior |
|---|---|
PLF. |
On any form control inside a root: shows message in the element its aria-errormessage names, sets aria-invalid="true" and adds the message to aria-describedby. The next edit of the control, a reset of its form or an empty message clears all three |
With ESM, import Field from @pelatform/ui/components/field.
React
Input from @pelatform/react renders this native input, Field the field around one control and FieldGroup the fieldset around several; the control inside takes the label’s id, the help and error as its description, aria-invalid with an error and required, so you write no ids.
| Component | Prop | Values | Default |
|---|---|---|---|
Input |
size |
sm, lg |
The default size |
Field |
label |
The label (field-label), which names the control |
None |
Field |
id |
The control’s id, which the label names; without a label or id, each control keeps its own, so one Field can describe several |
A generated id |
Field |
required |
true makes the control required and marks the label (field-required) |
false |
Field |
requiredLabel |
The label’s mark when required |
(required) |
FieldGroup |
legend |
The legend (field-legend), which names the group |
None |
Field, FieldGroup |
help |
Help text (field-help), which describes the control, or each control in the group |
None |
Field, FieldGroup |
error |
An error message after the help (field-error), which makes the controls invalid; start it with “Error:” |
None |
The other props are the element’s own, an input’s, a div’s or a fieldset’s, className and ref included.
CSS variables
Override on a root or on a single control; Input, Textarea, native selects and Input Group share them.
| Variable | Default |
|---|---|
--plf-input-height |
Minimum height: 2. (36px); 2rem (32px) with input-sm, 2. (40px) with input-lg; when set, it takes precedence over the size classes |
--plf-input-bg |
--plf-color-field; read-only and disabled inputs use their own surfaces. Set --plf-input-border with it when the two must match |
--plf-input-color |
--plf-color-text |
--plf-input-border |
--plf-color-input |
--plf-input-hover-border |
--plf-input-border, otherwise --plf-color-border |
--plf-input-invalid-border |
--plf-color-danger |
--plf-input-radius |
--plf-radius-lg, 0. (8px) |
--plf-input-shadow |
--plf-shadow-xs |
--plf-input-font-weight |
--plf-font-weight-normal (400) |
--plf-control-font-size |
--plf-font-size-sm, 0. (14px), at every size |
--plf-input-focus-width, --plf-input-focus-color |
--plf-focus-width (2px), --plf-focus-color |
--plf-input-focus-offset |
0px, so the ring sits just outside the border |
--plf-field-gap |
--plf-space-2, 0. (8px), between a field’s parts and below a legend |