/5 min read

Duck-typing in style with TypeScript and Tachyons

It can be challenging to set up a new project. You need to decide not only on the technologies to use, but also on what patterns will be utilised in the project. Sometimes, certain patterns work well with sets of technology and might even, dare I say, create a synergy. We discovered that, for us, using Tachyons in combination with TypeScript and React was an excellent solution. Below we cover how we did it.

Tachyons?

Tachyons is a simple CSS strategy, where each class rule is responsible for only one part of the style. For example if we want our element to have a bold text we add a b class to it.

<p>Hello, <span class="b">I am bold</span></p>

The Tachyons library already specifies the most common rules, which lets you start your project with a minimum amount of CSS. Having a class responsible for only a subset of CSS rules was/is considered by some a bad pattern. It causes repetition if the same set of styles need to be applied to different elements. If we were discussing the use of Tachyons in days before React (or similar frameworks), I too would look at Tachyons suspiciously.

How does React address the issue?

React allows us to create elements of our page as encapsulated components. Therefore, we can start hiding away some of the details of the styling from the user of the component.

Let’s look at an example:

    const Page = ({ isDark, children }) => {
    	return (
    		<div className={isDark ? "bg-dark-blue washed-blue" : "bg-washed-blue dark-blue"}>
    			{children}
    		</div>
    	)
    }
    ...
    Example uses:

    <Page isDark={true}> Hello! it is dark in here!</Page>
    <Page> Hello! it is dark in here! Oh wait, it is not.</Page>

Obviously, you can achieve the same result with BEM style CSS, Bootstrap or alternative styling options. The main benefit comes from the fact that we did not have to write any new CSS. All the classes used in the example are from the default Tachyons CSS file. It is true that some of the styling libraries like Bootstrap provide us with a set of classes to take advantage of, but they most often focus on the whole component while Tachyons provides us only the building blocks, thus giving us more granular control.

Customizing your Tachyons

It is all and good to have all these classes predefined by someone else, but in your use case you might not agree with the colours, paddings and margins defined by the creators of Tachyons. That is simple to address. The source code of Tachyons is written with SASS; and that means we can swap out the variables of spacing, fonts, and colours to our own hues and get our own custom CSS bundle.

At Papercup, we use PostCSS to combine our custom classes, overridden variables and Tachyons base library into a single stylesheet. The whole process is part of our build script to ensure that, if there was a change made to the base styles, the developer does not have to remember to recompile the CSS manually. We created another abstraction layer over the class names of the colours. Instead of using the name of the colour, we defined the class name by its definition in our style guide:

.bg-primary {
  background-color: var(--near-black);
}
.bg-brand {
  background-color: var(--orange);
}
.bg-inverted {
  background-color: var(--near-white);
}

The pattern mirrors our style guide, therefore changes there quickly propagate through our websites. This allows us to update the colours of the site from a single place.

Sometimes you have to get creative when styling 3rd party libraries

Adding some syntax sugar on top

The className prop can start getting confusing as we increase the number of classes we use to define it, especially if they do not follow any grouping. To improve the readability of the code, we use a library called classNames, which allows us to take arrays, objects, strings and convert them into a class name. By using the library, we can split out classes, group them based on what aspect they are styling, and even enable or disable some classes based on the props of the component.

    className={classNames(
        "flex db",  // Layout
        "w-100 pl4 h4", // Sizing
        "lh-f5 fw5 f5 pff", // Font
        !selected && "b--solid bw1 br2",  // Add some border if the element is selected
    )}

The final piece, TypeScript

Now we have our React components with our Tachyons classes that match our design. The final piece, that takes the experience to the next level is TypeScript. To take advantage of it we need to create an abstraction layer on top of a div element. Below is a simplified version of our Container component

    import classNames from "classNames";

    type Width = "w1" | "w2" | "w3" | "m-w1" | "m-w2" | "m-w3"

    interface ContainerProps = {
     dimension?: { width: Width | Width[] }
    }

    const Container: React.FunctionalComponent<ContainerProps> = ({ dimension }) => {

    	return (
    		<div className={classNames(dimension && Object.values(dimension))}>
    			{children}
    		</div>
    	)
    }

What is happening here?

First, we define a string literal with a set of values the width can have, in this case we are using our class names. We do this on purpose, to reduce any unnecessary abstraction layers, such as mapping from one value to a class name. The string literal is used as part of the declaration of the interface of the component. Notice the property can be an single value or an array of values. We can specify an array of values if we want to use properties like max-width in combination with width or if we have device resolution-based properties.

Now when you use the Container, as soon as you type the dimensions props and width, you will start seeing appropriate values for the width of the component.

Real examples

export interface InterfaceContainer extends ClassNameOverrideInterface {
  asElement?: React.ElementType;
  border?: BorderProperties;
  dimensions?: DimensionProperties;
  flex?: FlexProperties;
  layout?: LayoutProperties;
  background?: ColoursBackground;
}

export const Container: WithAsElementPropWithRef<InterfaceContainer> = React.forwardRef(
  (
    {
      asElement,
      border,
      dimensions,
      flex,
      layout,
      background,
      children,
      classNameOverride,
      ...rest
    }: any,
    ref: any,
  ) => {
    const Component = asElement || 'div';

    return (
      <Component
        ref={ref}
        className={classNames(
          border && Object.values(border),
          dimensions && Object.values(dimensions),
          flex && Object.values(flex),
          layout && Object.values(layout),
          background,
          classNameOverride,
        )}
        {...rest}
      >
        {children}
      </Component>
    );
  },
) as WithAsElementPropWithRef<InterfaceContainer>;

export const FlexColumn: WithAsElementProp<InterfaceContainer> = ({
  children,
  flex,
  layout,
  ...props
}: any) => {
  return (
    <Container
      layout={Object.assign({ d: 'flex' }, layout)}
      flex={Object.assign({ fd: 'flex-column' }, flex)}
      {...props}
    >
      {children}
    </Container>
  );
};

export const FlexRow: WithAsElementProp<InterfaceContainer> = ({
  children,
  flex,
  layout,
  ...props
}: any) => {
  return (
    <Container
      layout={Object.assign({ d: 'flex' }, layout)}
      flex={Object.assign({ fd: 'flex-row' }, flex)}
      {...props}
    >
      {children}
    </Container>
  );
};

Once we defined all our properties, we can duck type way.

<FlexRow
  id="layout"
  background={'bg-primary'}
  dimensions={{ h: 'vh-100', w: 'vw-100' }}
  flex={{
    fai: 'items-center',
    fjc: 'justify-center',
  }}
>
  {children}
</FlexRow>

Is it worth it?

The initial set up of the base components and the style definitions was not the most fun experience. However, as the project grew, we started appreciating the solution more.

Subscribe to the blog

Receive all the latest posts right into your inbox

Doniyor Ulmasov

Doniyor Ulmasov

Sr. Software Engineer at Papercup. Human.

Read More