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-cssandbs-css-emotionto thebs-dependenciesin yourbsconfig.json - Finally, run
bsb -make-worldto actually build those dependencies
- We already did a bit of lightweight hand-coded styling for
NavButtonandNavBar
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 NavBars 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.globalcall 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.resfile: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.
ViewRecipeandAllTagscan have their outer<div>changed to<div className=CardStyles.card>AddRecipeFormcan 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: pointerstyle - 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.