Skip to content
On this page

Configuration

The main concept of Vanilla Components is to provide a good developer expirience while still giving you the freedom to style your components the way you want. Before you read this section and start blowing your mind, lets keep it simple, all you need is a object that contains classes that will be applied to the component mark, that's it!

This brings a nice concept and saves ( at least for me ) all the boring process of copying over themes & components across projects.

Styling & Preset 💅

The whole point of this library is to provide you freedom to style your components the way you want, we provide a default configuration that you can use as a starting point, but you are free to change it and provide your own configuration any time! The configuration is a simple array/json file that you can use to configure the components.

Style your Components

The library comes out of the box configured to be used with Tailwind, you are free to change this configuration and provide your own when installing the plugin. The configuration keys can be found here, you can even include configuration for your components and leverage the configuration system for your components

Here is a small demo overriding the Input component:

ts
import { createApp } from 'vue'
import { Plugin } from '@flavorly/vanilla-components' 
const app = createApp()

app.use(Plugin, {
    Input: { 
        fixedClasses: {
          input: 'm-5 shadow-xl',
        },
        classes: {
          input: 'bg-purple-100 border-pruple-100',
          wrapper: 'relative-custom',
        },
        props: {
          placeholder: 'Please write something here',
        }
  },
})

Please also note that when you override, you will lose the default styling for this component, which means you will have to configure this component on your own. You can find our presets on the official GitHub.

If you want to override only a certain parts or classes of a component while keeping the rest of the configuration, you can use the defineConfiguration function provided by the package, which will merge the configuration with the default one while also still let you provide your own preset if required.

ts
import { createApp } from 'vue'
import { Plugin, defineConfiguration } from '@flavorly/vanilla-components' 
const app = createApp()

app.use(Plugin, defineConfiguration({ 
    Avatar: {
        classes: {
            wrapper: 'bg-red-600',
        },
    },
}))

You can also like we said earlier, provide your own preset and still override its own configuration.

ts
import { createApp } from 'vue'
import { Plugin, defineConfiguration } from '@flavorly/vanilla-components'
import preset from './presets/talwind-funny.json' 

const app = createApp()

// Define some overrides
const overrides = {
    Avatar: {
        classes: {
            wrapper: 'bg-red-600',
        },
    },
};

app.use(Plugin, defineConfiguration(overrides,preset))

Default Style & Configuration

You can check the default and full configuration here : full Configuration Where it also includes the configuration file separated for each component here The default configuration is using TailwindCSS, but you can use any other framework you want, just keep in mind that you will have to provide the correct classes for each component.

Here is a demo of the Input Component Configuration file:

