Component System
@saastro/forms is UI-agnostic. It doesn’t bundle a UI kit — you provide the primitives (inputs, buttons, selects, …), typically from shadcn/ui. This page explains the three ways to inject components, how auto-discovery works, and utilities for detecting missing components.
Three Modes
1. Zero-Config (Recommended)
Pass components directly to the <Form> component. Only provide the components your form actually needs.
import { Form } from '@saastro/forms';
import { Input, Button, Label, Checkbox } from '@/components/ui';
import { Field, FieldLabel, FieldDescription, FieldError } from '@/components/ui/field';
import { FormField, FormControl } from '@/components/ui/form';
<Form
config={config}
components={{
Input,
Button,
Label,
Checkbox,
Field,
FieldLabel,
FieldDescription,
FieldError,
FormField,
FormControl,
}}
/>;
This is the simplest approach. No setup, no context providers. If a component is missing, you’ll see a helpful warning with install instructions. (Submission is configured on the form config itself — see the Submit guide.)
2. Glob Auto-Discovery (Vite projects)
Use FormComponentsProvider with Vite’s import.meta.glob to automatically discover all your shadcn components. Set it up once in your root layout:
import { FormComponentsProvider } from '@saastro/forms';
export default function RootLayout({ children }) {
return (
<FormComponentsProvider components={import.meta.glob('@/components/ui/*.tsx', { eager: true })}>
{children}
</FormComponentsProvider>
);
}
Then use <Form> anywhere without passing components:
<Form config={config} />
The provider extracts all named exports from your components/ui/ directory and builds a registry automatically. Named exports are registered under their export name; default exports are registered under the PascalCased file name (radio-group.tsx becomes RadioGroup).
3. Legacy Provider
For apps with many forms, wrap your tree with ComponentProvider and a full registry:
import { ComponentProvider, createComponentRegistry } from '@saastro/forms';
import * as ui from '@/lib/form-components';
const registry = createComponentRegistry(ui);
<ComponentProvider components={registry}>
<App />
</ComponentProvider>;
createComponentRegistry() accepts an object with all component exports and returns a typed ComponentRegistry.
The legacy provider is marked
@deprecatedin favor of zero-config. It still works and will continue to work.
How Resolution Works
When a field renders, the system calls useComponents() to get the registry. Resolution order:
- Legacy mode — If
ComponentProviderexists in the tree, use its full registry - Zero-config mode — If
<Form components={...}>orFormComponentsProviderexists, use that partial registry - Error — If neither exists, throw with a helpful message showing both setup options
The zero-config and legacy providers can coexist. Legacy takes precedence for backward compatibility.
ComponentRegistry
The full registry has 49 component slots organized by category:
| Category | Components |
|---|---|
| Inputs | Input, Textarea, Button, Label |
| Checkbox & Switch | Checkbox, Switch |
| Radio | RadioGroup, RadioGroupItem |
| Select | Select, SelectTrigger, SelectContent, SelectItem, SelectValue |
| Native Select | NativeSelect |
| Slider | Slider |
| Popover | Popover, PopoverTrigger, PopoverContent |
| Tooltip | Tooltip, TooltipTrigger, TooltipContent, TooltipProvider |
| Separator | Separator |
| Dialog | Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription |
| Command | Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem |
| Input OTP | InputOTP, InputOTPGroup, InputOTPSlot |
| Accordion | Accordion, AccordionItem, AccordionTrigger, AccordionContent |
| Calendar | Calendar |
| Form | FormField, FormControl |
| Field | Field, FieldLabel, FieldDescription, FieldError |
You don’t need all 49. Only provide the components your form’s field types require. Five of them are needed by every form — the package exports them as coreComponents:
import { coreComponents } from '@saastro/forms';
// ['Button', 'Field', 'FieldLabel', 'FieldDescription', 'FieldError']
Use getRequiredComponents() to find out exactly which components a given form config needs (it always includes the five core ones).
Missing Component Handling
When a field tries to render but a required component isn’t in the registry, it shows a MissingComponentFallback — a styled warning with:
- The field name and type
- The list of missing components
- A
npx shadcn@latest add ...command to install them
import { MissingComponentFallback } from '@saastro/forms';
// Or create placeholder components (component name + field type):
import { createMissingComponentPlaceholder } from '@saastro/forms';
const PlaceholderInput = createMissingComponentPlaceholder('Input', 'text');
Hooks
Four hooks expose the component registry. All are importable from @saastro/forms and follow the same resolution order described above (legacy registry first, then zero-config).
useComponents()
function useComponents(): ComponentRegistry;
Returns the active registry. Throws when no provider is found — the error message explains both setup options (zero-config components prop vs. ComponentProvider). Use it in components rendered inside a <Form> (or under a provider), such as custom field renderers:
import { useComponents } from '@saastro/forms';
function MyCustomField() {
const { Input, Button } = useComponents();
return <Input placeholder="Custom field" />;
}
Note that the return type is the full ComponentRegistry even in zero-config mode, where only a partial registry was provided — components your form doesn’t use may still be missing at runtime.
usePartialComponents()
function usePartialComponents(): PartialComponentRegistry;
Same resolution as useComponents(), but never throws — it returns an empty object {} when no provider exists. Every slot is optional, so check before rendering:
import { usePartialComponents } from '@saastro/forms';
function OptionalTooltip({ children }) {
const { Tooltip } = usePartialComponents();
if (!Tooltip) return children;
return <Tooltip>{children}</Tooltip>;
}
useHasComponentProvider()
function useHasComponentProvider(): boolean;
Returns true if any component provider (legacy or zero-config) is present in the tree. Useful for rendering a setup hint instead of crashing.
useComponentMode()
function useComponentMode(): 'legacy' | 'zero-config' | 'none';
Reports which injection mode is active: 'legacy' when a ComponentProvider registry is present, 'zero-config' when components come from <Form components={...}> or FormComponentsProvider, and 'none' when there is no provider at all.
Utility Functions
mergeComponentRegistries()
Merge multiple partial registries. Later registries override earlier ones.
import { mergeComponentRegistries } from '@saastro/forms';
const merged = mergeComponentRegistries(baseRegistry, overrides);
parseGlobModules()
Convert Vite’s import.meta.glob result into a component registry. Used internally by FormComponentsProvider.
import { parseGlobModules } from '@saastro/forms';
const modules = import.meta.glob('@/components/ui/*.tsx', { eager: true });
const registry = parseGlobModules(modules);
withComponents()
Higher-order component that injects a components prop (the full registry from useComponents()) into the wrapped component. Like useComponents(), it throws when no provider exists.
import { withComponents } from '@saastro/forms';
const FieldWithComponents = withComponents(MyField);
// MyField receives `components: ComponentRegistry` as a prop
ComponentResolver
Singleton for async component resolution with caching. Useful for advanced setups with code-split components. It only resolves components you pre-cache or register an importer for — resolve() returns null for anything else.
import { getComponentResolver, configureComponents } from '@saastro/forms';
// Pre-cache components you've already imported:
configureComponents({ Button, Input, Label });
// Or register lazy importers for code splitting:
const resolver = getComponentResolver();
resolver.registerImporter('Calendar', () =>
import('@/components/ui/calendar').then((m) => m.Calendar),
);
const button = await resolver.resolve('Button'); // ComponentType | null
Related
- Utilities —
getRequiredComponents(),getMissingComponents(),getInstallCommand() - Types Reference — Full
ComponentRegistryinterface - Installation — Initial setup