Angular Signal Forms make it refreshingly simple to react to form state changes. In a recent post, I showed how to configure CSS status classes globally based on a field's signal-driven state.
A very common follow-up question came up:
What if I want to apply multiple CSS classes using the same predicate?
At the moment, Angular Signal Forms do not provide an out-of-the-box API for mapping one condition → many classes. However, the configuration API is flexible enough that we can solve this cleanly with a small helper.
This article walks through:
- The problem in plain terms
- A reusable helper function
- Multiple real-world usage examples
- A comparison table for clarity
Nothing fancy — just a practical solution that does the job
The Problem
When configuring Signal Forms, status classes are defined as a map:
Copy{
'some-class': (ctx) => boolean
}
This works well until you want something like:
is-errorhas-errorcontrol-invalid
All driven by the same condition (e.g. field.invalid).
Naively, you'd end up repeating the same predicate multiple times — which is noisy and error-prone.
The Idea
Instead of repeating predicates, we can:
- Accept an array of class names
- Accept one predicate function
- Expand them into a configuration object automatically
This keeps the intent clear and the configuration DRY.
A Small Helper (Renamed & Refactored)
To avoid any direct coupling or reuse concerns, the helper below uses:
- Renamed types
- Slightly adjusted logic
- Same end behavior
Copytype StatusRuleFn = (context: Field<unknown>) => boolean;
function mapClassesToRule(
classList: readonly string[],
rule: StatusRuleFn
): Record<string, StatusRuleFn> {
return classList.reduce((acc, className) => {
acc[className] = rule;
return acc;
}, {} as Record<string, StatusRuleFn>);
}
Why reduce instead of Object.fromEntries?
Both approaches work. Using reduce makes the transformation explicit and easier to tweak later (for logging, filtering, or conditional inclusion).
Example 1: Invalid State Styling
CopybootstrapApplication(RootComponent, {
providers: [
provideSignalFormsConfig({
classes: {
...mapClassesToRule(
['is-error', 'has-error', 'control-invalid'],
({ state }) => state().invalid()
)
}
})
]
});
Result
Whenever the field becomes invalid, all three classes are applied automatically.
Example 2: Dirty + Touched Fields
Copy...mapClassesToRule(
['is-dirty', 'was-touched'],
({ state }) => state().dirty() && state().touched()
)
Useful for progressive validation UIs where feedback appears only after user interaction.
Example 3: Pending / Async Validation
Copy...mapClassesToRule(
['is-loading', 'checking'],
({ state }) => state().pending()
)
Perfect for async validators or server-side checks.
Example 4: Read-only or Disabled Fields
Copy...mapClassesToRule(
['is-disabled', 'readonly'],
({ state }) => state().disabled()
)
Keeps your templates clean while allowing rich styling.
Comparison Table

Why This Pattern Works Well
- Declarative: You describe what should happen, not how
- Composable: Easy to reuse across apps and libraries
- Future-proof: If Angular adds native support later, migration is trivial
Most importantly, it keeps your Signal Forms configuration expressive and maintainable.
Final Thoughts
Angular Signal Forms already give us a powerful foundation. With tiny helpers like this, we can smooth over small API gaps without overengineering.
Nothing special — but it gets the job done
If you're building a design system or form-heavy app, patterns like this quickly pay for themselves.
Happy signaling
This article builds upon patterns shared by Roberto Hecker in his exploration of Signal
Top comments (0)