Input
{
    "fixedClasses": {
        "input": "text-sm appearance-none w-full focus:outline-none shadow disabled:opacity-50 disabled:cursor-not-allowed border-0",
        "wrapper": "relative",
        "addonBefore": "absolute inset-y-0 left-0 pl-3 flex items-center cursor-pointer",
        "addonAfter": "absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer",
        "addonBeforeInputClasses": "pl-10",
        "addonAfterInputClasses": "pr-10",
        "addonClasses": "w-5 h-5",
        "roundedFull": "rounded-lg",
        "roundedTop": "rounded-none rounded-t-lg",
        "roundedBottom": "rounded-none rounded-b-lg",
        "roundedLeft": "rounded-none rounded-l-lg",
        "roundedRight": "rounded-none rounded-r-lg",
        "roundedBottomLeft": "rounded-none rounded-bl-lg",
        "roundedBottomRight": "rounded-none rounded-br-lg",
        "roundedTopLeft": "rounded-none rounded-tl-lg",
        "roundedTopRight": "rounded-none rounded-tr-lg",
        "inputBorder": "",
        "disabled": "opacity-60 select-none cursor-not-allowed "
    },
    "classes": {
        "input": "border-0 text-gray-700 dark:text-white placeholder-gray-500/60 bg-white dark:bg-gray-900 ring-1 ring-inset focus:ring-inset focus:ring-2 ring-gray-300 focus:ring-primary-600 dark:ring-white/20 dark:focus:ring-primary-600 focus-visible:ring-primary-600 px-4 py-3 !autofill:bg-white !dark:autofill:bg-gray-900",
        "roundedFull": "",
        "roundedTop": "",
        "roundedBottom": "",
        "roundedLeft": "",
        "roundedRight": "",
        "roundedBottomLeft": "",
        "roundedBottomRight": "",
        "roundedTopLeft": "",
        "roundedTopRight": "",
        "inputBorder": "border-0",
        "wrapper": "",
        "addonBefore": "",
        "addonAfter": "",
        "addonBeforeInputClasses": "",
        "addonAfterInputClasses": "",
        "addonClasses": "text-gray-300 dark:text-gray-600",
        "disabled": ""
    },
    "variants": {
        "error": {
            "classes": {
                "input": "text-red-400 placeholder-red-400 bg-white dark:bg-gray-900 ring-1 ring-inset focus:ring-inset focus:ring-2 ring-red-400 focus:ring-red-500 dark:ring-red-400 dark:focus:ring-red-500 focus-visible:ring-red-500 dark:focus-visible:ring-red-500 px-4 py-3 !autofill:bg-white !dark:autofill:bg-gray-900",
                "inputBorder": "border-0",
                "wrapper": "",
                "addonBefore": "",
                "addonAfter": "",
                "addonBeforeInputClasses": "",
                "addonAfterInputClasses": "",
                "addonClasses": "text-red-300 dark:text-red-300/70"
            }
        },
        "compact": {
            "classes": {
                "input": "text-sm text-gray-700 dark:text-white placeholder-gray-500/60 bg-white dark:bg-gray-900 border-0 ring-1 ring-inset focus:ring-inset focus:ring-2 ring-gray-300 focus:ring-primary-600 dark:ring-white/20 dark:focus:ring-primary-600 focus-visible:ring-primary-600 rounded-lg px-4 py-2",
                "roundedFull": "",
                "roundedTop": "",
                "roundedBottom": "",
                "roundedLeft": "",
                "roundedRight": "",
                "roundedBottomLeft": "",
                "roundedBottomRight": "",
                "roundedTopLeft": "",
                "roundedTopRight": "",
                "inputBorder": "border-0",
                "wrapper": "",
                "addonBefore": "",
                "addonAfter": "",
                "addonBeforeInputClasses": "",
                "addonAfterInputClasses": "",
                "addonClasses": "text-gray-300 dark:text-gray-600",
                "disabled": ""
            }
        }
    }
}
Button
{
    "fixedClasses": {
        "button": "block justify-center inline-flex items-center",
        "container": "flex items-center space-x-1",
        "spinner": "-ml-1 mr-1 h-4 w-4 text-whit",
        "disableOpacity": "opacity-50",
        "enableOpacity": "opacity-100",
        "busyOrInvalidState": "cursor-not-allowed",
        "validState": "cursor-pointer",
        "roundedFull": "rounded-md",
        "roundedCircle": "rounded-full",
        "roundedTop": "rounded-t-md",
        "roundedBottom": "rounded-b-md",
        "roundedLeft": "rounded-l-md",
        "roundedRight": "rounded-r-md",
        "roundedBottomLeft": "rounded-bl-md",
        "roundedBottomRight": "rounded-br-md",
        "roundedTopLeft": "rounded-tl-md",
        "roundedTopRight": "rounded-tr-md"
    },
    "classes": {
        "button": "text-sm font-medium whitespace-nowrap focus:outline-none focus:ring-2 dark:focus:ring-offset-gray-900 shadow focus:ring-offset-2 focus:ring-primary-600 text-gray-700 focus:text-gray-600 dark:text-white dark:hover:text-white dark:focus:text-white bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 dark:focus:border-primary-600",
        "container": " ",
        "spinner": "",
        "disableOpacity": "",
        "enableOpacity": "",
        "busyOrInvalidState": "",
        "validState": "",
        "padding": "px-3 py-2",
        "paddingCircle": "p-2",
        "roundedFull": "",
        "roundedCircle": "",
        "roundedTop": "",
        "roundedBottom": "",
        "roundedLeft": "",
        "roundedRight": "",
        "roundedBottomLeft": "",
        "roundedBottomRight": "",
        "roundedTopLeft": "",
        "roundedTopRight": ""
    },
    "variants": {
        "error": {
            "classes": {
                "button": "text-sm font-medium whitespace-nowrap focus:outline-none focus:ring-2 dark:focus:ring-offset-gray-900 shadow focus:ring-offset-2 focus:ring-red-500 text-white focus:text-gray-200 bg-red-600",
                "container": "",
                "spinner": "",
                "disableOpacity": "",
                "enableOpacity": "",
                "busyOrInvalidState": "",
                "padding": "px-3 py-2",
                "paddingCircle": "p-2"
            }
        },
        "error_muted": {
            "classes": {
                "button": "text-sm font-medium whitespace-nowrap focus:outline-none focus:ring-2 dark:focus:ring-offset-gray-900 shadow focus:ring-offset-2 focus:ring-red-500 dark:focus:ring-red-400 text-red-500 focus:text-red-600 dark:text-red-400 bg-white dark:bg-gray-800 border border-red-400 focus:border-red-400 dark:focus:border-red-400",
                "container": "",
                "spinner": "",
                "disableOpacity": "",
                "enableOpacity": "",
                "busyOrInvalidState": "",
                "padding": "px-3 py-2",
                "paddingCircle": "p-2"
            }
        },
        "primary": {
            "classes": {
                "button": "text-sm font-medium whitespace-nowrap focus:outline-none focus:ring-2 dark:focus:ring-offset-gray-900 shadow focus:ring-offset-2 focus:ring-primary-500 text-white focus:text-gray-200 bg-primary-600",
                "container": "",
                "spinner": "",
                "disableOpacity": "",
                "enableOpacity": "",
                "busyOrInvalidState": "",
                "padding": "px-3 py-2",
                "paddingCircle": "p-2"
            }
        },
        "success": {
            "classes": {
                "button": "text-sm font-medium whitespace-nowrap focus:outline-none focus:ring-2 dark:focus:ring-offset-gray-900 shadow focus:ring-offset-2 focus:ring-green-500 text-white focus:text-gray-200 bg-green-600",
                "container": "",
                "spinner": "",
                "disableOpacity": "",
                "enableOpacity": "",
                "busyOrInvalidState": "",
                "padding": "px-3 py-2",
                "paddingCircle": "p-2"
            }
        },
        "outline": {
            "classes": {
                "button": "text-sm font-medium whitespace-nowrap focus:outline-none focus:ring-2 dark:focus:ring-offset-gray-900 focus:ring-offset-2 focus:ring-primary-600 text-gray-700 focus:text-gray-600 dark:text-white dark:hover:text-white bg-transparent border border-gray-400 dark:border-gray-600 dark:focus:border-primary-600",
                "container": "",
                "spinner": "",
                "disableOpacity": "",
                "enableOpacity": "",
                "busyOrInvalidState": "",
                "padding": "px-3 py-2",
                "paddingCircle": "p-2"
            }
        },
        "transparent": {
            "classes": {
                "button": "text-sm font-medium whitespace-nowrap focus:outline-none focus:ring-2 dark:focus:ring-offset-gray-900 focus:ring-offset-2 focus:ring-primary-600 text-gray-700 focus:text-gray-600 dark:text-white dark:hover:text-white bg-transparent",
                "container": "",
                "spinner": "",
                "disableOpacity": "",
                "enableOpacity": "",
                "busyOrInvalidState": "",
                "padding": "px-3 py-2",
                "paddingCircle": "p-2"
            }
        },
        "pagination": {
            "classes": {
                "button": "text-sm font-medium whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium text-gray-500 dark:text-white hover:text-gray-400 dark:hover:text-gray-100 px-3 py-2 cursor-pointer relative inline-flex items-center bg-white dark:bg-gray-800 border-0 ring-1 ring-inset ring-gray-300 dark:ring-gray-600/60 focus:z-10 focus:outline-none focus:ring-offset-0 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-500 -ml-px",
                "container": "",
                "spinner": "",
                "disableOpacity": "",
                "enableOpacity": "",
                "busyOrInvalidState": "",
                "padding": "",
                "paddingCircle": "p-2"
            }
        },
        "pagination_page": {
            "classes": {
                "button": "text-sm font-medium whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium text-gray-500 dark:text-white px-3 py-2 cursor-pointer relative inline-flex items-center border-0 bg-white dark:bg-gray-800 ring-1 ring-inset ring-gray-300 dark:ring-gray-600/60 focus:z-10 focus:outline-none focus:ring-offset-0 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-500 -ml-px",
                "container": "",
                "spinner": "",
                "disableOpacity": "",
                "enableOpacity": "",
                "busyOrInvalidState": "",
                "padding": "",
                "paddingCircle": "p-2"
            }
        }
    }
}
Select
{
    "fixedClasses": {
        "wrapper": "relative",
        "select": "appearance-none block w-full focus:outline-none shadow disabled:opacity-50 disabled:cursor-not-allowed px-4 py-3",
        "selectIfMultiple": "space-y-2"
    },
    "classes": {
        "wrapper": "",
        "select": "text-sm text-gray-700 dark:text-white placeholder-gray-500/60 bg-white dark:bg-gray-900 border-0 ring-1 ring-inset focus:ring-inset focus:ring-2 ring-gray-300 focus:ring-primary-600 dark:ring-white/20 dark:focus:ring-primary-600 focus-visible:ring-primary-600 rounded-lg",
        "selectIfMultiple": "",
        "optGroup": "px-4 py-3 disabled:opacity-50 disabled:cursor-not-allowed text-sm rounded-lg checked:font-semibold checked:text-primary-900 checked:bg-primary-100 checked:dark:bg-primary-300 checked:dark:text-black select:focus:text-red-100 w-full disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed text-sm font-normal px-3 py-2"
    },
    "variants": {
        "error": {
            "classes": {
                "wrapper": "",
                "select": "text-sm ring-1 ring-inset focus:ring-inset focus:ring-2 text-red-400 placeholder-red-400 bg-white dark:bg-gray-900 border-0 ring-red-400 focus:ring-red-500 dark:ring-red-400 dark:focus:ring-red-500 focus-visible:ring-red-500 dark:focus-visible:ring-red-500 rounded-lg",
                "selectIfMultiple": "",
                "optGroup": "px-4 py-3 disabled:opacity-50 disabled:cursor-not-allowed text-sm rounded-lg checked:font-semibold checked:text-red-900 checked:bg-red-100 checked:dark:bg-red-300 checked:dark:text-black select:focus:text-red-100 w-full disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed text-sm font-normal px-3 py-2"
            }
        }
    }
}

