Rebuilding AgnosticUI Using Lit Web Components – Frontend Masters Blog

Rebuilding AgnosticUI Using Lit Web Components – Frontend Masters Blog

3 Min Read

It’s Fall 2025. I’m exhausted with my tech stack, and my side projects aren’t progressing. I started doubting if I even enjoyed web development anymore.

So, I did what any developer would: opened the console and indexed a lookup table with Math.random():

“`javascript
const options = [
“Grind LeetCode. Hate life. Land FAANG.”,
“Hard pivot to PM or Design.”,
“Quit. Live off the land.”,
];

const nextMove = options[Math.floor(Math.random() * options.length)];
“`

There was one issue with that.

`!options.includes(correctAnswer)`

I devised a better move for myself: finish what I’d started. So, I revisited AgnosticUI, a project from 2020 that needed a modern update.

The first version addressed branding consistency across React, Vue, Svelte, and Angular. However, maintaining JSX, Vue, and Svelte SFC components, along with ViewEncapsulation in sync across a single CSS source, was a maintenance nightmare. I almost archived the repo but felt I had unfinished business.

Some discussions with Cory LaViska (creator of Shoelace) directed me towards Web Components as the right solution.

**The Plan**: A complete rewrite using Lit with the following mandates:

– **Close the loop**. Double v1 component coverage. Finish what I started.
– **Platform over Framework**. Use Lit and Web Components as the base. Embrace the platform.
– **Disciplined AI**. While AI aids in speed, own the architecture to avoid a messy codebase.
– **Focused Scope**. Make hard choices, narrow scope: no data grids, no complex versioning. Just complete it.
– **Zero expectations**. Accept that this might reach nobody and yield nothing.

The work needed to be the core focus.

### Web Components in 2026

Framework compatibility concerns once shadowed Web Components. The website custom-elements-everywhere.com tracks this, and as of 2026, the scores are impressive. React 19 gets a perfect score, though @lit/react still improves developer experience (DX) significantly.

### Encapsulation Without a Black Box

As a novice with Lit web components, the main question came up quickly: encapsulation is great, but how can consumers customize anything?

The answer is `::part`. Encapsulation benefits by maintaining visual consistency across components, thanks to the Shadow DOM boundary, and is crucial for a design system. However, users still need styling hooks for essentials like colors and padding. CSS custom properties offer partial solutions, exposing `–ag-*` tokens for user overrides.

`::part` creates intentional gaps in that boundary:

“`xml

Users can now directly target those parts from outside the shadow DOM:

“`css
ag-input::part(ag-input) {
border-radius: 999px;
border-color: hotpink;
}
“`

No leaking internals. No `!important` wars. Clean styling hooks only.

Here’s a minimal working example showing the token override and `::part` approach together:

“`xml

:root {
–ag-space-2: 0.5rem;
–ag-space-3: 0.75rem;
–ag-border-subtle: #cbd5e1;
–ag-text-primary: #0f172a;
–ag-background-primary: #ffffff;
–ag-font-size-sm: 0.875rem;
}

ag-input::part(ag-input) {
border-radius: 999px;
border-color: hotpink;
}

ag-input::part(ag-input-label) {
font-weight: 700;
color: hotpink;
}

“`

### The A11y Trade-Off

The example’s `label` is inside the shadow root, breaking the usual HTML `for`/`id` convention. Shadow DOM disrupts this link. The workaround: keep all form control components within the shadow DOM, using intern-generated IDs:

“`xml

“`

Users can’t move the `label`, but `part=”ag-input-label”` allows restyling.

### The Final Frontier: Form Participation

Shadow DOM a11y trade-offs noted. Another challenge remains: native form participation.

`static formAssociated = true` appears as intent but merely signals the browser. Full implementation needs `

You might also like