Styling in Rescript with Emotion
Introduction
This is part of an ongoing series about the Rescript programming language. I’ve been taking notes as I build a toy “Recipe Book” app in this delightful language.
Rescript is everything Javascript should be and no more. It works beautifully with best practices in the React and Redux ecosystem.
This article completes the first miniseries by adding styling to the app we’ve been developing all along.
Patreon
This series takes a lot of time to write and maintain. I started it while I have some free time between jobs, but I’ll probably need some extra motivation to keep working on it once I’m busy with work again.
It’s become popular enough that I’ve already fielded a few change requests that have taken additional time. This has slowed me down on new content, but it makes the existing content a lot better.
For some context, I’ve written about 40,000 words of rescript content so far, which would fill a short book, and I’m still writing.
I’m not one for begging for money on the Internet, but if you’re interested in providing some extra motivation, I’ve set up a Patreon account. I thank you for your support.
As a bonus for patrons, articles will be published there at least two weeks before they make it to my public blog.
Other articles in series
With over a dozen articles and counting, I’ve created a table of contents listing all the articles in this series in reading order.
Let’s begin
If you haven’t been following along with these articles, you can git checkout
the
stateful-components
branch of the accompanying github
repository.
Commits that roughly map to the sections of this article are made on the emotion-styling branch. You may find the diffs in the JS files interesting, as it shows what Rescript is compiling to underneath it all.
A note on design
I’m not a designer. I can convert a designer’s vision to actual product, but I can’t make a designer’s vision myself. luckily, current trends in design seem to be to just make everything flat and blocky, and even I can do that!
So for this article, I’ll just be adding a few styles to our recipes app to try it out. I’m kind of targetting a “recipe on an index card” look, but please don’t judge my results.
Dependencies
From my research so far, the standard for rescript styling appears to be the bs-css library. It provides bindings to the emotion CSS library, a popular library for both framework-agnostic and React styling.
The first step, of course, is to install the library. We haven’t done a lot of
dependencies in this series, so you may not recall one of the weird things
about rescript: You need to depend two dependency arrays. You need the normal
javascript dependencies in package.json
, and another for rescript
dependencies in bs-config.json.
Let’s install the library in both places:
npm install --save bs-css-emotion
will put the library in package.json
.
Next, manually add both bs-css
and bs-css-emotion
to the
bs-dependencies
in bsconfig.json
.
- First, we’ll need to install the library
- One of the weird things about rescript is that you need to maintain two dependency arrays
- First
yarn add bs-css-emotion
- This will update the dependencies in
package.json
- This will update the dependencies in
- Then add
bs-css
andbs-css-emotion
to thebs-dependencies
in yourbsconfig.json
- Finally, run
bsb -make-world
to actually build those dependencies
- We already did a bit of lightweight hand-coded styling for
NavButton
andNavBar
Finally, run bsb -make-world
to actually build those dependencies
Port NavButton to emotion styles
We already did a bit of lightweight hand-coded styling for NavButton
and
NavBar
in an earlier article. Let’s port that over to emotion components
first. I’ll add a few tweaks to the style to go for that recipe card
aesthetic.
First add a new module at the top of the NavBar.res
file:
module Styles = {
open CssJs
let navButton = selected =>
style(.[
backgroundColor(
if selected {
"656565"->hex
} else {
"efefef"->hex
},
),
padding(1.0->ex),
cursor(#pointer),
])
}
There’s a lot to grok here! We are creating a new module named Styles
inside
the NavBar
module. Inside that scope, we open CssJs
, which brings a bunch
of style-related functions into the namespace. Opening the module makes the
style calls easier to read than if we were putting CssJs.backgroundColor
and
CssJs.padding
and so on everywhere. Note, however, that the open
is local
to the Styles
module so we don’t have to worry about polluting the outer
NavBar
namespace.
Then we define a new function named navButton
, which accepts a single boolean
argument. It makes a single call to the style()
function which accepts
strongly typed values and returns a string representing an emotion classname.
The strongly typed values are specified by other functions from the CssJs
module.
Note that .
at the beginning of the call to style(.)
, just inside the
parenthesis. This tells Rescript that the call is “uncurried”. If you aren’t
interested in the details, it’s basically a performance optimization. If you
are, see the
docs
The style()
function accepts a list of css arguments, each of which is
created by a function from the CssJs
module. Each of those the functions are
named after camelcase versions of the css prop names, much like React styles.
So background-color
becomes backgroundColor
and border-bottom
becomes
borderBottom
.
Even the arguments to those functions are strongly typed. backgroundColor
can accept a variety of types. In this case, I wanted to use hex colour naming,
so I used the hex()
function. However, the pipe symbol allows me to send the
string into the function, which I think makes it more readable. It tells you
the important bit (the colour) before the type.
Recall that we want to have a different colour depending whether the current
tab is selected. This was easy to do by leveraging the fact that the if
statement is an expression. if
“returns” the value from the chosen branch.
This is the recommended way to do ternary-style conditionals (although the ?
ternary operator does exist in Rescript for the cases where you think it’s more
legible).
Similar to the color
types passed into backgroundColor
, the padding
function accepts a variety of length types. In this case, I wanted to specify
1ex
so I piped the 1.0
into the ex
function. You will never accidentally
typo a value of 1exx
in a stylesheet ever again. I mean, if you’ve ever done
that.
We can now simplify our NavButton
component, as we have separated the
displaying of the component from the logic of styling it:
module NavButton = {
@react.component
let make = (~name, ~selected, ~linkTo) => {
<div
className={Styles.navButton(selected === name)}
onClick={_ => RescriptReactRouter.push(linkTo)}>
{React.string(name)}
</div>
}
}
Reload your app to make sure the styles haven’t changed.
The important bit is the className=
part. Styles.navButton
returns the
result of a style
call, (It will look something like css-mcuw8t
in the
inspector) which is a string that can be used as a classname.
Styling the NavBar
We can add a second function to the Styles
module to style the navigation bar itself:
let navBar = style(.[
display(#flex),
justifyContent(#center),
borderBottom(1->px, #solid, "656565"->hex),
])
The #flex
passed to display
is a polymorphic
variant.
Unlike normal variants such as option
, polymorphic variants don’t require an
explicit type definition. In this case, they are used to guarantee you only
provide valid display
values. Other valid values include block
and
inlineBlock
, for example.
The #center
passed to justifyContent
is similar, as is the #solid
passed
to borderBottom. The borderBottom
function accepts multiple arguments passed
in order. As an aside, I didn’t have to look anything up to figure out what the
right order was. I just let the lightning-fast linter highlight my errors until
I had it right. (I feel I haven’t gushed enough about how fast the Rescript
tooling is in the last couple articles, so it’s time to pick it up again!)
Actually applying this style can be done by replacing the <div style =...>
for the NavBar
s make function with <div className=Styles.navBar>
. This may
actually be a surprising syntax; unlike normal Javascript JSX, you do not need
to put {}
around the property value.
Global styles
We can also use emotion to define some global styles that apply to all
components. Open Index.res
and remove the %raw css import line. (You can
also remove the css file itself).
- Add a
CssJs.global
call to set some global styles:
{
let maxwidth = 40.0
open CssJs
global(.
"body",
[
fontFamily(#custom("Garamond")),
maxWidth(maxwidth->#rem),
border(1->px, #solid, lightgrey),
margin(2.0->#rem),
],
)
global(.
"h1, h2, h3",
[
color("656565"->hex),
borderBottom(1->px, #dashed, lightgrey),
maxWidth((maxwidth /. 2.0)->#rem),
],
)
}
global
is an uncurried (remember the .
) function pretty much the same as
style
except the results are applied–you guessed it–globally. It accepts
any css
selector as the first argument, and the styles to be applied as the
second.
The {...}
surrounding the whole thing isn’t a module. It just creates a local
scope so that the open
doesn’t pollute the Index.res
namespace with a bunch
of functions.
One other interesting note from this example is the division by two. The /.
operator is used for dividing floats. There is no operating overloading in
Rescript, so there are two different division operators: /
for integer
division and /.
for floats.
Sharing and merging styles
Looking at the add and view recipe pages, it’s clear that the values inside the “card” need a margin. I think the add recipe form also needs a little padding, which gives us a chance to showcase merging styles. Basically, cards used for data entry have a different style from those used for viewing.
-
Create a new
CardStyles.res
file:open CssJs let card = style(.[margin(2.0->#rem)]) let formCard = merge(.[card, style(.[padding(1.0->#rem)])])
The main new concept here is the merge
call on formCard
. It “safely merges
styles by name”, to quote the docs. In practice, it’s just a wrapper around
emotion’s cx method.
These styles can be added to the top-level div of each of our components.
ViewRecipe
andAllTags
can have their outer<div>
changed to<div className=CardStyles.card>
AddRecipeForm
can get<div className=CardStyles.formCard>
Final tweak for AddRecipe
It doesn’t introduce any new ideas, but I wanted to improve the textfields on AddRecipeForm
:
module Styles = {
open CssJs
let longEntry = style(.[
width(100.0->#percent),
maxWidth(20.0->#rem),
borderRadius(0.5->#rem),
padding(0.5->#rem),
])
}
Conclusion
I’m going to stop here as there doesn’t seem to be anything left to learn or teach about bs-css. I invite you to make additional styling changes to make the Recipe Book app look real special. Here are some suggestions:
- The buttons on add tag and add recipe look out of place
- The nav buttons could look a bit more tab like
- The links to individual recipes in All Tags need a
cursor: pointer
style - Might want to play with centering text
- Every good index card needs a drop shadow and rounded corners ;-)
At this point we’ve built a small app entirely in Rescript. There are now
%raw
tags anywhere. We’ve covered JSX syntax, React functional-style
components, redux-style reducers, and even styling.
The app is fully and soundly type checked. I haven’t written any tests yet (hopefully in a future article), but I feel Rescript has given me a lot of confidence that it works.
I’m going to take a break from this single page app for a bit. My next article will be on using Rescript to write an express server! Later, though, I’ll port this app to be offline-enabled and eventually hook it up to the express server as a progressive web app.