You may also import the default configuration object from the package and use it as a starting point for your own configuration.

ts
import { defaultConfiguration } from '@flavorly/vanilla-components'
// ...
// Define some overrides
const overrides = {
    Avatar: {
        classes: {
            wrapper: 'bg-red-600',
        },
    },
};
app.use(Plugin, defineConfiguration(overrides,defaultConfiguration))

Inline Style Configuration

You are not limited to setting a configuration on a file and loading it, even though we recommend doing it, you are also able to define classes, fixedClasses & variants directly to the component. We will cover more of this in the variants section.

vue
<template>
    <Button
        :variants="{ 
            orange: {
                classes: {
                    foo: 'bar'
                }
            }
        }"
        :classes="{
            foo: 'baz'
        }"
        :fixed-classes="{
            foo: 'fixed-baz'
        }"
        variant="orange"
    />
</template>

Reset Classes Modifier

If you want to reset a particular component classes, you can use the $reset modifier, which will remove all the classes BEFORE the given modifier from the component for that specific key, this is useful to get rid of fixedClasses and classes injected after they are resolved and all together.

vue
<template>
    <Button
        :classes="{
            button: '$reset bg-red-600'
        }"
    />
    
    <!-- Also works for nested properties -->
    <PhoneInput
        :classes="{
            input: {
                wrapper: '$reset bg-red-600'
            },
        }"
    />
