Build your own Vue UI library with Unstyled PrimeVue Core and Tailwind CSS
Published: 2024-01-07 19:22:34

Wrapping UI components to encapsulate customized behaviors is a common pattern when building your own UI library, especially in larger scale teams where a shared library is utilized amongst numerous applications. A major advantage of this approach is decoupling the dependency to a 3rd party library since the consuming applications depend on the shared library instead. 

[PrimeVue](https://primevue.org) unstyled core and Tailwind CSS would be a perfect toolset if you require to build a custom UI library. The main idea is to create your UI component by wrapping a PrimeVue component, pass your props as fall through and configure the pass-through Tailwind preset locally instead of a global configuration.

## Toggle Switch

Let's build our own ToggleSwitch component inspired by Material Design. For this, we'll be using the PrimeVue InputSwitch component.

**Template**

The template section consists of a wrapper flex container, a built-in label and the PrimeVue InputSwitch. Notice the use of `v-bind="$attrs"` as we'd like to apply any property passed to our own component to the underlying InputSwitch e.g. `v-model` and `v-on` events. Component here is responsible for the main functionality and accessibility while we focus on standardizing the design requirements.

```html
<template>
    <div class="flex items-center gap-4">
        <label :for="$attrs.inputId">{{ label }}</label>
        <InputSwitch v-bind="$attrs" />
    </div>
</template>
```

**Script**
The script sets `inheritAttrs` as false otherwise the fall through properties are applied to the main div container element. The additional props such as label are included as the props of our own component.

```js
<script setup>
    defineOptions({
        inheritAttrs: false
    });

    defineProps(['label']);
</script>
```

**Preset**
So far so good as all the wiring is in place, time to apply our custom material design inspired style implemented with Tailwind utilities. The style is applied with the pt property of the InputSwitch locally, since we are using a local preset disable the merging with the global preset with ptOptions. For this we'll be using the [material-toggle](https://stackblitz.com/github/cagataycivici/material-toggle-vue?file=README.md) preset from the PrimeVue Tailwind CSS Presets [Gallery](https://tailwind.primevue.org/gallery/).

```html
<template>
    <div class="flex items-center gap-4">
        <label :for="$attrs.inputId">{{ label }}</label>
        <InputSwitch v-bind="$attrs" :pt="preset" :ptOptions="{ mergeSections: false, mergeProps: false }" />
    </div>
</template>
```

The preset is a simple pt configuration based on the InputSwitch PassThrough API with some extensions, notice the use of attrs to add an accent mode which does not exist in the API of the PrimeVue InputSwitch. We're able to utilize arbitrary attributes to add functionality to the components we wrap without waiting for component library maintainer to add it for us. This is a great example of the PrimeVue philosophy, providing 3rd party UI library that is easy to tune and customize as if it were an in-house library.

```js
<script setup>
defineOptions({
    inheritAttrs: false
});

defineProps(['label']);

const preset = {
    root: ({ props }) => ({
        class: [
            'inline-block relative',
            'w-10 h-4',
            {
                'opacity-40 select-none pointer-events-none cursor-default': props.disabled
            }
        ]
    }),
    slider: ({ props, state, attrs }) => ({
        class: [
            // Position
            'absolute top-0 left-0 right-0 bottom-0 before:transform',
            { 'before:translate-x-5': props.modelValue },
            { 'before:-translate-x-1': !props.modelValue },

            // Shape
            'rounded-2xl',

            // Before:
            'before:absolute before:top-1/2',
            'before:-mt-3',
            'before:h-6 before:w-6',
            'before:rounded-full',
            'before:duration-200',
            'before:flex before:justify-center before:items-center',
            'before:[text-shadow:0px_0px_WHITE] before:text-transparent',
            { 'before:ring-4': state.focused },
            {
                "before:bg-surface-500 before:dark:bg-surface-500 before:content-['➖']": !props.modelValue,
                "before:content-['✔️']": props.modelValue,
                'before:bg-violet-500 before:ring-violet-300': !attrs.type & props.modelValue,
                'before:bg-amber-500 before:ring-amber-300': attrs.type === 'accent' && props.modelValue
            },

            // Colors
            'border border-transparent',
            {
                'bg-surface-200 dark:bg-surface-400 before:ring-surface-200 dark:before:ring-surface-400': !props.modelValue,
                'bg-violet-300': !attrs.type & props.modelValue,
                'bg-amber-300': attrs.type === 'accent' && props.modelValue
            },

            // States
            {
                'hover:before:bg-surface-400 hover:dark:before:bg-surface-600': !props.modelValue,
                'hover:before:bg-violet-600': !attrs.type & props.modelValue,
                'hover:before:bg-amber-600': attrs.type === 'accent' && props.modelValue
            },

            // Transition
            'transition-colors duration-200',

            // Misc
            'cursor-pointer'
        ]
    })
};
</script>
```

**Final Result**
Let's wrap it up with an example usage. See the [material-toggle demo](https://stackblitz.com/github/cagataycivici/material-toggle-vue?file=README.md) for a working sample of the preset we used in this component. 

```html
<ToggleSwitch v-model="checked1" inputId="primary" label="Primary" />
<ToggleSwitch v-model="checked2" inputId="accent" label="Accent" type="accent" />
```