Skip to main content
Version: Current

Forms Quick Start

Build a working form in 5 minutes. By the end you will have a validated form that shows field errors and a loading state on submit.

What you will build

Preview · Quick Start — Contact Form

Step 1 — Install

npm install primus-react-ui @primus/ui-core    # React
npm install primus-angular-ui @primus/ui-core # Angular

Step 2 — Import styles

// main.tsx
import '@primus/ui-core/dist/primus-ui.min.css';
import 'primus-react-ui/styles.css';

Step 3 — Build the form

import { useState } from 'react';
import { PrimusInput, PrimusTextarea, PrimusButton } from 'primus-react-ui';

export function ContactForm() {
const [form, setForm] = useState({ name: '', email: '', message: '' });
const [errors, setErrors] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);

const set = (key: string, val: string) =>
setForm(f => ({ ...f, [key]: val }));

const validate = () => {
const e: Record<string, string> = {};
if (!form.email) e.email = 'Email is required';
else if (!/\S+@\S+/.test(form.email)) e.email = 'Enter a valid email';
if (!form.message) e.message = 'Message is required';
return e;
};

const handleSubmit = async () => {
const e = validate();
if (Object.keys(e).length) { setErrors(e); return; }
setLoading(true);
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(form),
});
setLoading(false);
};

return (
<div className="card" style={{ maxWidth: 440, padding: '1.75rem' }}>
<h2>Get in touch</h2>
<p>We will get back to you within 24 hours.</p>

<PrimusInput
label="Full name"
value={form.name}
onChange={(v) => set('name', v)}
/>

<PrimusInput
label="Work email"
type="email"
required
value={form.email}
onChange={(v) => set('email', v)}
error={errors.email}
/>

<PrimusTextarea
label="Message"
rows={3}
required
value={form.message}
onChange={(v) => set('message', v)}
error={errors.message}
/>

<PrimusButton
style={{ width: '100%' }}
loading={loading}
onClick={handleSubmit}
>
Send message
</PrimusButton>
</div>
);
}

What just happened

FeatureHow it works
Styled inputs@primus/ui-core styles <input>, <textarea>, <select> automatically — no classes needed
Error displaydata-field="error" + aria-invalid="true" → red border + error text below the field
Loading stateloading={true} on PrimusButton → spinner + disabled, prevents double-submit
Field label<label> wrapping <input> (HTML) or label prop (React/Angular)
Required marker<span style="color:var(--danger)">*</span> in label, or required prop

Next steps

Version history

See the Changelog for version history and breaking changes.