</template>

Given the example above, everything before the $reset modifier will be removed, so the final result will be:

html
<button class="bg-red-600"> Hey!</button>

This modifier works for classes and fixedClasses and also for nested properties.

Override Props Values

As mentioned before you may also override the props default values from the components using the variants & other settings, just keep in mind to always use valid types, as this could bring unwanted behaviors.

ts
import { createApp } from 'vue'
import { Plugin } from '@flavorly/vanilla-components'
const app = createApp()

app.use(VanillaComponents, {
    Input: { 
        props: { 
          // Here we are expecting a string
          placeholder: true,
        }
    },
    Dialog: {
        props: {
          // Here we are expecting a boolean, but we getting a string! :(
          closeOnLeaving: 'yes',
        }
    },
})

Variants 🔃

Variants are way to quickly swap your component styles while keeping some sane styles left over. A good example of a variant usage is the error variant, where we only want to change the colors to red but still keep the paddings all the rest of the classes. This is where things get interesting! 😃

Using Variants

One of the main features of Vanilla Components is the usage of variants as explained in the configuration section, when using variants the component will swap the classes automatically while preserving the fixedClasses you may use the variant prop to quickly toggle different variants.

There is one special variant for errors called error, this variant is meant to be temporary and once you change or interact with the component it will fall back to the original variant provided initially ( if any ).

