This content originally appeared on Go Make Things and was authored by Go Make Things
Today, I want to talk about how to build frontend systems—design systems, UI libraries, and so on—that can be easily extended for use cases and situations you didn’t plan for.
Let’s dig in!
tl;dr: Lots of “hooks” in the form of CSS variables, cascade layers, web component attributes, and custom events.
The challenge
I’ve built, maintained, and worked with numerous design and UI systems at various companies.
One of the biggest challenges I see around adoption is that the teams working with them often need to use them in a way that they weren’t designed for. It starts to feel like you’re fighting your tools instead them helping you work better and faster.
So, teams will often just… stop using them and build their own thing instead!
A real-world example
Early in my career, I worked at Constant Contact. At the time, they had a unified product that was actually four different products, mostly acquired through acquisitions, run by four different engineering teams on four different tech stacks.
My team was working on a design system that would help create a more consistent and unified UI across all four products.
But as you can imagine, building something for four teams with four different sets of needs and four different technology suites meant that assumptions we would make about how something might be used would very quickly run into edge cases we hadn’t thought about or considered.
That meant that something that could make our engineers’ lives easier often wouldn’t.
So what can you do about that?
Is this something your team struggles with? I help build frontend architecture that’s cheaper, faster, and easier to maintain. Get in touch.
Hooks. Hooks everywhere!
WordPress is by no means the best or most modern system.
But one thing it did quite well very early on was add all sorts of hooks developer can use to customize and extend things without touching the core code.
This was the mindset I had when building out Kelp, my UI library for people who love HTML.
In most modern design systems, customizing things means you’re…
- Opening up Sass files and modifying SCSS variables.
- Adding a shit-ton of modifier classes to things.
- Going in an editing or overwriting JavaScript files directly.
Most systems are either rigid, and require you to crack them open and start editing stuff directly. Or they’ve tried to account for every possible edge case, so they’re bloated and heavy with lots of code you don’t personally need but someone someday might.
With Kelp, I tried to provide hooks: places you latch on and modifying things, without running a build or modifying the core.
So what does that look like?
CSS Variables
Any values that are shared across components get mapped to CSS variables.
In Kelp, that means there are variables for changing colors and fonts and sizes and more.
If you wanted to, for example, change the shade of blue that’s used across the entire site, you can do that with just one or two CSS variables.
:root {
--color-blue-hue: 241.55;
--color-blue-chroma: 0.7;
}
Instead of running a custom build every time, you can create a kelp.custom.css
file, drop in your changes, and you’re done.
Cascade Layers
One challenge with letting developers customize CSS is ensuring that their customizations load in the correct place relative to existing styles.
If you mess up the cascade order, things look weird.
Tools like React CSS Modules or Tailwind try to solve this by scoping styles directly to the components using them. This, of course, creates a lot of extra CSS!
Cascade layers solve this problem is a more browser-native way.
If you’re not familiar with them, the @layer
rule lets you define a handful of cascade layers and tell the browser the order in which to load them. You can add code to any layer at any time, and it will always be rendered by the browser in the predefined layer order.
That means you could have some base styles, so state-based effects, and some utilities in your main file…
@layer base, state, utilities;
/* The rest of the CSS... */
And later add some code to the base
layer in a separate file.
@layer base {
/* moar CSS... */
}
That prevents that new code from doing weird things like like negating the effects of your utility classes.
Historically, developers would slap !import
on those, and that creates a new set of problems. Cascade layers side-step that whole issue.
Kelp takes that a step further by exposing four public cascade layers that sit in-between some other layers used only by the core system.
It ensures that customizations will always load in the right place and override anything that’s part of Kelp core, without a build step.
@layer kelp.theme {
:root {
--color-blue-hue: 241.55;
--color-blue-chroma: 0.7;
}
}
HTML Attributes
My favorite thing about web components is that they’re self-instantiating.
Instead of doing this…
<div id="toc"></div>
const toc = new TableOfContents('#toc', {
levels: 'h2, h3',
label: 'On This Page...'
listStyle: 'ol'
});
You can do this…
<kelp-toc
levels="h2, h3"
label="On This Page..."
listStyle="ol"
></kelp-toc>
I know exactly from looking at my HTMl what a component is going to do.
And my JavaScript is smaller, because I don’t need to write different init scripts for different variations.
Like a good class-based library, a good web component provides properties for anything and everything someone might want to customize, with smart defaults if they don’t customize things.
In Kelp, I also add an [is-ready]
attribute to the component after it instantiates and completes its internal setup process.
You can hook into that for custom styling, like only showing certain elements once the JS is ready. Kelp also includes a [hide-until-ready]
component that will display: none
the component entirely until it’s loaded.
<kelp-toc
class="callout margin-end-2xl"
hide-until-ready
></kelp-toc>
By default, this component would render as an empty callout component with some big margin on the bottom.
Using [hide-until-ready]
, it doesn’t show in the DOM at all until loaded.
Custom Events
Yesterday, I added a tabs component to Kelp.
I immediately had someone reach out and ask if there was a way to lazy-load content in tabs that weren’t visible on initial load.
That’s not a feature of the component, and I don’t think I’d ever add it. But there is a way to add that feature yourself without touching the core code: custom events.
Every web component in Kelp emits custom events that you can listen for, and run code in response.
The <kelp-tabs>
component emits a kelp-tabs:select-before
event when a new tab is about to be toggled, and a kelp-tabs:select
event after it’s been toggled.
You can hook into this to add your own lazy-load script if you want.
Let’s say your HTML looks like this. Only the #wizards
tab has HTML in it by default.
<kelp-tabs>
<ul class="list-inline" tabs>
<li><a href="#wizard">Wizard</a></li>
<li><a href="#sorcerer">Sorcerer</a></li>
<li><a href="#druid">Druid</a></li>
</ul>
<div id="wizard" is-loaded>
Wizards gain their magic through study...
</div>
<div id="sorcerer"></div>
<div] id="druid"></div>
</kelp-tabs>
Whenever a new tab is about to be shown, you can fetch()
it’s content from the server and render it into the nextPane
using a humble addEventListener()
callback.
document.addEventListener('kelp-tabs:select-before', async (event) => {
// If content is already loaded, do nothing
if (event.detail.nextPane.hasAttribute('is-loaded')) return;
// Fetch content from the server
const request = await fetch('/path/to/content');
const response = await request.text();
// Inject content into the next content pane
event.detail.nextPane.innerHTML = response;
// Add the [is-loaded] attribute
event.detail.nextPane.setAttribute('is-loaded');
});
Kelp includes events both before and after interactions, with the *-before
events being cancelable.
That let’s you check for certain conditions, and prevent the interaction from completing if needed with the event.preventDefault()
method.
document.addEventListener('kelp-tabs:select-before', (event) => {
// Is user signed in?
if (user.isSignedIn) return;
// If not, block access to the tab and show a message
event.preventDefault();
showSignInModal();
});
What are some other use cases?
What other ways might people use this? I don’t know!
And that’s kind of the whole point.
When you build design systems and UI libraries—whether internal or public facing—people will want to use them in ways you haven’t considered or planned for.
CSS variables, cascade layers, web component attributes, and custom events provide hooks developers can use to customize your code without ever having to modify the core or the run a build step.
If you need help adding this stuff to you or your teams design system or library, get in touch. Or, enjoy Kelp, which is wrapping up its beta phase and will be hitting v1 very soon.
Like this? A Lean Web Club membership is the best way to support my work and help me create more free content.
This content originally appeared on Go Make Things and was authored by Go Make Things

Go Make Things | Sciencx (2025-08-04T14:30:00+00:00) Building extensible frontend systems. Retrieved from https://www.scien.cx/2025/08/04/building-extensible-frontend-systems/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.