Writing components is a good way to structure our client-side apps. However we should be careful when writing and using them.
In my recent projects I always kept a Styleguide. All components built by me and the team I work with live there as a components catalog. That’s great, because it provides us an isolated component development.
There we create components in a generic way, outside from its real scenario. We can ensure that it’s built to be reusable and not tailor made for a specific feature.
An example of that kind of tool is React Storybook. Although, you can do it in your own way, anything that fits your stack, but please, maintain a Styleguide, it will help everyone.
Reusability and Independence
Components should be reusable, but they should be independent too. Every behavior must be driven by the component API, it shouldn’t be manipulated by outside.
Lets take a button component as an example. It may have lots of variations, it can be a small button, an iconed button, a disabled button, etc… To achieve this we need classes like:
.btn {}, .btn-primary {}, .btn-small {}, .btn-loading {}, ...
Those classes must be managed by a small component API. The developer who will use this component mustn’t touch those classes, the developer must only tell the component which variation is wanted.
<!-- Using the UI-Button component -->
<ui-button type="primary" size="small" loading="true">
Ship it!
</ui-button>
<!-- Generates the HTML below -->
<button class="btn btn-primary btn-small btn-loading">
Ship it!
</button>
What do you do when you need a new variation of a component? I’ve been seeing developers creating variations like:
<!-- A circle version of UI-Button adding "btn-circle" class -->
<ui-button type="primary" size="small" loading="true" class="btn-circle">
Edit
</ui-button>
As you can see, a new variation isn’t created. That circular button is styled and managed outside component’s API. That isn’t healthy for the app, because in future another developer may need to reuse that, what this developer will do is to copy & paste it in another place.
Doing things this way will certainly lead us to have variations of components not specified in our Styleguide.
Remember, that’s a single small example, imagine it happening with Modals, Cards and Tables with different variations through the app. It’ll be a Component-Frankenstein.
I think the correct way is to tell the button’s component API what I want. In that case I created a shape
optional property:
<!-- A circle variation of UI-Button using the shape property -->
<ui-button type="primary" size="small" loading="true" shape="circle">
Edit
</ui-button>
I advocate to that because in my opinion every behavior and style must be defined inside the component. CSS classes shouldn’t be exposed. Whatever methodology you’re using, CSS is about composition. I may use 3 different classes to supply a variation and I don’t want those classes being copied and pasted all over the app.
When everything is inside the component I can refactor CSS classes without changing component’s API or touching places that are using it.
For using a good component, you’ll need to just drop it and pass its options. Only with all styles being internal we can ensure that the Styleguide is being properly maintained.
Here is an example of how I’d build this <ui-button>
. If you’re not familiar with, I’m using Vue.js to write the component below:
<!-- UI-Button Component definition -->
<template>
<button
:type="buttonType"
:class="classes"
:disabled="isDisabled"
:autofocus="autofocus"
@click="action">
<i :class="iconClass" v-if="icon"></i>
<slot></slot>
</button>
</template>
<script>
export default {
props: {
buttonType: {
type: String,
default: 'button',
validator (value) {
return ['button', 'submit', 'reset'].includes(value)
}
},
type: {
type: String,
default: 'default',
validator (value) {
return ['default', 'primary', 'link'].includes(value)
}
},
shape: {
type: String,
validator (value) {
return ['full', 'circle'].includes(value)
}
},
size: {
type: String,
default: 'medium',
validator (value) {
return ['small', 'medium', 'large'].includes(value)
}
},
icon: String,
disabled: Boolean,
loading: Boolean,
autofocus: Boolean,
action: {
default: () => {},
type: Function
}
},
computed: {
classes () {
return {
btn: true,
[btn-${this.type}
]: this.type,
[btn-${this.shape}
]: this.shape,
[btn-${this.size}
]: this.size,
['btn-disabled']: this.isDisabled,
['btn-loading']: this.loading
}
},
isDisabled () {
return this.disabled || this.loading
},
iconClass () {
return icon icon-${this.icon}
}
}
}
</script>
Here is the usage case (all values are optional):
<!-- Using the UI-Button component -->
<ui-button
button-type="submit"
type="primary"
icon="basket"
size="large"
shape="full"
:loading="addingToCart"
:disabled="product.stock > 0"
:autofocus="false"
:action="addToCart">
Add to cart
</ui-button>
<!-- Generates the HTML below -->
<button
type="submit"
disabled="disabled"
class="btn btn-primary btn-full btn-large btn-disabled btn-loading">
<i class="icon icon-basket"></i>
Add to cart
</button>
The logic behind it applies to any kind of component library or web-components, you can choose your favorite one since you’ve a way to validate and set default properties for them.
Composability
Components must be composable. If you think it isn’t valid to add a shape
property in the API of your <ui-button>
component. Or if it isn’t only about styling but the circular button will have different behavior and responsibilities. One of the options is to create a circular button component (<circular-button>
).
That component can composes <ui-button>
and adds its own variations. For example, a floating property that will make the button be a floating fixed button in the right-bottom corner of screen.
<!-- <circular-button> Component definition -->
<template>
<ui-button
type="primary"
size="small"
:loading="isLoading"
:disabled="isDisabled"
:action="clickHandler"
:class="{ 'btn-floating': this.floating }"
class="btn-circular">
<slot></slot>
</ui-button>
</template>
<script>
import UiButton from './UiButton.vue'
export default {
components: { UiButton },
props: {
floating: Boolean,
//...
},
//...
}
</script>
<style>
.btn-circular {
border-radius: 50%;
/* ... */
}
.btn-floating {
position: fixed;
right: 10px;
bottom: 10px;
}
</style>
More options could be added, as a floating-direction
, and others, but I think you got it.
CSS Composability
Sometimes it may not make sense to create a component for something. I’m talking about a scenario where we’ve defined classes to style typography. Imagine we’ve reusable classes that style titles in our app like:
.title {}, .title-1 {}, .title-2 {}, ...
If doesn’t make sense to create a title
component, whether because we want it to be free to set any typography style to any text element, such as h1, h2, span, p, li, for example, or any other reason. (I know that we can create a component with a dynamic tag, but that isn’t the point here).
We want it to be a reusable style in our app. That seems to be a good case for CSS Modules. We can solve it composing the .title, .title-1
classes inside another component.
<!-- <modal> Component definition -->
<template>
<div class="$style.modal">
<strong class="$style.modalTitle">{{title}}</strong>
<!-- ... -->
</div>
</template>
<!-- Using CSS Modules -->
<style module>
.modal-title {
composes: title, title-1;
/* ... Other styles ... */
}
</style>
The .modal-title
will composes those typography styles we want and we are free to use any HTML tag together with the modal-title class. When using the Modal component, the generated HTML will use all three classes:
<strong class="modal-title title title-1">Hello World!</strong>
Composing classes will make our template code cleaner, but we must only do it when isn’t better to compose a component.
Those are my thoughts when writing components, I hope it helps someone.
Please share it if you liked. Or leave a comment 🙂
We want to work with you. Check out our "What We Do" section!