You may use variants as shown below:

vue
<template>
    <Button variant="soft-red"/>
    <Button :variant="true ? 'soft-red' : 'soft-blue'"/>
    <!-- This will use "error" variant, and once you hit, it will fallback to soft-red -->
    <Button variant="soft-red" :errors="'There is something wrong'"/>
</template>

You are not limited to configuring everything when booting the plugin, you may also define your classes, fixedClasses & variants on your component, this is useful for edge cases or specific scenarios that you want to override something specific.

A note on overrides & inline configuration

Please just keep in mind that when you do this, we will completely ignore the global configuration and use the classes, fixedClasses, or variants provided "inline".

Here is a small example:

vue
<template>
    <Button
        :variants="{
            dark: {
                classes: {
                    wrapper: 'pt-20'
                }
            }
        }"
        :classes="{
            wrapper: 'pt-10'
        }"
        :fixed-classes="{
            wrapper: 'pb-20'
        }"
        variant="dark"
    />
</template>

Given the example above we would always use the pb-20 from the fixed classes, and pt-20 from the dark variant we picked up, ignoring the default pt-10 that was the default value for the key "wrapper".

The actual HTML result would be something like the following :

html
<button class="pb-20 pt-20"></button>

Error Variant

All components should include the default variant and error variant, the error variant is meant to be used when you want to show an error message to the user, this variant will be automatically applied when you provide the errors prop to the component. Once you "touch" the v-model of the component the error variant will be removed and the component will fall back to the original variant.

This is really useful when errors are coming from the backend ( Ex: Inertia ) and you want to flash them but instantly remove them as soon as the user tries again.

Here is a small example using hybridly or inertia:

vue
<script setup lang="ts">
const login = useForm({
	method: 'POST',
	url: route('login'),
	fields: {
		email: '',
		password: '',
		remember: false,
	},
})
</script>
<template layout="auth/layout-auth">
    <VanillaInputGroup
        :label="t('app.fields.email')"
        name="email"
    >
        <VanillaInput
            v-model="login.fields.email"
            :errors="login.errors.email" 
            :placeholder="t('app.placeholders.email')"
            type="email"
            required
            autofocus
        />
    </VanillaInputGroup>
