Shoelace 2.0 release: UI toolkit that works with all frameworks or none at all
shoelace.style
shoelace.style
A couple days ago, I released Shoelace 2.0, an open source library of common UI components.
These components work with any framework, can be loaded via CDN, are fully customizable with CSS (no preprocessor required), and install easily with a simple script + stylesheet. They were built with Stencil.js, which is a fantastic tool. The end result compiles down to vanilla web components.
Each component was designed from scratch to be lean, customizable, and easy to use. Accessibility is a common question folks have about component libraries. I’m definitely not an expert here, but I've spent a lot of time trying to get it right. I would like to echo the experts and say that accessibility only starts with components, but hopefully having a good foundation to build on will encourage others to think about it more at higher levels.
I hope you'll take a second to check it out! I'm happy to answer any questions you have about the project, how it's built, etc.
Also there are small version of buttons but none for other form controls which is quite odd.
Seems nice and lightweight overall. I would love to see a dark version too.
I can't seem to reproduce this for other components, but I'll check again.
> Also there are small version of buttons but none for other form controls which is quite odd.
Button, input, textarea, and select all have size variations. It's missing from checkboxes, radios, and switches but I'm not sure how useful that is. Feel free to submit an issue so folks can vote on it.
Edit: this has been identified and fixed in dialogs!
From Stencil's website [1]:
> Stencil is a compiler that generates Web Components (more specifically, Custom Elements) and builds high performance web apps. Stencil combines the best concepts of the most popular frameworks into a simple build-time tool.
> Stencil takes features such as Virtual DOM, async rendering (inspired by React Fiber), reactive data-binding, TypeScript, JSX, and Static Site Generation (SSG) and then generates standards-based Web Components and web apps with these features baked in.
Bootstrap is great for when I just want to quickly prototype and build a decent looking interface, but it feels confined when I start wanting to style things myself. When designing MPAs I typically use Bootstrap.
Tailwindcss is great for styling anything and everything, but I find sometimes I waste too much time styling little things that other CSS frameworks already have nice defaults for (and I am a terrible designer so this compounds my dificulties). I often use tailwind for SPAs.
Forgive me if my question is a little naive, but where could I fit Shoelace in for pet projects? Do you see it as being a suitable replacement for one of these usecases? Complementing them? Does it lie somewhere in the middle?
One thing Shoelace doesn't provide right now is a set of base styles. I've considered offering these as an opt-in CSS class or a <sl-prose> component, but I'm not sure it belongs tightly coupled with the library.
On one hand, it makes sense to provide a single "theme" that styles both components AND the underlying website/app. On the other hand, this will make themes very opinionated and potentially overbearing. I still need to think about this more.
In the meantime, you can use something like Tailwind's typography plugin [1] to handle base styles and use Shoelace in lieu of Bootstrap. IMO Shoelace + Tailwind is a great combination for those who prefer utility classes.
To style the internals of a Shoelace component, you can target it in a stylesheet using CSS custom properties or CSS parts as described in the docs. [1]
<button class="btn btn-outline-secondary">...</button>
would be like: <bs-button outline color="secondary">...</bs-button>
While this doesn't sound all that useful, I think this would be very handy for the interactive components like popovers, tooltips, carousels that need a lot of tag soup and javascript: <bs-popover>
Hello World
</bs-popover>I'm new to web components though, so I was surprised when I saw in the docs that you need to use the CSS `::part` selector to style components, rather than simply using more "traditional", well-known CSS. Is this a result of using a shadow DOM? If so, what benefits does using a shadow DOM use for these components?
Thanks!
- Customizations can be made to components with explicit selectors, such as ::part(icon), rather than implicit selectors, such as .button > div > span + .icon, that are much more fragile.
- The internal structure of a component will likely change as it evolves. By exposing component parts through an API, the internals can be reworked without fear of breaking customizations as long as its parts remain intact.
- It encourages us to think more about how components are designed and how customizations should be allowed before users can take advantage of them. Once we opt a part into the component's API, it's guaranteed to be supported and can't be removed until a major version of the library is released.
1. https://shoelace.style/getting-started/customizing?id=compon...
I had another look at the docs, but I still don't see anything about the benefits of shadow DOM for this library (unless the benefit is the "part thing"?)
The concept can get confusing, especially once we start talking about how styles reflect on slotted content (i.e. elements in the "light DOM") vs. shadow DOM elements, but that's probably more relevant to component developers rather than consumers.
¹ Requiring JavaScript harms accessibility, especially for web pages (as distinct from web apps). It makes pages take longer to load, not be as spider-friendly, and be more fragile when JavaScript fails to load, whether deliberately or accidentally—and simple network failures and the likes break things more often than you might imagine. I myself browse the web with JavaScript disabled via uMatrix, mostly because it speeds the web up a lot on average. Privacy improvements are a distant secondary reason. When I encounter pages that require JavaScript, I either give up on them or open them in a Private Browsing window, where I don’t have the uMatrix extension enabled; or if it’s something I expect I may be using more than once or twice, I see about more precise whitelisting in uMatrix.
I concur that normal content like news articles should still be accessible without activating any JS at all.
The idea of real web components not bound to any of these fancy JS frameworks that die and get replaced so quickly nowadays is amazing in my opinion.
People talk about the good of web components not tied to any framework, but in practice it doesn’t tend to pan out that way. These things are a framework themselves, so now you probably have a whole extra framework for every such thing you use (though admittedly each will be a lot lighter than React or similar), and it forces you to work in a different way from what you’re used to within your main framework. People routinely end up wanting (or sometimes needing, especially when React is the root framework) tighter integrations between their frameworks, so they go wrapping the web components in a new layer, e.g. to get better types and tooling support, or to route events through their own system rather than the plain DOM events system, or to do something or other involving types that aren’t just strings. That last one is a major limitation and shortcoming of Web Components as a fully-general solution: if you’re passing in attributes, for example, it’s strings all the way, but people often want and sometimes need more complex things. As an example where this causes substantial inefficiency, imagine a chart element that needs an array of data; if it’s straight web components, you’ll be nudged firmly towards turning it into a bunch of elements, or serialising it as JSON and setting it as an attribute, where setting it as a property (something you can only do via JS, so you’ve lost the declarative nature of the element) will be way more efficient.
- file uploads, the browser fails to indicate that an upload is in progress, the user clicks "submit" again and again because they don't see that the browser is uploading,
- file uploads when part of a form with other fields: if some text field doesn't validate, then the form will be shown again and the user will have to upload his file again, unless you go through the length of partially saving the form ... in which case you give up on having a nice transaction that saves the form at once
- selects with at least thousands of choices, they will freeze the browser, you need an autocomplete
So, as long as your website requires a file upload, or offers to select an option for a foreign key on a table with a lot of data then JS is required. Now, that might only be the case for a page or two, that doesn't prevent the rest of your site from working without JS because webcomponents aren't as invasive as typical JS frameworks that basically want to own all of your page.
If you really want to pass JSON to your webcomponent it's pretty easy:
<your-component><script type="text/json" slot="your-data">.. dump your json here</script></your-component>
But, you could also render your stuff directly and let the webcomponent inspect the DOM and build its own data structure.
Anyway, I fail to see the shortcoming here.
WebComponents are not tied to any framework once they are built, or, if they were coded without.
Shoelace doesn't require loading of any framework, but it's built with the brilliant StencilJS library: required to build, not to use.
For me the only shortcoming is with browser based ES modules but they are being fixed with the ImportMap proposal.
JavaScript itself is a workaround - it shouldn’t be the solution.
What exactly do you suggest to fix select ? Even if it was parsing a JSON blob, you still have thousands of results, an autocompletion endpoint seems much more efficient.
File uploads are just fine without JavaScript. Sure, some users will fail to notice the throbber and status bar text that indicate that the browser is sending the request and hit the submit button again. Unfortunate, but hardly destructive.
And then if the form doesn’t validate? Yes, you should partially save the form. You typically have to do something like that when you implement all your validation in JavaScript anyway, because client-side validation is typically imperfect, and there are extra possibilities of failure on the server. The robust way of doing uploads in APIs of this style has always been to upload a blob, and then pass its ID to the form, so that you still have it if something goes wrong.
Select with zillions of choices: that’s a terrible case for a select anyway; I’d be inclined to leave it as a text box and enhance from that. Purely out of interest (not because I would recommend it), I just ran an <input list=x><datalist id=x> with 10,000 options and it performed quite tolerably in Firefox. (When I type something that matches all 10,000 options it takes as much as a few hundred milliseconds to start displaying options, but it seems to load the options to present progressively, otherwise it’d have blocked for much longer than just a few hundred milliseconds.) But that means you’ve got to transfer all the options up-front, which might be otherwise unnecessary. Anyway, again you could implement it otherwise and I can even see how a full autocomplete (just, y’know, manual autocomplete) could work without JavaScript, using an iframe and formtarget=_parent. You’ve roused my interest, I’m going to put this on my list of fun technical demonstrations to make just because I can. I have a SSR/CSR hybrid framework in mind to make which will handle these sorts of things properly, using client-side scripting where beneficial and available but working as close to perfectly as possible without it. But I certainly wouldn’t recommend anyone go to the effort of trying to make something like this work without framework support; that’s a recipe for pain. In the short to medium term, this is a case where it’s quite reasonable to require JavaScript, because the framework tooling to handle it otherwise and do it well with both server-side and client-side rendering just doesn’t exist.
In my comment I was not saying that JavaScript shouldn’t be used, but that in general you should make things so that they work in the absence of JavaScript, even if imperfectly.
My remark on Web Components shortcomings wasn’t about passing JSON—which you can do in an attribute without difficulty, no need for a text/json script in general—it was at the fact that you need to do something like that, because all you have for representing things is DOM nodes. The shortcoming is that you can’t pass data structures and objects by identity to your component like you might with components in systems like React, Vue, Angular or Svelte. Instead, you have to serialise and deserialise constantly, which is wasteful, or else dodge out of its declarative nature and use properties instead of attributes, which is rather contrary to the design and concept of Web Components (and would broadly be anathema to Declarative Shadow DOM, contrasted to the likes of Svelte SSR + rehydration can if necessary relink things where object identity and exact data structures are valuable).
I think you understand what I meant by “framework”. React is a framework: it requires that you write your code in such-and-such a way, upon which it offers such-and-such functionality. Web Components is a framework. StencilJS targets the Web Components framework, and by virtue of extra runtime code it provides or must generate, it’s kind of a framework itself.
Aside from an SEO perspective, I don't understand why this is still such a popular perspective. JavaScript is so ubiquitous that turning it off is akin to removing the wheels from a car and expecting it to drive. Perhaps some cars will, but would you really want to take a trip in it?
To your point, I tried disabling JS on Amazon recently and I was able to make a purchase, but the website felt broken, the product preview carousel didn't work, and I definitely didn't want to use it like that again. IMO, you can't disable JavaScript in 2020 and have a good web experience.
But as you say, as soon as you want to do something like a form that's a bit complicated then of course some browser side interactivity is going to be very helpful.
Thanks to you for sharing your code ! We're lucky to have Shoelace and you ;)
As an additional bonus I can get trough reading an entire article without newsletter signup modals being shoven down my throat.
For sites I use regularly and I as a user benefit from more interactivity not just the advertisers, I hit the toggle switch to activate js.
But I dont think this is a particularly popular or that common a perspectice. If it werent for fear of a SEO penalty the web would be much less usable without js activated.
It's one of the reasons I really like Hacker News.
Also, as soon as you want to do something that's a bit more complicated than displaying a textarea and a handful of inputs or selects in a profile page, then JS is going to help a improving UX a lot ...
TBH it feels to me that webcomponent pages load faster than Vue/React/Angular sites, so at least that's an interesting compromise.
I absolutely understand this point. It's the most frustrating thing about this decade of the web or, really, ever since browsers disabled popups.
However, I don't think this is a case against JavaScript as much as it is about poor web design practices. This is what happens when websites force feed visitors garbage to get clicks and when developers add things without regard to performance.
For me this was just a "hack" to significantly improve my browsing experience. While I'm not advocating people doing the same, I disagree with the sentiment that absolutely everything is an app and static documents being an archaic concept.
One of my gripe is with people claiming that the absolute priority is the user and their experience, while all they really care about is conversion numbers and how quickly the devs can churn out new features.
And the other aspect is the same why I don't like this image so popular with product managers
https://blog.crisp.se/wp-content/uploads/2016/01/Making-sens...
While I do get and agree with the main point they are making, the image also implies that the more complex a product is, the better. But a car is not inherently better, more useful than a bicycle. The latter will outperform it in many use cases.
I see it the same with websites/apps. If you are building an browser based photoshop alternative, by all means throw all the shiny tools and frameworks at it, I'm willing to let it load a little and I'm not expecting it to work blazingly fast on a 50$ phone and a spotty 3G connection. But for a textual content based website, maybe consider letting the user just hop on and get going.
And I do think framework independent web components are the way to go for this btw.
And it doesn't cut it for a foreign key: it can't even have a separate option value and label like the basic select ... Well, it sort of can, but then the user sees the value of the option (the related row id in the context of a CRUD) and not the label ...
Does that mean that when I choose "something" and go back to my form again i will see "123" because "123" is the id of "something" on the remote row ?
I must be doing it wrong ... otherwise, it's far from acceptable UX.
Yes, this could be a problem if the label is not unique. But if that could happen you already had problems, because how was the user supposed to tell which “ACME Inc.” they were supposed to click on?
Also I know users would be happier if I could display some kind of image along with the labels like an avatar, to make a form (boring by definition?) more lively. Do you really believe it was a mistake to make select fields accept a different label and value ? If not, then why don't the input+datalist do the same ?
And even then, what's the equivalent of a select multiple based on an input+datalist ? Please, forgive my ignorance, but I don't understand how to allow multiple selection with a datalist and an input and no JS, without having the user submit the form for each choice they want to add.
I to filter the datalist choices based on another field value without JS ? for example, a country field that would filter users.
How can I have a datalist of countries and a datalist of cities, so that you get to choose your city based on the country ? Do I have to omit the Country field and show 300k cities ? Do you realize how many cities are named "Springfield" ? The datalist would have to propose like 50 results at the time and the user would have to search through them (assuming their browser could load the 300k cities without crashing), wouldn't it be much better to first have a country and then state or region selector to make it easier for them ?
I'm not trying to create problems, I'm trying to find solution for problems that users actually have ... And I have a repo that does /just/ that with a very specific backend framework and has 1400+ stars so the problem seems pretty real to me ...
You can typically get a really long way with just plain text boxes, and then if they produce an invalid or probably-incorrect value, prompt them “did you mean such-and-such?” (and either force them to select a correct value or allow them to say “no, I really meant what I typed”). In fact this would sometimes be better than the types of combo boxes or autocompleting dropdowns people often use, which often have poorly-implemented failure flows (e.g. if you have to choose a valid value and it’s fetching valid values from the backend, and it makes you wait a second until the response comes back, ever time, or else it’ll forget you typed anything). And it can combine with live JS-powered validation if desired, or not.
Especially for addresses, I find the approach of allowing almost freeform text entry and then proposing ways of massaging it into shape to be excellent. Either one totally freeform field, or a series of text boxes depending on the country—and isn’t it an issue that we often put the country field last even when it can affect the understanding of earlier fields! But it’s technically more difficult to do this and requires extra functionality probably on the backend or via some external API to do well (to say nothing of countries with, shall we say, massage-resistant addresses), so software developers tend to do things the technically-trivial way rather than the more adventurous way that, if done well, will be better for users. (And perhaps it’s just as well, because if done badly it would be much worse.)
But really, most of what I’m talking about is just thinking about the no-JS behaviour, particularly when designing things from scratch. When you have an existing system that already doesn’t want to work that way you’re kinda stuck, but greenfield developments are a good time to lay down the right foundations.
I have found I write far less markup than bootstrap (despite I only have a few custom css), can do custom styling easily, can do theming easily, etc. And with htmx I could move nearly 95% of all to the server side, yet have close to the same experience than with vue.
Considering that a lot of stuff still need to travel to the backend have the logic on the client have not been that useful for my needs.
My only gripe? I need to use npm to run the production builds with tailwind for have less size, but, I have (finally!) small css!
I've never used web components before, and I use SSR a lot (ASP. NET Core) - are you unable to use SSR with all web components, or only those that use shadow DOM? What's the reason I can't just render an `<sl-whatever>` and render the `value` attribute at the server?
Apologies if I've misunderstood or got some of this wrong!
So if you have a component looking like this in your dev tools:
<sl-emoticon which="smile">
#shadow-root (open)
:-)
</sl-emoticon>
Its HTML serialisation will still be <sl-emoticon which="smile"></sl-emoticon>, never with a :-).Look, there are ways around this lack of serialisation of shadow roots; the examples in https://github.com/skatejs/ssr succinctly show early experiments in the field, where it serialises the shadow root as an element, which it then turns into a shadow root with an inline script—but if the JavaScript doesn’t run, that won’t work and you’ll be left with a weirdly broken element, so I disqualify it and say “Shadow DOM is incompatible with SSR”.
But for Web Components in general, there’s nothing stopping you serialising them with their light DOM, though if your component goes adding children to itself when or after you connect it you might need your connectedCallback to detect if this has effectively already happened (on the server side), and wire things up instead of changing its children. This process (wiring a JS-side component tree to an already-existing DOM, rather than rendering from scratch) is known as rehydration and applies to more than just Web Components.
I am in love with the tooltip :)
I personally appreciated the introduction to Custom Elements and an explanation of the philosophy of the framework. So much of what we do as developers involves tight coupling and lock-in to a full-stack.
It's basically a law: anything RDF touches, will fail to get adoption.
Another place where “components” generally fall-over is when a component needs to act as a parent or container for arbitrary markup: yes, there’s the “slot” system but those are still defined and controlled by the component, not the consumer - even though ultimately it’s the consumer’s application.
I compare the situation to the component ecosystem for VB6 and WinForms - which to be fair was a lot worse as consumers often had little to no control over how those components rendered, appeared or behaved - but (no pun intended) elements of the same problem exist today.
Another example is WPF - which has major unaddressed issues still - which to be fair is worlds apart from WinForms in terms of what it’s templating system is capable of. A nice thing about WPF was that a component (Control’s) XAML was transparent and editable at design-time in VS, so the consuming application’s developer could get what they want out of it, the catch was that WPF templates are horrendously complex: getting VS to expose the XAML for even a <Button> would dump 50KB+ of markup into your application XAML because it was an all-or-nothing system and you needed XAML for every button state and behaviour - right down to the pulsating button glow effect that was only ever used in Windows Vista...
For example, Shoelace's <sl-select> component mimics the browser's <select> component. It has single select, multi-select, size variations, shape variations, type-ahead selection, validation states, etc. This component is a composition of <sl-dropdown>, <sl-input>, <sl-menu>, etc. and its entire source is less than 500 LOC (including doc comments and prettier formatting).
This is significantly less than many independent <select> alternatives I've seen, and the component itself doesn't have to worry about positioning, styling, etc. because that's handled by lower-level components.
> Another place where “components” generally fall-over is when a component needs to act as a parent or container for arbitrary markup: yes, there’s the “slot” system but those are still defined and controlled by the component, not the consumer - even though ultimately it’s the consumer’s application.
With web components, slotted content stays in the light DOM and is therefore controlled by the user. It's actually kind of a pain point when building them because, internally, you don't have that much control over arbitrary markup. (You can access markup through slot.assignedNodes() and style things with ::slotted() but it's not what I'd call a first-class experience for component developers.)
So it's probably likely that it's younger developers (such as myself) that are used to using emoji and like to use a bit of emoji to add colour and personality to the page.
I'm rather stereotyping here though, it could be some older person who just loves emoji too
It'll happen to you!
It's definitely high up on the list of components that are challenging to get right!
Anyway, that's the feature to pre-render everything: https://stenciljs.com/docs/hydrate-app
EDIT: don't understand the downvotes, seeing as we built Stencil. Stencil can definitely be used in a more traditional server-side environment like ASP.Net or PHP
(I did not downvote you, and someone has downvoted me as well, it seems)
What is it exactly that you can't pre-render ? That's not very clear to me. You can put any kind of content inside your web-component tags:
<your-paragraph><p>your test</p></your-paragraph>
If the browser is recent enough to support TLS 1.3 (which we all want to move on too don't we !) then it is recent enough to support web-components for sure.
So, that's pretty much how I see it: browsers have to upgrade for security reasons, if we're getting features like the ability to do some components without having to load a fat frontend framework, components that integrate perfectly with my favorite server side framework, then my question is why: why should we not haz nice things ? we're getting them for free after all ! Without even webpack to make a 1MB bundle ! I'd like your help to better understand what's the bad news here ...
I’m talking about the document becoming interactive and operative without or before JavaScript is loaded. Shadow DOM is not part of the HTML serialisation, so anything using Shadow DOM fails this test. Fixing this is what Declarative Shadow DOM research project is about.
Agreed the shadow dom didn't seem ready for me neither, but I feel that we're getting there, and meanwhile it's hopefully optional.
Just tested with Firefox 78 and verified it's working now. You may have an older cache.
UIkit sprinkles CSS and JS on top of existing HTML elements: <div uk-dropdown></div>
Shoelace defines web components, which are custom elements: <sl-dropdown>
You can probably achieve that with UIkit if you make one bundle per page with webpack or something, but with web components such as Shoelace you get that for free. So there's that, wether it matters to you or not is up to you of course.
https://stenciljs.com/docs/browser-support/
It seems it is necessary for browsers that have been abandoned and that are never going to have TLS 1.3 support: IE11, Edge 16-18
Personally I'd leave it just to push users onto abandoning those browsers that will never support TLS 1.3.