Styling Remix using Vanilla CSS
Tags: react, remix, javascript, webdev
Published: 2021-12-01 08:09:16

## Table of Contents
  * <a href="#tldr" target="_self">TL;DR: Source and Demo</a>
  * <a href="#intro" target="_self">Introduction</a>
  * <a href="#clean-up" target="_self"> Cleaning up</a>
  * <a href="#global-style" target="_self"> Global styling</a>
  * <a href="#route-style" target="_self">Styling a Route</a>
  * <a href="#route-child-style" target="_self">Styling a child Route</a>
  * <a href="#route-sibling-style" target="_self">Sibling Route Styling</a>
  * <a href="#reusable-component-style" target="_self">Styling Reusable Component</a>
  * <a href="#basic-html-element" target="_self">Styling Basic HTML element</a>
  * <a href="#dark-mode" target="_self"> Dark Mode</a>
  * <a href="#responsive" target="_self">Responsive Layout</a>
  * <a href="#conclusion" target="_self">Conclusion</a>


> ### TL;DR: Source and Demo<a name="tldr"></a>
> Link to the [source code](https://github.com/codegino/playground/tree/part2/styling)
>
> Here's a live [demo](https://playground-irds16t7i-codegino.vercel.app/words)


### Introduction <a name="intro"></a>

In my last [blog post](https://dev.to/codegino/remix-and-supabase-integration-cci), I discussed how to perform a CRUD operation in a Remix app.
In this blog post, I will discuss how to style a Remix app using only plain CSS.

<br />

#### Why to use plain CSS?

According to the [official documentation](https://remix.run/docs/en/v1/guides/styling#css-ecosystem-and-performance)

> In general, stylesheets added to the page with `<link>` tend to provide the best user experience:
>
> - The URL is cacheable in browsers and CDNs
> - The URL can be shared across pages in the app
> - The stylesheet can be loaded in parallel with the JavaScript bundles
> - Remix can prefetch CSS assets when the user is about to visit a page with `<Link rel="prefetch">`.
> - Changes to components don't break the cache for the styles
> - Changes to the styles don't break the cache for the JavaScript

<br />

### Cleaning up styles from boilerplate <a name="clean-up"></a>

```bash
# Remove all files under `app/styles`
rm -r app/styles/*
```

<br />

### Global Styling <a name="global-style"></a>

<br />

#### Recreate the `app/styles/global.css` file

Some CSS defaults I copied from the official documentation with some adjustments.

```css
/* app/styles/global.css */
:root {
  --color-foreground: hsl(0, 0%, 0%);
  --color-background: hsl(0, 0%, 100%);
  --color-primary: hsl(230, 100%, 50%);
  --color-primary-light: hsl(230, 100%, 60%);
  --color-primary-dark: hsl(230, 100%, 40%);
  --color-gray-dark: hsl(0, 0%, 60%);
  --color-gray-light: hsl(0, 0%, 90%);
  --font-body: -apple-system, "Segoe UI", Helvetica Neue, Helvetica, Roboto,
    Arial, sans-serif, system-ui, "Apple Color Emoji", "Segoe UI Emoji";
}

html {
  box-sizing: border-box;
}

*,
*::before,
*::after {
  box-sizing: inherit;
}

html,
body {
  padding: 0;
  margin: 0;
  background-color: var(--color-background);
  color: var(--color-foreground);
}

body {
  font-family: var(--font-body);
  line-height: 1.5;
}

a {
  color: var(--color-primary-light);
  text-decoration: none;
}

a:hover {
  color: var(--color-primary-dark);
  text-decoration: underline;
}
```

<br />

We'll modify `app/root.tsx` to import the global stylesheet; then, export the style in a `links` function.

```tsx
// app/root.tsx
import type { LinksFunction } from "remix";
import styles from "~/styles/global.css";

export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: styles,
    },
  ];
};

// ...
```

After Adding the links function, nothing changes in the app. If we check the elements inspector, there is no link tag inside the `<head>` tag.
<Img src="https://i.imgur.com/lES2wax.png" width={1185} height={407} />

<br />

#### Add `Links` to `head`

Since Remix will only do what we tell it to do, we need to inform it to render the links we exported in our pages using the `Links` compnent.

```tsx
// app/root.tsx
import {/*other imports*/, Links} from "remix";

// The function containing the HTML declaration
function Document({/*props*/}) {
  return (
    <html lang="en">
      <head>
        {/* This is the magic piece */}
        <Links />
        {/*...*/}
      </head>
      {/*body content*/}
    </html>
  );
}
```

The [changes](https://github.com/codegino/playground/commit/b8846e32cfd3a97ac4a2ae3b75f307d6cd86998e) above will yield to:
<Img src="https://i.imgur.com/TXb6jSn.png" width={1185} height={436} />


<br />

> TRIVIA: We can technically put `<Links/ >` anywhere inside the `html` tag; however, here's a [reason why you should not do it](https://stackoverflow.com/a/1642259).

<br />

### Styling a Route <a name="route-style"></a>

> For now, we're not aiming to get the best design award; we'll just apply some styles for the sake of making it look different.

#### Create the `/words` route styles

```css
/* app/styles/words.css */
.words__page {
  margin: 1rem;
  padding: 1rem;
  border: 1px solid black;
}

.words__content {
  padding: 0.5rem;
  border: 1px solid var(--color-gray-dark);
  display: grid;
  grid-template-columns: 1fr 1fr;
  border-radius: 3px;
}
```

#### Add class name definition inside the component

```tsx
// app/routes/words.tsx
// ...

export default function Index() {
  // ...

  return (
    <div className="words__page">
      {/*...*/}
      <div className="words-content">
        {/*...*/}
        <Outlet />
      </div>
    </div>
  );
}
```

#### export the links function

```tsx
// app/routes/words.tsx
// ...
import type { LinksFunction } from "remix";
import styles from "~/styles/words.css";

export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: styles,
    },
  ];
};

// ...
```

The [changes](https://github.com/codegino/playground/commit/5f078e6ec14ece3d1243ae1f753a7f30ef40e495) above will yield to:
<Img height={220} width={480} src="https://media2.giphy.com/media/wyhurq5jLbhOYge40I/giphy.gif"/>

> Apologies if GIFs might not be that clear.

<br />

> NOTE: every child route of `/words` will inherit the styles exported in `app/routes/words.tsx`

<br />

### Styling a child Route <a name="route-child-style"></a>

Since both our `/add` and `/edit/$id` routes use a form, let's create a shared css file.

> I'm too lazy to think of a great design. Let's simply add a border.

```css
// app/styles/words/shared.css
form {
  border: 1px solid var(--color-gray-dark);
  padding: 0.5rem 1rem;
  border-radius: 3px;
}
```

<br />

Now, let's expose the style by exporting it in our `links` function.
```tsx
// app/routes/words/add.tsx
// ...
import sharedStyles from "~/styles/words/shared.css";

export const links: LinksFunction = () => [
  {
    rel: "stylesheet",
    href: sharedStyles,
  },
];

// ...
```

Here's the result:
<Img height={242} width={480} src="https://media2.giphy.com/media/xLnnCCXJXEyvzjqGrV/giphy.gif"/>

After clicking the `Add new word` button, the `word form` will be styled as expected.
However, since the `Add new word` is inside a form as well, that form will also have a border.

<br />

A straightforward way to fix this is to improve the [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) of the word form by adding a class name.

```css
/* app/styles/words/shared.css */
form.word-form {
  border: 1px solid var(--color-gray-dark);
  padding: 0.5rem 1rem;
  border-radius: 3px;
}
```

```tsx
// fileName=app/components/WordForm.tsx
// ...
export function WordForm({ word }: { word?: Word }) {
  // ...
  return (
    <Form method="post" className="word-form">
      {/*...*/}
    </Form>
  );
}
```

After adding the class name, our `word form` styles won't leak to other form elements.
<Img src="https://i.imgur.com/DAWKmVg.png" width={1185} height={614} />

<br />

### Sibling Routes Styling <a name="route-sibling-style"></a>

_I'm adding a temporary background color to demonstrate how sibling routes styling behave._

```css
/* app/styles/words/shared.css */
form.word-form {
  border: 1px solid var(--color-gray-dark);
  padding: 0.5rem 1rem;
  border-radius: 3px;

  /* Temporary style */
  background-color: red;
}
```

<Img height={242} width={480} src="https://media2.giphy.com/media/HPrsNSdsodJmux5ola/giphy.gif"/>


After navigating from `/add` to `/edit/$id`, the `word form` styling is removed in the `head`; The reason why styles were not applied to the `/edit/$id` form.

The reason is simple; sibling routes do not share styles with other sibling routes. 

<br />

To fix the issue, we need to apply similar change to `app/routes/words/edit.$id.tsx`

```tsx
/* fileName=app/routes/words/edit.$id */
// ...
import sharedStyles from "~/styles/words/shared.css";

export const links: LinksFunction = () => [
  {
    rel: "stylesheet",
    href: sharedStyles,
  },
];

// ...
```

Now it's working as expected. There's no magic or whatever, just pure JavaScript doing its thing.
<Img height={242} width={480} src="https://media2.giphy.com/media/JRaCZXkbchvIdNgnwV/giphy.gif"/>

<br />

### Styling a reusable component <a name="reusable-component-style"></a>

> Revert changes in `app/styles/words/shared.css`, `app/routes/words/add.tsx`, and , `app/routes/words/edit.$id.tsx`

We were able to share styles between sibling routes. However, this is not the right approach for this app.

The change we made was specific to a form and not a page, so we'll make the changes in the word form component.

<br />

#### Create a new folder `word-form` under `app/components`

##### Create the word form styles

```css
/* app/components/word-form/styles */
.word-form label {
  font-size: 0.8em;
}

.word-form {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1rem;
  border: 1px solid var(--color-gray-dark);
  border-radius: 0.5rem;
}
```

#### Rename `WordForm.tsx` to `index.tsx` and move it to `app/components/word-form`

#### Export the declaration of our `links` magic function

```tsx
// app/components/word-form/index.tsx
import type { LinksFunction } from "remix";
import styles from "./styles.css";

export const links: LinksFunction = () => [
  {
    rel: "stylesheet",
    href: styles,
  },
];

export function WordForm({ word }: { word?: Word }) {
  // ...
  return (
    <Form method="post" className="word-form">
      {/*...*/}
    </Form>
  );

```

Uh oh! The styles are not applied to the `word form` component. Now, we only have 2 `link` tag in the `head`
<Img src="https://i.imgur.com/oAlYoQ7.png" width={1185} height={430} />

It seems the magical behavior of `export const links` only applied to `routes`

To fix the issue, we need to propagate the styles to a route


```tsx
// app/routes/words/add.tsx
import type { LinksFunction } from "remix";
import { WordForm, links as formStyles } from "~/components/word-form";

export const links: LinksFunction = () => [...formStyles()];

// ...
```

> Apply the same thing in `app/routes/words/edit.$id.tsx`

The [changes](https://github.com/codegino/playground/commit/5a2170f3b59c1762e991d9e16144bd5ba914786c) above will yield to:
<Img src="https://i.imgur.com/nIVB8vo.png" width={1185} height={646} />

<br />

### Styling custom basic HTML Element <a name="basic-html-element"></a>

> **Q**: So how do we style a custom basic HTML element using CSS?
> 
> **A**: The same as for `word form`. Although, we need to propagate more until the reference reaches a `route`.

#### Custom Button

```tsx
// app/components/basic/button/index.tsx
import React from "react";
import styles from "./styles.css";
import type { LinksFunction } from "remix";

export const links: LinksFunction = () => [
  {
    rel: "stylesheet",
    href: styles,
  },
];

type CustomButtonProps = {
  color?: "primary" | "success" | "danger" | "warning" | "info";
};

export const Button = React.forwardRef<
  HTMLButtonElement,
  JSX.IntrinsicElements["button"] & CustomButtonProps
>(({ color, ...props }, ref) => (
  <button {...props} ref={ref} custom-button={color ? color : ""} />
));
```

```css
/* app/components/basic/button/styles.css */
button[custom-button] {
  display: block;
  height: 2rem;
  color: #000000;
}

button[custom-button="primary"] {
  background-color: var(--color-primary);
  color: #ffffff;
}
```

#### Other custom elements
There is nothing fancy for other elements, so you can view the [sources here](https://github.com/codegino/playground/commit/bc0efd132cb32da6d6b4b3ad0941243c2886b7a9) for `input`, `select`, and `textarea`.

<br />

#### Here is how we can propagate the components used in the word form to the `routes`

```tsx
// app/components/word-form/index.tsx
// ...
import { Input, links as inputLinks } from "../basic/input/Input";
import { Select, links as selectLinks } from "../basic/select/Select";
import { TextArea, links as textAreaLinks } from "../basic/textarea/Textarea";
import { Button, links as buttonLinks } from "../basic/button/Button";
import styles from "./styles.css";

export const links: LinksFunction = () => [
  ...inputLinks(),
  ...selectLinks(),
  ...textAreaLinks(),
  ...buttonLinks(),
  {
    rel: "stylesheet",
    href: styles,
  },
];

// ...
```

#### New Word Form Component

After replacing the default html elements with our custom ones, our word form will look like this:

```tsx
// app/components/word-form/index.tsx
import type { LinksFunction } from "remix";
import { useTransition, Form } from "remix";
import { Word, WordType } from "~/models/word";
import { Input, links as inputLinks } from "../basic/input/Input";
import { Select, links as selectLinks } from "../basic/select/Select";
import { TextArea, links as textAreaLinks } from "../basic/textarea/Textarea";
import { Button, links as buttonLinks } from "../basic/button/Button";
import styles from "./styles.css";

export const links: LinksFunction = () => [
  ...inputLinks(),
  ...selectLinks(),
  ...textAreaLinks(),
  ...buttonLinks(),
  {
    rel: "stylesheet",
    href: styles,
  },
];

export function WordForm({ word }: { word?: Word }) {
  let transition = useTransition();

  return (
    <Form method="post" className="word-form">
      <div>Form State: {transition.state}</div>
      <div>
        <label htmlFor="name">Word</label>
        <Input
          id="name"
          name="name"
          type="text"
          placeholder="Word"
          required
          defaultValue={word?.name ?? ""}
          disabled={Boolean(word?.name)}
        />
      </div>
      <div>
        <label htmlFor="type">Type</label>
        <Select
          id="type"
          name="type"
          defaultValue={word?.type ?? WordType.NOUN}
        >
          <option value={WordType.NOUN}>Noun</option>
          <option value={WordType.VERB}>Verb</option>
          <option value={WordType.ADJECTIVE}>Adjective</option>
        </Select>
      </div>
      <div>
        {word?.sentences.map((sentence, i) => (
          <SentenceField index={i + 1} sentence={sentence} key={i} />
        )) ?? <SentenceField index={1} sentence={""} />}
      </div>
      <div>
        {word?.definitions.map((definition, i) => (
          <DefinitionField index={i + 1} definition={definition} key={i} />
        )) ?? <DefinitionField index={1} definition={""} />}
      </div>
      <Button type="submit" color="primary">
        Submit
      </Button>
    </Form>
  );
}

const SentenceField = ({ index, sentence }: any) => (
  <div>
    <label htmlFor={`sentence.${index}`}>Sentence #{index}</label>
    <TextArea
      id={`sentence.${index}`}
      name="sentence"
      defaultValue={sentence}
      placeholder={`Sentence #${index}`}
      minLength={10}
    />
  </div>
);

const DefinitionField = ({ index, definition }: any) => (
  <div>
    <label htmlFor={`definition.${index}`}>Definition #{index}</label>
    <TextArea
      id={`definition.${index}`}
      name="definition"
      defaultValue={definition}
      placeholder={`Definition #${index}`}
      minLength={10}
    />
  </div>
);
```

The [changes](https://github.com/codegino/playground/commit/159a0d4f272b7865ea58701dd0e02574a081f3d6) above will yield to:
<Img src="https://i.imgur.com/Eb8SSnE.png" width={1185} height={646} />

<br />

### Dark Mode <a name="dark-mode"></a>

There are many ways to implement dark mode.
This example will use `prefers-color-scheme` to update the CSS variables when a media query is satisfied.

<br />

#### Create `app/styles/dark.css` file with the following content:

```css
/* app/styles/dark */
:root {
  --color-foreground: hsl(0, 0%, 100%);
  --color-background: hsl(0, 0%, 7%);
  --color-primary-dark: hsl(230, 100%, 65%);
  --color-primary-light: hsl(230, 100%, 80%);
}
```

#### Update `app/root.tsx` file with the following content:

```tsx
// app/root.tsx
// ...
import styles from "~/styles/global.css";
import darkStyles from "~/styles/dark.css";

export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: styles,
    },
    {
      rel: "stylesheet",
      href: darkStyles,
      media: "(prefers-color-scheme: dark)",
    },
  ];
};
```


The [changes](https://github.com/codegino/playground/commit/55e1aa57d668538988fbe47b14912cee65163d5d) above will yield to:
<Img height={276} width={480} src="https://media2.giphy.com/media/2UiODQD1eXy4Hv1N3n/giphy.gif"/>

<br />
<br />

### Responsive Layout<a name="responsive"></a>

We'll use the same approach in the dark mode example to change the layout depending on the result of a media query.

<br />

#### Create `sm-words.css`

```css
/* app/styles/words-sm */
.words__content {
  grid-template-columns: 1fr;
  border: none;
}

.words__content form {
  margin-top: 1rem;
}
```

#### Use in `app/routes/words.tsx`

```tsx
// app/routes/words.tsx
import type { LinksFunction } from "remix";
import styles from "~/styles/words.css";
import smStyles from "~/styles/words-sm.css";

export const links: LinksFunction = () => [
  {
    rel: "stylesheet",
    href: styles,
  },
  {
    rel: "stylesheet",
    href: smStyles,
    media: "(max-width: 600px)",
  },
];

// ...
```

The [changes](https://github.com/codegino/playground/commit/5ab6fc7303d43e3bc4e3a72c1184136feea5edfa) above will yield to:
<Img height={313} width={480} src="https://media2.giphy.com/media/jm5PbsI2yLMHrREtMP/giphy.gif"/>

<br />

## Conclusion <a name="conclusion"></a>
Styling Remix using Vanilla CSS is a fun exercise for me.
I was able to review concepts that I almost forgot due to the convenience of using third-party libraries.
I have fine-grain control over the styling of my routes and components, and I know that I only ship styles that will be used.
Less code to transmit to the Network means faster load times.
However, I'm not a fan of working in vanilla CSS, which is why in this project's next iteration, I will be integrating either `Tailwind` or `Emotion`.

## What's next?

- Styling using [`Tailwind`](https://dev.to/codegino/styling-remix-using-tailwind-and-postcss-54b9) or `Emotion`
- Error handling
- Authentication
- Pagination
- Deeply nested routes
- SEO