</template>

Hands on!

Yes, you are not limited to having a fixed color or style set in your component! But enough talk, let's see some real examples.

And here is the code :

vue
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@flavorly/vanilla-components'

// The current variant
const variant = ref('superCool')

// A List of our own variants
const variants = {
  cool: {
    classes: {
      button: 'rounded-md sm:text-sm text-base font-medium leading-6 whitespace-nowrap focus:outline-none focus:ring-2 dark:focus:ring-offset-gray-900 focus:ring-primary-600 text-gray-700 focus:text-gray-600 dark:text-white dark:hover:text-white bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 dark:focus:border-primary-600',
    },
  },
  superCool: {
    classes: {
      button: 'inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center text-primary-100 border border-primary-500 rounded-lg shadow-sm cursor-pointer hover:text-white bg-gradient-to-br from-pink-500 via-primary-500 to-primary-500',
    },
  },
}

const clickHandler = () => variant.value = variant.value === 'cool' ? 'superCool' : 'cool'
</script>

<template>
  <Button
    label="👉 Click me to change!"
    :variant="variant"
    :variants="variants"
    @click="clickHandler"
  />
</template>

Options

The plugin also supports an object of configurable options, you can pass them as the third argument when installing the plugin. You may import defineOptions, that will help find out what options are available and ensure a type-safe experience. Here is a quick example.

ts
import { Plugin, defineConfiguration, defineOptions } from '@flavorly/vanilla-components'

app.use(Plugin, defineConfiguration({}), defineOptions({
    swapErrorsVariantOnModelValueChanges: true,
}))

swapErrorsVariantOnModelValueChanges

This option will automatically swap the error variant to the original variant once the v-model of the component changes.

Setting it to true, will assume that your error prop is reactive and will change once the error gets changed, it will also remove the error variant once the user changes the input.

Settings it to false will change the behaviour, and the error will persist even if the user changes the input, it will only go back to the original variant once errors prop is cleared or set to a nullish value.

In both cases, the input will go back to the normal variant once the errors prop is set to a nullish value.

Styling Structure 🧬

Components contain three major keys that can be set: fixedClasses, classes, variants & props, Below we will explain what each of them means and what's their behavior, as they represent the core concept:

  • fixedClasses - List of classes that are always persisted, even if the variant changes. This is useful in case you want to change the border color but still for example keep the same paddings & other relevant styles.
  • classes - This contains the base classes for the "default" layout of the component.
  • variants - This is an object with your own variant, each variant contains its own classes & props
  • props - This will provide/override the default props for this component, so they are injected as you need.

With this little system, you can imagine how flexible this can be, the limit is your imagination! We will get deeper into this Below. You can see the demo of the Input config here

Shared Props on Vanilla Components

To check the full list of the available properties for all components please check the props section

Tailwind Defaults

Note on the default Primary Color for Tailwind, the color used by default is called primary, which relies also on @tailwindcss/forms so please extend your tailwind.config.js to have the primary color as you wish.

Here is a demo tailwind configuration:

js
const colors = require('tailwindcss/colors')

/** @type {import('tailwindcss').Config} */
module.exports = {
  mode: 'jit',
  darkMode: 'class',
  content: [
    // Add our default tailwind preset to the content list
    './node_modules/@flavorly/vanilla-components/dist/presets/tailwind/all.json', 
  ],
  theme: {
    extend: {
      colors: {
        // Set your primary color
        primary: colors.indigo, 
      },
    },
  },
  plugins: [
    require('@tailwindcss/typography'),
    require('@tailwindcss/aspect-ratio'),

    // Forms plugin is required if you are using the tailwind preset
    require('@tailwindcss/forms'), 
  ],
}

That's it! With this in mind, you are free to start being creative and create your own thing. If you want to style your components please have a look under Advanced Configuration

Released under the MIT License.