Designing a shared UI library for several Angular applications

Angular, Shared UI, Design systems, Nx, Testing · Published 1 October 2026 · Updated 2 October 2026

The useful part

Share behaviour with a stable contract. Keep permissions, domain rules and application data at the product boundary.

Several Angular applications often grow similar data grids independently. The second one is slightly different, the third one has the filter bar from the first and the paging from the second, and a year later a bug in the sorting has to be fixed in three places. A shared UI library can help. The risks are familiar: the library knows too much about the first product, it owns behaviours that a framework already owns, and nobody can see all of its states in one place.

This is how I structured the shared layer behind my own product, twelve packages under one @ui scope, and the rules that keep it reusable.

UI catalog task grid with filtering, an expanded row and record details.
In practice / Own-product catalog with sample data. The toolbar, filters and details compose real library components; this is not a client application. Open the image for a larger view.

One job per package

The packages are small and named after their job: @ui/grid, @ui/forms, @ui/controls, @ui/upload, @ui/preview, @ui/details, @ui/communicator, @ui/commands, @ui/chat, @ui/map, @ui/service and @ui/catalog. A package that does two things has two reasons to change and two sets of consumers that break.

The boundary between them is a lint rule: packages may depend on each other, never on an application. The rule runs in CI, so a convenient import from the product into the library is a build failure, not a code review remark that gets waved through on a Friday.

Domain-free by construction

The libraries know nothing about the product that uses them. No parcel rules, invoice policies or hard-coded product language. Localized labels are supplied through the public API. The test is simple: could a banking back office or an ERP module use this package as it is? If a component needs a product-specific rule, the rule stays next to the screen that needs it and reaches the library through an input, a token or a registered strategy.

The API is neutral: English names, options instead of defaults borrowed from the first product, and coded errors that the application phrases in its own language. This is more work for the first consumer and far less for the second.

Example boundary: a grid in two products

A parcel screen and an invoice screen can share sorting, paging, column visibility and keyboard navigation. The parcel screen owns projection and land-register rules; the invoice screen owns approval and accounting rules.

An illustrative split; neither consumer is a claim of client work.
Shared gridProduct adapter
Stable row ID and selection stateMap parcel ID or invoice ID to the row
Loading, empty and error presentationsFetch permitted data and translate errors
Action slot and keyboard behaviourDecide who may approve, export or edit

A useful check: load the same grid with another domain’s sample records. If the grid imports that application to work, the boundary is incomplete. UI visibility never replaces server-side authorization.

One owner per behaviour

A shared library is tempting to fill with implementations of things that already exist. Resist it. In my packages:

  • keyboard, focus and ARIA behaviour come from Angular Aria and the CDK;
  • grid data and state belong to TanStack Table, wired in a single file;
  • form state belongs to Signal Forms.

The library adds composition, layout and the product-independent conventions, not a second implementation of focus management. When a behaviour has one owner, a fix has a clear home. Consumers still need to update, run their checks and deploy before a dependency patch reaches users.

Pure core, Angular view

Each package keeps a pure core, its model, state and rules with no view imports, apart from its Angular view. The core is tested in jsdom in milliseconds and can be reused by a different view, a test harness or a server-side process. The view is thin: it renders the core's state and forwards events.

The split also decides what a unit test is. A grid's filter logic is a core test with plain objects. The filter bar's keyboard behaviour is a view test, and only the states that depend on real layout go to a browser.

The look comes from tokens only

Every colour, radius, spacing and font in the libraries is a --ui-* custom property. A product restyles the whole layer by overriding properties in one stylesheet; it never reaches into a component's styles. Two products on the same packages can look unrelated, and a change of brand is a change of tokens.

Device rules are decided in the library once. Touch targets are 44 px on coarse pointers and compact under a mouse; no screen in any application has to remember that.

A living catalog, not a style guide

A style guide drifts from the code the week after it is written. A catalog is an application that imports the real packages and shows every pattern on the same sample data: 53 pages in seven groups, from data and tables to windows and feedback, with pretend servers where a backend would be.

The catalog has three jobs. It is where a new state is designed, because an empty grid and a grid with ten thousand rows are both one page away. It is the surface the tests drive. And it is the demo that a product team looks at before asking for a new component. It ships in development builds only, so the product's production bundles do not carry it.

Tests for important states

The same four checks run over every package and are the difference between a library and a folder of components:

  1. Unit tests in jsdom. The 1 October 2026 inventory counted 2,524 test declarations across the twelve packages. This describes scope, not the outcome of a fresh run. Targeted mutation checks: break the code, see it fail, restore.
  2. Scenarios per pattern. The same dated inventory records 819 Playwright scenarios driving the catalog as a user would, with the keyboard as well as the pointer.
  3. Accessibility across recorded states. axe checks catalog states at two viewport widths, including open overlays. Automated checks cover only part of accessibility: keyboard, focus order, screen-reader output and task completion still need manual assessment.
  4. Visual baselines. Twenty-two screenshots of catalog and application screens, desktop and phone, compared only on the CI agent, where fonts and rendering are stable.

A catalog page that adds a demo to a shared page can break neighbouring tests through page-wide locators. Scope locators to the demo's container from the start; it costs nothing on day one and saves a day later.

Versioning and release

In one workspace, the libraries and the applications are released together: a change in @ui/grid is tested against every consumer in the same gate, and there is no version matrix to maintain. That is the simplest model and it is the one I use.

Publishing the packages for teams outside the workspace changes the contract: semantic versions, a changelog per package, an agreed deprecation and migration policy, and a demo application that pins the published versions rather than the source. Choose the model before the second team arrives, not after.

Where to start in an existing code base

Start with one repeated behaviour and name its second consumer. Extract its stable contract; leave speculative options and product policy outside. The GeoAtlas workspace uses a library-first rule for behaviour already known to be reusable. The first package will be small and will absorb one duplicated behaviour. The second will absorb the next. After a few screens the catalog has its first pages, the lint rule has its first boundary, and the applications have stopped diverging.

The Reusable UI case study shows the result of that rule applied for one product; the architecture review service is where I help a team apply it to theirs.

Sources and scope

Package and test figures are the dated 1 October 2026 inventory in the case study. They are not a fresh passing test run or a claim of complete accessibility.