Introduction
Choco-ui is a Svelte UI-kit that will help you create reactive, accessible, SSR-ready, composable & extendable components by either using and customizing the provided components or by using the primitives they are built on to create your own.
You can customize:
- the UI components
- the headless components
- the mixins everything is built on
Let’s have a quick look at each level.
UI components
The easiest way to get started. Just customize the styles to your liking.
For example to use the Accordion
component:
<script lang="ts">
import * as Accordion from "$lib/components/accordion/index.js";
</script>
<Accordion.Root class="mx-auto max-w-md">
<Accordion.Item value="item-1">
{#snippet header()}
The Origins of Chocolate
{/snippet}
Chocolate has been around for over 3,000 years. The ancient Olmecs of southern Mexico were among
the first to turn cacao beans into a drink, which later became a prized beverage in Mayan and
Aztec cultures. It wasn’t sweet back then—it was often spiced with chili peppers!
</Accordion.Item>
<Accordion.Item value="item-2">
{#snippet header()}
Chocolate and Your Mood
{/snippet}
Ever wonder why chocolate makes you feel good? It’s packed with flavonoids, which are believed
to improve heart health, lower blood pressure, and even boost brain function.
</Accordion.Item>
<Accordion.Item value="item-3">
{#snippet header()}
The World’s Largest Chocolate Bar
{/snippet}
The world’s largest chocolate bar weighed over 12,770 pounds (about 5,800 kilograms) and was
created in the UK in 2011. It was about the size of an adult African elephant!
</Accordion.Item>
</Accordion.Root>
Feel free to open the components and modify the styles to suit your design. If you want to go to the next level and tweak the logic in a reusable way then you want to have a look at the corresponding headless component.
Headless components
Each UI component is paired with a corresponding headless component built from sharable building blocs and logic, and managing the attributes and the behavior.
When you instantiate a headless component you can use it with the choco
action. The preprocessor
takes care of spreading the attributes and managing actions for you.
Here’s an example using the headless ToggleButton
class to create an unstyled toggle button:
<script lang="ts">
import { choco } from "chocobytes";
import { ToggleButton } from "$lib/headless/toggle.svelte";
const toggle = new ToggleButton();
</script>
<p>
<!-- Just use the choco action and you're done -->
<button
use:choco={toggle}
onclick={() => console.log("still toggling")}
class="aria-pressed:text-dark rounded border border-white py-2 px-4 aria-pressed:bg-white"
>
I'm {toggle.active ? "" : "not"} pressed
</button>
</p>
<pre>{JSON.stringify(toggle.attributes, null, 2)}</pre>
{ "aria-pressed": "false", "type": "button", "value": "" }
In the above example there is no clash between the toggle’s inner click
event
listener and the one declared on the button. The ChocoBase
class and all headless components
pass their behavior through an action, avoiding clashes with other declarative listeners.
Mixins
The headless components are built from a few primitives. By combining these primitives you can easily create your own headless components and extend the provided ones.
These primitives are mixins. What’s a mixin? It’s like a class decorator, but we don’t officially have decorators in js yet, so mixins do the job with no additional setup or preprocessing.
So mixins are just functions taking a class and returning a decorated class with new attributes or new behavior. And since functions compose well together, they are nicely composable primitives.
To use a mixin we just extend from its application on the base class, and to compose them we just
compose the applications. For example, the Togglable
mixin adds an initTogglable
method taking the (initial) attributes to be toggled, whether this initial state is the active
state, and the events toggling it. So the headless ToggleButton
class could be implemented
like this:
import { ChocoBase } from "chocobytes";
import { Togglable } from "$lib/mixins/togglable.svelte.js";
export class ToggleButton extends Togglable(ChocoBase) {
constructor(options?: { active: boolean }) {
super();
const active = options?.active ?? false;
this.initTogglable({
initial: { "aria-pressed": `${active}` },
active,
toggle: "click",
});
}
}
You see how we could very easily adapt this to create a headless switch component, by toggling aria-checked
on click. Many things in a UI are togglable so this is a powerful abstraction. From there we can
also build a disclosure component by toggling aria-expanded
, a hoverable by toggling
on mouseenter
and off during mouseleave
. See the corresponding mixins
for more on these low level primitives.
Also notice how readable and short the code is.
Credits
This project draws inspiration from:
- Melt - https://melt-ui.com
- shadcn-svelte - https://www.shadcn-svelte.com
- HeadlessUI - https://headlessui.com
- ReactAria - https://react-spectrum.adobe.com/react-aria
- Skeleton - https://skeleton.dev