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

Don't Design Components. Discover Them.

Recently, while working on our Design System, I realized that most of our discussions weren’t really about implementation—they were about abstraction.

It started with what seemed like a straightforward request from design: implement a reusable Progress Tile component.

The design library presented it as a single component with four layout variations. At first glance, the implementation looked simple enough. We could build a configurable component with a handful of variants and expose enough props to support every layout.

But the more we explored the design, the more uncomfortable it became.

That experience led me to two important questions.

1. Configuration or Composition?

The easiest way to implement the Progress Tile would have been to create a highly configurable component.

Internally, it contained a Card, Progress, Button, Badge, images, labels, descriptions, and other elements. Every new variation introduced another configuration option.

Eventually, the API would start looking something like this:

1
2
3
4
5
6
7
8
9
<ProgressTile
cardProps={...}
progressProps={...}
buttonProps={...}
badgeProps={...}
image={...}
description={...}
...
/>

At first, this feels convenient.

Over time, however, the wrapper becomes tightly coupled to all of its children. Every time a child component evolves, the wrapper must expose another prop or another variant. The abstraction becomes harder to understand than the UI it was trying to simplify.

This reminded us of a lesson we’d already learned from some of our page components. They started simple, but gradually accumulated more and more configuration until they became difficult to evolve.

Instead of making configuration our default, we’ve adopted a different principle:

Prefer composition over configuration.

This doesn’t mean configuration is bad.

Configuration works well when a component has a stable responsibility and well-understood variations. Components like Button, Progress, or Badge are excellent examples.

Composition, however, is usually a better default while the design is still evolving.

A good example is a Dialog. We can provide a composable foundation:

1
2
3
4
5
<Dialog>
<Dialog.Header />
<Dialog.Body />
<Dialog.Footer />
</Dialog>

and then build opinionated components such as SimpleDialog or ConfirmationDialog on top of it.

Composition provides flexibility, while reusable components emerge naturally once the abstraction has proven itself.

2. Is It Actually a Component?

As we continued reviewing the Progress Tile, another question emerged.

Was it actually a reusable component?

The more we compared the four layouts, the harder it became to identify a stable structure.

Some layouts contained images, others didn’t.

Some placed labels above the progress bar, others beside it.

Descriptions appeared in different positions.

Actions moved around.

The only thing shared across every variation was a progress bar.

That wasn’t enough to define a reusable engineering abstraction.

Instead of asking:

“How should we implement this component?”

we found ourselves asking:

“Is there actually a component here to implement?”

Perhaps what design had created wasn’t an engineering component at all.

Perhaps it was a design pattern.

That distinction changed the entire discussion.

A design pattern absolutely belongs in the design system because it promotes consistency across products.

However, a design pattern doesn’t necessarily need to become a single React or Angular component.

Sometimes documenting a recommended composition of existing primitives is a better engineering abstraction than introducing another configurable component.

The Missing Review

This experience also highlighted something else.

Many teams have design reviews.

Many teams have code reviews.

But very few teams have abstraction reviews.

Someone needs to bridge the gap between design and engineering.

Their responsibility isn’t simply to translate Figma into code.

Their responsibility is to decide how a design should be represented in the design system.

When reviewing a proposed component, they might ask:

  • Is this a token, a primitive, a composite component, a pattern, or simply a business feature?

  • Does it have a clear, single responsibility?

  • Does it have a stable structure?

  • Does it encapsulate meaningful behavior?

  • Will its API remain stable as new use cases appear?

These questions are often more important than discussing props or implementation details.

Components Are Discovered, Not Designed

The biggest lesson I took away from this experience is that the hardest part of building a Design System isn’t implementing components.

It’s discovering the right abstractions.

Good components usually have three characteristics:

  • A clear responsibility.

  • A stable structure.

  • Well-defined behavior.

If those qualities don’t exist yet, forcing everything into a configurable component is usually solving the wrong problem.

Instead, start with smaller primitives, document patterns where appropriate, and allow reusable components to emerge as the design matures.

In other words:

Don’t design components. Discover them.