UI Frameworks are a great way to quickly build a sleek application, letting you focus on solving the core business needs rather than getting bogged down with how many pixels wide something should be. I still love writing custom CSS, but for client work it's often not worth going down the rabbit hole of creating an entire UI library.
Tailwind hits that sweetspot between having a UI library and handcrafted CSS. It achieves this by providing a tonne of utility classes that follow SRP. I think of it as 'someone a lot better than me at CSS has saved me the time of writing the classes I wanted to write', unlike when I've used UI frameworks in the past and it has felt like someone else's code and preferences.
Let's see how we can integrate this awesome tool with the SAFE Stack.
In this demo we will be using the full tailwind packages rather than downloading via a cdn, as that doesn't offer the full range of features
1) Create a new SAFE app
dotnet new SAFE
I am using the SAFE 3.0.0-beta004 template
2) Install dev dependencies
npm i -D tailwindcss postcss autoprefixer
3) Create tailwind.css
Create a folder called css
in the Client
project directory.
Inside that css
folder, create a new file and name it tailwind.css
.
Open the tailwind.css
file and add:
@tailwind base;
@tailwind components;
@tailwind utilities;
4) Generate tailwind and postcss config files
Run this command in the Client
project directory:
npx tailwindcss init -p
That should've generated two files the tailwind.config.js
and postcss.config.js
. You can think of the tailwind.config.js
as the place you will need to change should you want to customise your tailwind setup.
5) Install and add postcss-loader
Install postcss-loader
as a dev dependency.
npm i -D postcss-loader
I had to use version "4.2.0" of postcss-loader in order for the Client to compile, you may need to do the same in your
package.json
In the webpack.config.js
file, within the use
section of the css
rules, add 'postcss-loader'
to the end.
It should look something like:
...
{
test: /\.(sass|scss|css)$/,
use: [
isProduction
? MiniCssExtractPlugin.loader
: 'style-loader',
'css-loader',
{
loader: 'sass-loader',
options: { implementation: require('sass') }
},
'postcss-loader'
],
},
...
6) Using tailwind!
Within Index.fs
in the Client
directory we need to import our tailwind.css
file.
open Fable.Core.JsInterop
importAll "./css/tailwind.css"
Check out the SAFE docs for more information about import statements
Let's try to use some tailwind classes.
let view (model: Model) (dispatch: Msg -> unit) =
Html.button [
prop.className "bg-blue-500 h-10 w-20 rounded text-gray-50"
prop.text "Hi"
]
We should now see a pretty swanky button! 😎
7) Styling different states
Tailwind can generate a tonne of classes, some of which can be used to target particular states of an element. For example the hover
or focus
states. We can add these custom styles by prefixing the state:
hover:bg-blue-300
Let's start applying some of these to our button.
let view (model: Model) (dispatch: Msg -> unit) =
Html.button [
prop.className
"bg-blue-500
h-10 w-20
rounded
text-gray-50
hover:bg-blue-300
hover:text-gray-500
focus:ring
focus:ring-offset-2
focus:ring-blue-300
focus:ring-opacity-50"
prop.text "Hi"
]
We should see a few changes:
- The background is now lighter on hover 💡
- The text is darker on hover 🕶
- There is a blue ring around the button when the element is in focus 💍
8) Extending the tailwind config
Tailwind comes with a lot of behaviour out of the box, but sometimes we have a particular need to style things a certain way. You may, for example, want to change the colour of the button when it is clicked so that the user has some feedback.
Head over to the tailwind.config.js
we created earlier, and within the variants.extend object we need to add
backgroundColor: ['active']
The config file should now look like:
module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: { backgroundColor: ['active'] },
},
plugins: [],
}
We put it within the
extend
object, as opposed to the top level in thevariants
object, as that would override the existing setup and cause states likehover
to no longer be applied
With this we should be able to add an active
state to our button!
let view (model: Model) (dispatch: Msg -> unit) =
Html.button [
prop.className
"bg-blue-500
h-10 w-20
rounded
text-gray-50
hover:bg-blue-300
hover:text-gray-500
focus:ring
focus:ring-offset-2
focus:ring-blue-300
focus:ring-opacity-50
active:bg-blue-700"
prop.text "Hi"
]
9) CSS crazy!
Our tailwind classes are quickly highjacking our template and we've only got one button on the screen! So how can we extract this out and make it into a class we can use everywhere?
Let's copy the classes from our button, and head over to the tailwind.css
file.
In between the components
and the utilities
add a normal CSS class .awesome-btn
.
Before pasting all the tailwind classes we've copied from Index.fs
, prepend them with @apply
:
@tailwind base;
@tailwind components;
.awesome-btn {
@apply
bg-blue-500 h-10
w-20
rounded
text-gray-50
hover:bg-blue-300
hover:text-gray-500
focus:ring
focus:ring-offset-2
focus:ring-blue-300
focus:ring-opacity-50
active:bg-blue-900
}
@tailwind utilities;
The reason the class goes in between components and utlities is that we may need to update the utlities on a per use basis, so one button using this class may need extra padding (p-5) than another (p-10). You can also use the @layer attribute to achieve the same result.
The apply
attribute will take all the utility classes and combine them under awesome-btn
.
We can now update our class in Index.fs
to use awesome-btn
!
10) Components FTW
Before you go off and start using tailwind utilities to build your own UI framework, you may find a much better approach is to instead build components rather than a CSS class.
let awesomeButton =
Html.button [
prop.className
"bg-blue-500
h-10 w-20
rounded
text-gray-50
hover:bg-blue-300
hover:text-gray-500
focus:ring
focus:ring-offset-2
focus:ring-blue-300
focus:ring-opacity-50
active:bg-blue-700"
prop.text "Hi"
]
This is just a simple function, to create an actual React component check out Feliz!
This is because CSS classes will hide away a lot of the intent that can be seen from tailwinds utilities. Abstracting away the declarative classes that tailwind provides will make it harder to reason about why things are styled a certain way.
Another is that by using @apply
to create custom CSS classes you are duplicating the generated CSS, which can be avoided by making components that have styles built in.
11) Customising our tailwind config
This is really where tailwind shines, it's such an extendable tool since all it does is provide utility classes.
So how do you know what can be extended?
In the Client
directory run:
npx tailwindcss init tailwind-full.config.js --full
This will not affect our current setup as tailwind looks for
tailwind.config.js
We should now have a tailwind-full.config.js
file that explicitly has all the tailwind config. This step is like running an npm eject
when using a create-react-app, so it is not generally recommended. We are using it here to figure out how we can customise our tailwind.config.js
.
Let's add some new colours to our configuration from the tailwind-full.config.js
.
I can see a colors
key within the theme
object. If we head to our tailwind.config.js
, within theme.extend
we can add some new colours:
module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
colors: {
"cit-blue": "#102035",
"cit-light-blue": "#40a8b7",
"cit-green": "#8cbf41",
"cit-yellow" : "#fec903",
"cit-red": "#e1053a",
"cit-orange": "#e97305"
}
},
},
variants: {
extend: { backgroundColor: ['active'] },
},
plugins: [],
}
But we can make this a bit nicer by nesting the structure:
module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
colors: {
cit: {
"DEFAULT": "#102035",
"light-blue": "#40a8b7",
"green": "#8cbf41",
"yellow" : "#fec903",
"red": "#e1053a",
"orange": "#e97305"
}
}
},
},
variants: {
extend: { backgroundColor: ['active'] },
},
plugins: [],
}
Now we can do something like:
bg-cit-red
text-cit /* This will take the DEFAULT value which is our dark blue */
12) The Purge 🔪
Finally we're ready to Productionise our code. Tailwind generates a lot of css, some of which may never be used. In order to remove the uneeded classes we need to update the purge
array in our tailwind.config.js
:
purge: [ "./Index.fs"]
I've only started looking into Tailwind this week and it's already becoming a core technology in my workflow. I've only just scratched the surface and I can't wait to share more! ❤