DEV Community

Cover image for WIP: Styled components with Tailwind utility-first like syntax
Joakim Nystrom
Joakim Nystrom

Posted on • Edited on

WIP: Styled components with Tailwind utility-first like syntax

I really like using utility first libraries such as Tailwind, since it allows me to move quickly, uses the idea of composition over inheritance and what's most important: I don't need to worry about being consistent, since it's taken care of by just exposing a few variants of each variable. :)

However, when I was looking over tutorials on how to use TailWind in conjunction with styled-components, I noticed to my horror:

It requires you to set up post-css (with all that extra re-render time when developing and config tweaking)

What I want to achieve

When writing styled-components, I'd like a function that let me parse Tailwind like syntax e.g:

parseUtilsString('bg-blue fc-blue-lighten p-large mt-xl') 
Enter fullscreen mode Exit fullscreen mode

which would translate into

background-color: {defined theme blue}; 
font-color: {defined theme blue, but two increments lighter}; 
padding: {defined theme large units};
margin-top:  {defined theme extra large units};
margin-bottom:  {defined theme extra large units};
Enter fullscreen mode Exit fullscreen mode

I would also like to have the option to add extra CSS in the styled component and use the themes variables.

Introducing: tiny-util-first-like-tailwind-sort-of-setup

(I'll probably come up with a better name when this setup matures)

The setup

This is pretty straight-forward: You define your theme variables and import either just the themeParser or/and the theme to your component and use it there.
I know you can use a themeProvider in styled-components but writing

font-size: ${props => props.theme.fs.large}
Enter fullscreen mode Exit fullscreen mode

is longer and more cumbersome, than just

font-size: ${theme.fs.large}
Enter fullscreen mode Exit fullscreen mode

(Yeah, I'm lazy or cheap with my characters)

Usage

So how do we make this bird fly? you ask. We'll a snippet sais more than 1000 chars, so here goes:

import React from 'react'
import styled from 'styled-components';
import {themeParse} from '../Styles/theme'

const HeaderStyle = styled.header`
    ${themeParse('p-l ta-c')}
    font-weight: bold;
`;

const TitleStyle = styled.div`
    ${themeParse('bg-primary-darkest fs-xl ff-primary fc-white')}
    span{
        ${themeParse('fs-s ff-secondary d-b')}
        transform-origin: bottom left;
        transform: rotate(-10deg) translateY(4em);
    }
`;


export default function Header() {
    return (
        <HeaderStyle>
            <TitleStyle>
                <span>Welcom to</span>
                tiny-util-first-like-tailwind-sort-of-setup
                </TitleStyle>
        </HeaderStyle>
    )
}
Enter fullscreen mode Exit fullscreen mode

which renders into something like this

render-that-bird

How to use it

  1. Copy this pretty snippet below and save it as a file in your project.
  2. Modify and/or add the properties of themeStyles (Perhaps you prefer full names instead of the bootstrap like shorts for all utilities. After all text-center is more descriptive that ta-c).
  3. Add polished to your node_modules (Or comment out the import and write your own shades of color)
  4. Import it to the component and hack away.
import { lighten, darken } from 'polished';

const units = {
  xs: 5,
  s: 10,
  m: 15,
  l: 30,
  xl: 50,
};

const fonts = {
    primary: 'Open Sans',
    secondary: 'Cursive',
};

const fontSizes = {
  xs: '.85rem',
  s: '1rem',
  m: '1.2rem',
  l: '1.5rem',
  xl: '2rem',
};

const colors = {
  primary: _setColorMap('#80C565'),
  secondary: _setColorMap('#002B55'),
  white: _setColorMap('#ffffff'),
};

const theme = {
  unit: units,
  color: colors,
  fontSize: fontSizes,
  font: fonts,
};
// Exported for use of independent values
export default theme;


const displays = {
  b: 'block',
  i: 'inline',
  ib: 'inline-block',
  f: 'flex',
  if: 'inline-flext',
  g: 'grid',
};

const textAligns = {
  c: 'center',
  l: 'left',
  r: 'right',
  j: 'justify',
};

const themeStyles = {
  fc: _renderVariationStyles('color', colors),
  ff: _renderStyleSeries('font-family', fonts, false),
  fs: _renderStyleSeries('font-size', fontSizes, false),

  bg: _renderVariationStyles('background-color', colors, false),
  br: _renderStyleSeries('border-radius', units),

  p: _renderStyleSeries('padding', units),
  py: _renderStyleSeries(['padding-top', 'padding-bottom'], units),
  px: _renderStyleSeries(['padding-left', 'padding-right'], units),
  m: _renderStyleSeries('margin', units),
  my: _renderStyleSeries(['margin-top', 'margin-bottom'], units),
  mx: _renderStyleSeries(['margin-left', 'margin-right'], units),

  d: _renderStyleSeries('display', displays, false),
  ta: _renderStyleSeries('text-align', textAligns, false),
};

/**
 * Parser function for tailwind like syntax
 *
 * @param {String} atomicString A set of tailwind parameters as a string
 */
function themeParse(atomicString) {

  var output = atomicString.split(' ').map((classString) => {
    const [first, second, third] = classString.split('-');

    // Handle "flat" colors
    if (themeStyles[first][second].hasOwnProperty('base') && !third) {
      return themeStyles[first][second]['base'];
    }
    return third
      ? themeStyles[first][second][third]
      : themeStyles[first][second];
  });
  return output;
}

// Exported for use in components
export { themeParse };

/**
 * Renders the styles for a property
 *
 * @param {Array} styles
 * @param {Array} units
 * @param {Boolean} isPixleValue
 */
function _renderStyleSeries(styles, units, isPixleValue = true) {
  // Let us use either a string value or  an array
  if (!Array.isArray(styles)) styles = [styles];

  let styleSerie = {};
  let suffix = isPixleValue ? 'px' : '';
  for (const unit in units) {
    styleSerie[unit] = ``;
    styles.forEach((style) => {
      styleSerie[unit] += `${style}: ${units[unit]}${suffix};`;
    });
  }

  return styleSerie;
}

/**
 * Renders deep nested values as e.g. 'colors'
 *
 * @param {Array} styles
 * @param {Array} units
 */
function _renderVariationStyles(styles, units) {
  // Let us use either a string value or  an array
  if (!Array.isArray(styles)) styles = [styles];

  let styleSerie = {};
  for (const unit in units) {
    styleSerie[unit] = {};
    for (const subUnit in units[unit]) {
      if (subUnit === 'toString') continue;
      styleSerie[unit][subUnit] = ``;
      styles.forEach((style) => {
        styleSerie[unit][subUnit] += `${style}: ${units[unit][subUnit]};`;
      });
    }
  }
  return styleSerie;
}

/**
 * Render a color in different variations; light, lighter, lightest and dark, darker, darkest
 * Either just pass a mainColor or a set of preferred values
 *
 * @param {String} mainColor a color hex value for the standard color
 * @param {String} dark
 * @param {String} darker
 * @param {String} darkest
 * @param {String} light
 * @param {String} lighter
 * @param {String} lightest
 */
function _setColorMap(
  mainColor,
  dark,
  darker,
  darkest,
  light,
  lighter,
  lightest
) {
  if (!mainColor) throw Error('Main color must be provided');
  return {
    toString: () => mainColor,
    base: mainColor,
    dark: dark || darken(0.1, mainColor),
    darker: darker || darken(0.2, mainColor),
    darkest: darkest || darken(0.4, mainColor),
    light: light || lighten(0.1, mainColor),
    lighter: lighter || lighten(0.2, mainColor),
    lightest: lightest || lighten(0.4, mainColor),
  };
}

Enter fullscreen mode Exit fullscreen mode

Ending notes

So, this is something I came up with, but I haven't given it much thought about performance and scaling.
If you have suggestions or opinions (did I just reinvent the wheel or do I did manage to break a working wheel?), - don't be a stranger! Add a comment. :)

Top comments (0)