Building Composable Form Fields in Angular

don't judge the design by how many lines of Angular code it takes compared with React\. Judge it by whether the resulting API is clear, whether the integration is reliable, and whether future consumers can compose it without understanding the internal plumbing\.

Recently, I worked on integrating our design system with Angular Signal Forms.

The initial goal seemed straightforward: create a form field component that accepts a label, a description and an input, then handles validation states and error messages automatically.

We already had the fundamental components. We had input directives for form controls, an input box for visual elements such as borders, prefixes and suffixes, and a component that wrapped a label, description and input box.

It seemed like a relatively simple integration task.

It turned out to be a much more interesting problem about component boundaries, composition and communication.

Separating responsibilities

The first thing I realised was that our existing form field component wasn’t really a form field.

It was a UI wrapper. It provided a label, description and input box, but it had no connection to the actual form state.

The input directive handled the control itself. The input box handled its visual presentation, including borders, prefixes and suffixes. The wrapper simply brought these elements together.

These responsibilities are different, and I think they should remain separate.

The input should own its control behaviour. The input box should own its visual shell. The form field should handle field-level concerns such as labels, descriptions, validation messages and accessibility relationships.

This distinction also explains why the input box doesn’t need to replace the input.

In React, we sometimes wrap an input and forward its props through the wrapper. Angular doesn’t provide an equivalent general-purpose mechanism for transparently forwarding arbitrary properties to a child component or element.

But that isn’t necessarily a problem. The input box is not the input. It is a container around the input, and the two components have different responsibilities.

Once I stopped treating this as a limitation that needed to be worked around, the component boundaries became clearer.

Why composition became complicated

The next decision was how to structure the form field itself.

One option was to keep the label, description and error message inside a single component. Another was to make them independently composable:

1
2
3
4
5
6
7
8
9
<app-form-field>
<app-form-field-label>
Amount
</app-form-field-label>

<input appInput />

<app-form-field-error />
</app-form-field>

I prefer the composable approach for good reasons.

Different consumers may need different layouts. A label might eventually need different presentation. Some fields might not need descriptions, while others might need additional content. We can also provide preset components for common cases without taking flexibility away from consumers who need something different.

The preset becomes a convenient API built on top of reusable components, rather than a separate implementation with duplicated behaviour.

However, this flexibility introduces another problem: how do these components communicate?

The label and error components need access to the form field’s state. The form field needs to understand the associated control’s validation state. The input box may also need to know whether the field is invalid so it can change its border styling.

These components are separate, but they participate in the same interaction.

In Angular, I ended up introducing an injection token as a shared context. The form field obtains the relevant state, exposes the context, and the label and error components inject it when they need that information.

The implementation is reasonable, but it is considerably more involved than I initially expected.

Why does this feel easier in React?

React makes this style of composition feel natural.

A compound component can use context to share state between its parent and children. Hooks can encapsulate the logic for accessing that context. Components can be composed, omitted or rearranged without requiring every piece of state to be passed through intermediate components.

Angular has dependency injection, content projection and queries, so the necessary building blocks exist. But we need to connect these mechanisms explicitly.

The difference is not that React eliminates the underlying complexity. React’s context and component model make this particular communication pattern easier to express.

In Angular, I have to think more deliberately about how the parent discovers the relevant form state, how child components access it, and how the integration works with the form API.

This is especially noticeable when the component is intended to work with form state rather than simply render a visual layout.

Composition has a cost

This experience reminded me that composition is not free.

We often prefer composition over configuration because it produces smaller components with clearer responsibilities. Instead of creating one component with many inputs, variants and conditional layouts, we let consumers assemble the pieces they need.

That is still a useful principle.

But there is a cost that is easy to overlook: the more independently composable components become, the more coordination they may require.

A configurable component has complexity in its public API. A composable component can have complexity in its internal communication.

Neither is inherently better. The right choice depends on the problem being solved.

For example, independently composable label and error components are valuable if consumers genuinely need to control their placement or presentation. If every consumer uses exactly the same layout, a single form field component may be simpler.

The important question is not whether a component can be made composable. It is whether the flexibility justifies the additional complexity.

A better boundary, not necessarily fewer components

I don’t think the answer is to abandon composition or combine everything into a single component.

Instead, I want to keep the responsibilities explicit:

  • Input: owns control behaviour and integration with the form API.

  • Input box: owns the visual shell, including borders, prefixes and suffixes.

  • Form field: owns field-level presentation, validation messages and accessibility.

  • Preset components: assemble these pieces for common use cases.

The composable API remains available for consumers who need more control. Presets make the common cases straightforward.

The implementation details can be more involved than the public API suggests, but that is acceptable as long as the internal coordination remains well-defined and the complexity does not leak into every consumer.

There is also a limit. If each new component requires another context token, another state bridge and another layer of coordination, it is worth revisiting the boundaries.

The takeaway

Composition is a powerful way to design reusable components, but it does not automatically make a system simpler.

The goal is not to maximise the number of small components. It is to create components with clear responsibilities and give them just enough communication to work together.

Sometimes the hard part of component design isn’t deciding what each component should do. It is deciding how independently designed components should understand each other.

That is the problem I thought would be easy when I started integrating our design system with Angular forms.

I think the naming distinction is important here: your existing component is a visual layout wrapper, while the new one has actual form-field semantics and integrates with form state. Calling both FormField would hide that difference.

