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:
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.
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.
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:
{
"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": ""
}
}
}
}
{
"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"
}
}
}
}
{
"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.
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.
<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.
<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:
<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.
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:
<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:
<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 :
<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:
<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 :
<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.
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 ownclasses
&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:
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