Styling
@saastro/forms provides multiple ways to customize the appearance of your form fields. You can apply CSS classes to different parts of each field, control sizes, visibility, and dark mode. For grid layout configuration (columns, gap, breakpoints), see the Layout System page.
classNames() Method
The classNames() method allows you to add CSS classes to specific parts of a field:
.addField('email', (f) =>
f.type('email')
.label('Email')
.required()
.classNames({
wrapper: 'bg-gray-50 p-4 rounded-lg',
input: 'border-2 border-blue-500 focus:border-blue-700',
label: 'text-lg font-bold text-blue-900',
error: 'text-red-600 font-medium',
helper: 'text-gray-500 italic',
})
)
Available Class Targets
| Target | Description | Applied To |
|---|---|---|
wrapper | Container element | <div> wrapping the entire field |
input | Input control | <input>, <textarea>, <select>, etc. |
label | Field label | <label> element |
error | Error message | Error text below the field |
helper | Helper text | Help text below the field |
JSON Configuration
{
"type": "email",
"label": "Email",
"wrapper_className": "bg-gray-50 p-4 rounded-lg",
"input_className": "border-2 border-blue-500",
"label_className": "text-lg font-bold",
"error_className": "text-red-600 font-medium",
"helper_className": "text-gray-500 italic",
"schema": { "required": true, "format": "email" }
}
Field Order
Control the display order of fields (useful for responsive reordering):
.addField('sidebar', (f) =>
f.type('text')
.label('Sidebar')
.required()
.order({ default: 2, lg: 1 }) // Second on mobile, first on desktop
)
Two things to keep in mind:
- Ordering only applies in manual layout mode (
.layout('manual')). In auto mode, per-field order and column spans are ignored — see Layout System. - In a JSON config, set order under the field’s
layoutkey:"layout": { "order": { "default": 2, "lg": 1 } }. A top-levelorderproperty exists in the types but is not read by the runtime.
Field Size
Set the size of input fields:
.addField('search', (f) =>
f.type('text')
.label('Search')
.required()
.size('sm')
)
There are five size variants, with md as the default. They are applied as Tailwind utility classes to text inputs and input groups (icons inside inputs scale to match); textareas receive the same padding and text-size classes without the fixed height:
| Size | Classes applied |
|---|---|
xs | h-6 px-2 text-xs |
sm | h-8 px-3 text-sm |
md | h-10 px-3 py-2 text-base (default) |
lg | h-12 px-4 text-lg |
xl | h-14 px-4 text-xl |
The size() builder method is currently typed as 'sm' | 'md' | 'lg'. To use xs or xl, set the size property in a JSON field config:
{
"type": "text",
"label": "Search",
"size": "xl",
"schema": { "required": true }
}
Options Layout (Groups)
For fields that render a grid of options (checkbox-group, switch-group, button-radio, button-checkbox, button-card), control the options grid with optionsClassName. The default is "grid grid-cols-1 gap-2", except button-radio, which defaults to "grid grid-cols-2 gap-2":
{
"type": "checkbox-group",
"label": "Interests",
"optionsClassName": "grid grid-cols-2 md:grid-cols-3 gap-4",
"options": [...]
}
The equivalent builder method is .optionsClassName('grid grid-cols-2 md:grid-cols-3 gap-4').
Hiding Labels
Hide the label while keeping it accessible for screen readers:
.addField('search', (f) =>
f.type('text')
.label('Search') // Still needed for accessibility
.hideLabel() // Visually hidden
.placeholder('Search...')
.required()
)
Responsive Visibility
Hide fields by breakpoint:
.addField('mobileOnly', (f) =>
f.type('text')
.label('Mobile Field')
.required()
.hidden({ default: 'visible', lg: 'hidden' }) // Hidden on desktop
)
.addField('desktopOnly', (f) =>
f.type('text')
.label('Desktop Field')
.required()
.hidden({ default: 'hidden', lg: 'visible' }) // Hidden on mobile
)
To hide fields based on the values of other fields instead, see Conditional Logic.
Complete Example
const config = FormBuilder.create('styled-form')
.layout('manual')
.columns(12)
.gap(6)
.addField('name', (f) =>
f
.type('text')
.label('Full Name')
.placeholder('John Doe')
.required()
.size('lg')
.columns({ default: 12, md: 6 })
.classNames({
wrapper: 'bg-white shadow-sm rounded-lg p-4',
input: 'text-lg',
label: 'text-primary font-semibold',
}),
)
.addField('email', (f) =>
f
.type('email')
.label('Email Address')
.placeholder('[email protected]')
.required()
.email()
.columns({ default: 12, md: 6 })
.classNames({
wrapper: 'bg-white shadow-sm rounded-lg p-4',
error: 'text-destructive font-medium',
}),
)
.addField('bio', (f) =>
f
.type('textarea')
.label('Bio')
.placeholder('Tell us about yourself...')
.rows(4)
.optional()
.columns({ default: 12 })
.classNames({
wrapper: 'bg-muted/50 rounded-lg p-4',
input: 'min-h-[120px]',
helper: 'text-xs text-muted-foreground',
})
.helperText('Optional - maximum 500 characters'),
)
.addStep('main', ['name', 'email', 'bio'])
.build();
Styling the Components Themselves
@saastro/forms ships no stylesheet and no CSS variables to override. The field UI — inputs, labels, error text, buttons — comes from the components you inject through the Component System. Beyond size() and classNames(), the deepest customization point is those components: restyle them or swap them out entirely.
Tailwind CSS Integration
All class names work with Tailwind CSS out of the box. Use any Tailwind utility classes:
.classNames({
wrapper: 'space-y-2 transition-all duration-200',
input: 'focus:ring-2 focus:ring-primary/50 hover:border-primary',
label: 'tracking-wide uppercase text-xs',
})
Dark Mode
Classes work with Tailwind’s dark mode:
.classNames({
wrapper: 'bg-white dark:bg-gray-800',
input: 'border-gray-300 dark:border-gray-600',
label: 'text-gray-900 dark:text-gray-100',
})