For the existing component, FormItem is a reasonable candidate, although it can also imply a semantic form field in some design systems. FieldLayout would communicate its presentation-only responsibility more explicitly. I’d use FieldLayout for the UI-only wrapper and reserve FormField for the component that handles form state, validation messages and accessibility.

Here’s a draft for your technical blog, focusing on the architectural lesson rather than just the Angular implementation details.

Composition Is Easy Until Components Need to Talk

Recently, I worked on integrating our design system with Angular Signal Forms.

The initial goal seemed straightforward: create a form field component that accepts a label, a description and an input, then handles validation states and error messages automatically.

We already had the fundamental components. We had input directives for form controls, an input box for visual elements such as borders, prefixes and suffixes, and a component that wrapped a label, description and input box.

It seemed like a relatively simple integration task.

It turned out to be a much more interesting problem about component boundaries, composition and communication.

Separating responsibilities

The first thing I realised was that our existing form field component wasn’t really a form field.

It was a UI wrapper. It provided a label, description and input box, but it had no connection to the actual form state.

The input directive handled the control itself. The input box handled its visual presentation, including borders, prefixes and suffixes. The wrapper simply brought these elements together.

These responsibilities are different, and I think they should remain separate.

The input should own its control behaviour. The input box should own its visual shell. The form field should handle field-level concerns such as labels, descriptions, validation messages and accessibility relationships.

This distinction also explains why the input box doesn’t need to replace the input.

In React, we sometimes wrap an input and forward its props through the wrapper. Angular doesn’t provide an equivalent general-purpose mechanism for transparently forwarding arbitrary properties to a child component or element.

But that isn’t necessarily a problem. The input box is not the input. It is a container around the input, and the two components have different responsibilities.

Once I stopped treating this as a limitation that needed to be worked around, the component boundaries became clearer.

Why composition became complicated

The next decision was how to structure the form field itself.

One option was to keep the label, description and error message inside a single component. Another was to make them independently composable:

1
2
3
4
5
6
7
8
9
<app-form-field>
<app-form-field-label>
Amount
</app-form-field-label>

<input appInput />

<app-form-field-error />
</app-form-field>

I prefer the composable approach for good reasons.

Different consumers may need different layouts. A label might eventually need different presentation. Some fields might not need descriptions, while others might need additional content. We can also provide preset components for common cases without taking flexibility away from consumers who need something different.

The preset becomes a convenient API built on top of reusable components, rather than a separate implementation with duplicated behaviour.

However, this flexibility introduces another problem: how do these components communicate?

The label and error components need access to the form field’s state. The form field needs to understand the associated control’s validation state. The input box may also need to know whether the field is invalid so it can change its border styling.

These components are separate, but they participate in the same interaction.

In Angular, I ended up introducing an injection token as a shared context. The form field obtains the relevant state, exposes the context, and the label and error components inject it when they need that information.

The implementation is reasonable, but it is considerably more involved than I initially expected.

Why does this feel easier in React?

React makes this style of composition feel natural.

A compound component can use context to share state between its parent and children. Hooks can encapsulate the logic for accessing that context. Components can be composed, omitted or rearranged without requiring every piece of state to be passed through intermediate components.

Angular has dependency injection, content projection and queries, so the necessary building blocks exist. But we need to connect these mechanisms explicitly.

The difference is not that React eliminates the underlying complexity. React’s context and component model make this particular communication pattern easier to express.

In Angular, I have to think more deliberately about how the parent discovers the relevant form state, how child components access it, and how the integration works with the form API.

This is especially noticeable when the component is intended to work with form state rather than simply render a visual layout.

Composition has a cost

This experience reminded me that composition is not free.

We often prefer composition over configuration because it produces smaller components with clearer responsibilities. Instead of creating one component with many inputs, variants and conditional layouts, we let consumers assemble the pieces they need.

That is still a useful principle.

But there is a cost that is easy to overlook: the more independently composable components become, the more coordination they may require.

A configurable component has complexity in its public API. A composable component can have complexity in its internal communication.

Neither is inherently better. The right choice depends on the problem being solved.

For example, independently composable label and error components are valuable if consumers genuinely need to control their placement or presentation. If every consumer uses exactly the same layout, a single form field component may be simpler.

The important question is not whether a component can be made composable. It is whether the flexibility justifies the additional complexity.

A better boundary, not necessarily fewer components

I don’t think the answer is to abandon composition or combine everything into a single component.

Instead, I want to keep the responsibilities explicit:

  • Input: owns control behaviour and integration with the form API.

  • Input box: owns the visual shell, including borders, prefixes and suffixes.

  • Form field: owns field-level presentation, validation messages and accessibility.

  • Preset components: assemble these pieces for common use cases.

The composable API remains available for consumers who need more control. Presets make the common cases straightforward.

The implementation details can be more involved than the public API suggests, but that is acceptable as long as the internal coordination remains well-defined and the complexity does not leak into every consumer.

There is also a limit. If each new component requires another context token, another state bridge and another layer of coordination, it is worth revisiting the boundaries.

The takeaway

Composition is a powerful way to design reusable components, but it does not automatically make a system simpler.

The goal is not to maximise the number of small components. It is to create components with clear responsibilities and give them just enough communication to work together.

Sometimes the hard part of component design isn’t deciding what each component should do. It is deciding how independently designed components should understand each other.

That is the problem I thought would be easy when I started integrating our design system with Angular forms.

Show the actual Angular boundaryMake the design-system lesson explicit

Comments