ismaelramos.dev

← Case studies

A cross-team component library in LitElement

Building a framework-agnostic component library that ships into Angular apps, AEM-rendered pages and SaaS partner integrations — so accessibility, theming and product polish stay consistent across teams in different countries.

Context

By year six on the platform, the product surface had grown past what a single Angular component set could cover. New product formats (courses, video podcasts, short-form video) needed components that also rendered inside AEM-authored pages and inside SaaS partner UIs. Different teams across countries were re-implementing buttons, modals, cards and form controls — with subtle inconsistencies in accessibility, focus management and theming. The bank’s design language was also evolving: every team applying it independently was a recipe for drift.

Constraints

Components had to work outside Angular — inside AEM templates, inside partner SaaS pages, inside marketing pages with their own stacks. Couldn’t impose a framework choice on partners. Had to play well with existing CSS architectures (some BEM, some utility-first, some legacy AEM CSS). Theming had to support multiple brand variants (group, regional, product-line). Multiple contributing teams in multiple countries meant the contribution model and review process were as important as the code itself.

Decisions

Build the library on LitElement and Web Components — framework-agnostic by design, lightweight enough to ship into pages with other frameworks already loaded, and a natural fit for AEM templates. Use Storybook as the single source of truth for components: not just a showcase, but the contract surface, the accessibility documentation and the contribution doc. Treat accessibility primitives as the core of the library, not a layer on top — every interactive component is a focus-management primitive first, a styled component second. Theme via CSS custom properties so partners can override without forking.

Implementation

Each component shipped with a Storybook entry covering states, variants, accessibility notes and keyboard behaviour — the entry is reviewed as part of the PR, not after. Focus management, ARIA wiring and keyboard interaction baked into base classes shared across components (modal, popover, menu, listbox, tabs). CSS custom properties for every visual token, with a documented brand-override API. CI runs axe assertions on the Storybook stories themselves — every component is accessibility-checked at PR time. Multiple teams across countries contribute via a documented RFC-light process: small additions through PRs, larger patterns through a short proposal.

Outcome

Cross-team consistency improved measurably — components that used to be re-implemented per team now have one home. External SaaS partners adopted components in their own integrations, which raised the accessibility floor across the whole product surface (including code we don’t directly own). The library became one of the artefacts other teams point at when explaining the bank’s frontend approach, which made the contribution model self-reinforcing.

Outcome metrics

Maintainability
Single home for shared visual + accessibility primitives
Developer experience
Storybook as contract surface for contributors across countries
Accessibility
Axe assertions on every component story in CI
Scalability
Used across Angular, AEM and partner SaaS UIs

Tradeoffs

We chose LitElement over a React component library exported via Web Components. The Web Component output from React libraries is fine in theory but tends to ship more runtime than Lit and to lose ergonomic features when wrapped. Lit’s “Web Components first” model meant the components behave the same in every host — no surprises in AEM, no hydration mismatches in partner stacks.

We chose Storybook as the contribution contract, not just as documentation. Every PR ships the story alongside the code; reviewers read the story to understand the change. The cost is that a component without a story doesn’t ship — and that’s the point.

We deliberately did not version the library as a monorepo of one-package-per-component. The maintenance overhead of dozens of packages didn’t pay for itself at our scale; a single library with semver and selective imports gave us 90% of the benefit with 10% of the operational cost.

Engineering challenges

  • Theming across brands — multiple regional brands and product-line variants meant the token system had to be deep but cheap to override
  • Coexistence with AEM CSS — the legacy AEM templates have their own selectors and specificity issues; the library had to be defensive about cascade
  • Contributor onboarding across countries — async-first contribution model with documented patterns, because synchronous design reviews don’t scale across timezones
  • API stability — once partners depend on a component, its API is a contract; breaking changes need migration paths

What I learned

A component library lives or dies by the contribution model. The code is easy compared to deciding who can add what, how, and on what schedule. We invested in the RFC-light process and in Storybook-as-contract long before we had the components to populate them, and it paid back every time a new team started contributing.

The other lesson: framework-agnostic isn’t an aesthetic choice, it’s a strategic one. The moment a SaaS partner could use the same button as our Angular app, the bank’s product surface got more consistent than any style guide could have made it.

What I would do differently

I’d ship the design tokens as a separate, versioned package from day one. We kept them inside the component library initially, which was fine until non-component consumers (AEM templates, partner CSS) needed the tokens without pulling the runtime. Splitting them later was straightforward but would have been free if we’d done it up front.

Future evolution

The next steps are SSR-friendly rendering for the components that show up in SEO-critical AEM pages, and a small set of AI-aware components (input fields with embedded assistant affordances, chat panels) for the AI features shipping into the platform. Beyond that, exploring whether the library is mature enough to open-source parts of it as a contribution back to the LitElement community.

Principles applied

  • Framework-agnostic where it matters, framework-specific where it helps
  • Accessibility as a primitive, not a layer
  • Documentation as contract, not as afterthought
  • Optimise the contribution model, not just the code

The accessibility primitives in this library are what made accessibility-by-default practical at scale — without them, every team would have re-implemented focus management. The library also ships components used by the AI features, so consistency carries through to the AI surface.