Why GitHub standardised on Web Components
GitHub’s front end has grown from a small jQuery-based codebase into roughly 85,000 lines of code owned by hundreds of engineers across dozens of teams. That scale pushed the team toward better encapsulation, and Web Components—native browser technologies for building custom HTML elements—turned out to be the right fit. They offered portability and isolation without forcing a framework buy-in, and they could be adopted incrementally alongside existing infrastructure.
The first custom elements shipped in 2014: <relative-time> and <local-time>, which render dates in friendly formats, and <include-fragment>, which lazy-loads HTML fragments. Success with those led to replacing whole design patterns, such as swapping the legacy “facebox” modal for <details-dialog>. Today GitHub maintains more than a dozen open-source Web Components under the GitHub Elements umbrella, plus dozens more closed-source ones.
The authoring toolkit
With so many engineers contributing, GitHub needed tooling that encodes best practices rather than relying on manual review. Three pieces support that effort:
- ViewComponent pairs each backend Rails view with a single component abstraction, which maps naturally to a one-to-one relationship with a Web Component.
- Catalyst is GitHub’s open-source library that reduces Web Component boilerplate via TypeScript decorators. It was inspired by Stimulus and LitElement but tailored to GitHub’s internal needs. Developer feedback shows it substantially improves the authoring experience over legacy patterns.
- Linters: the team ships
eslint-plugin-githubfor general code practices andeslint-plugin-custom-elementsfor Web Component-specific checks. Internal tests also flag deprecated patterns; for example, one test rejects new “facebox” usage and suggests<details-dialog>instead.
class FaceboxDeprecationTest < Test::Fast::TestCase
EXPECTED_NUMBER_OF_FACEBOXES = 44
# Find facebox triggers set with rel=facebox, in either HTML attributes or
# as part of hash assignment (for rails helpers)
REGEX_FOR_FACEBOX_BINDING = %r|rel\s*[=:]>?\s*["']?facebox|
REGEX_FOR_DATA_FACEBOX = %r|data-facebox\s*=>?\s*|
def test_limit_facebox
actual_rel_facebox = grep(REGEX_FOR_FACEBOX_BINDING, options: %w[-In], paths: %w[app/**/*.erb])
actual_data_facebox = grep(REGEX_FOR_DATA_FACEBOX, options: %w[-In], paths: %w[app/**/*.erb])
count = actual_rel_facebox.count("\n") + actual_data_facebox.count("\n")
assert_operator count, :<=, EXPECTED_NUMBER_OF_FACEBOXES, <<-EOL
It looks like you added a facebox. Please use <details-dialog> instead.
If you must increment EXPECTED_NUMBER_OF_FACEBOXES in this test, please
/cc @github/ui-frameworks-reviewers in your pull request, as we may be able to help! Thanks.
EOL
assert_equal EXPECTED_NUMBER_OF_FACEBOXES, count, <<-EOL
It looks like you removed a facebox. YOU ARE AWESOME! 💖 💖 💖 💖 💖
Please decrement EXPECTED_NUMBER_OF_FACEBOXES in this test and treat yourself to
something special. You deserve it.
EOL
end
end
From prototype to open source
Web Components at GitHub follow a staged lifecycle. Development starts as a Catalyst component inside the monolith, where it can ship behind feature flags and be revised iteratively. Often a team has a specific use case, so the component may carry application-specific hard-coded options. That’s acceptable inside the monolith, but not for wider reuse.
When a component proves useful beyond its original context, GitHub extracts it. The first step is removing Catalyst-specific functionality and converting to a plain Web Component. This keeps the released component dependency-free: requiring an external contributor to learn Catalyst before submitting code back adds too much friction. A good extraction candidate must be:
- Free of third-party dependencies
- Framework- and library-agnostic
- Lightweight and style-free
- Single-purpose and decoupled from other components
A recent example shows the payoff. A product team built a terminal-inspired UI with a typing effect, initially reaching for typed.js. Tooling flagged that adding it would increase the page bundle fivefold. Working with the UI Systems team, they prototyped their own animation in Catalyst—under 40 lines of code and less than a day of work. Seeing broader potential, they refactored it to remove dependencies, ejected it from Catalyst, and open-sourced it as <typing-effect>. The released versions also include TypeScript definitions written as ES modules and a full test suite with the standard lint setup.
Results and what’s next
Internal developer surveys show the changes are landing well. Teams report that ViewComponent’s encapsulation makes UI easier to test and increases confidence, while Catalyst is seen as an approachable step up from older JavaScript patterns without a leap to a new framework.
GitHub continues to grow the GitHub Elements collection on github.com/github/github-elements, which syncs to their page on webcomponents.org. On the standards front, the team is watching the Template Parts and Declarative Shadow DOM proposals closely—both would address common pain points with the current Web Components model. GitHub has already shipped a ponyfill implementing the minimum viable Template Parts spec in production and hopes to see it gain broader community traction.



