DEV Community

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

Posted on • Edited on

1 1

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. :)

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay