Introduction
Choco-ui is a UI-kit for Svelte 5 to 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.
We can describe the architecture in the following way:
You own the design, and we share the logic.
This is a hybrid approach where you literally own the component files and can modify them to suit your designs, and yet still get updates and bug fixes on the shared logic, headless classes and building blocks, all in a seamless way.
You can think of the provided components as implementation examples of the headless classes that
you can customize. So choco-ui
can be used both as a headless component library if you
do not copy the components, or as a UI library if you want a solid starting point and customize later
on.
The library is composed of components, headless classes, and the primitive building blocks everything is built on. You can use these building blocks to go beyond what’s provided and create your own headless classes.
Let’s have a quick look at these different parts.
UI components
The easiest way to get started. Once you’ve added the library, the install script will let you choose the components you want to copy to your project’s $lib/components
folder. Then you can easily modify these files and tweak their Tailwind
styles.
Here’s an example using the Accordion
component. The files live
inside $lib/components
and the implementation can be modified there. Again, you own the
components.
<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>
Headless classes
Each UI component is paired with a corresponding headless class built from the building blocks. This headless class is in charge of managing the attributes, the behavior, keyboard navigation, accessibility etc.
You can use these headless classes directly with the choco
action: the preprocessor takes care of everything 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 "chocobytes/headless/toggle.svelte.js";
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="rounded border border-white py-2 px-4 aria-pressed:bg-white aria-pressed:text-black"
>
I'm {toggle.active ? "" : "not"} pressed
</button>
</p>
<pre>{JSON.stringify(toggle.attributes, null, 2)}</pre>
In the above example there is no clash between the toggle’s inner click
event
listener and the one declared on the button. Internally, the ChocoBase
class everything inherits from manages behaviors
with actions, avoiding clashes with other declarative listeners.
Building blocks
The headless classes are built from a few building blocks, which allow for powerful code reuse and extensions. By combining these building blocks you can easily create your own headless classes. Here’s an overview:
The main building block is the
ChocoBase
class. All headless classes inherit from it and its role is to set the simple contract that all components share.Two conceptual classes help solve recurring UI patterns:
- The
Toggleable
class allows to toggle a set of attributes on specific events. This is quite versatile: almost every interactive component toggles attributes in some way. - The
Triggerable
class helps orchestrate a control-target relation. Think Tabs, Disclosures, Accordions etc.
- The
The last main abstraction is the
Group
mixin. It allows us to easily group things together as in toggle groups, tabs, accordions etc; track active elements and manage keyboard navigation.
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. It’s just a fancy word for a function taking a class and returning a decorated class with new attributes or new behavior.
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