Concerning CSS in JS

CSS in JS is concerning and this is a talk concerning CSS in JS

Kent C. Dodds

Utah

1 wife, 3 kids

PayPal, Inc.

@kentcdodds

Please Stand...

if you are able

What this talk is

  • The "why" of CSS in JS
  • Trade-offs
  • Problems looking for solutions

What this talk is not

  • An attempt to get you to use CSS in JS
  • Badmouthing CSS or those who use/like it as it is.

Let's
Get
STARTED!

Our Component:

JavaScript + HTML = JSX

class Toggle extends Component {
  state = { toggledOn: false };

  handleToggleClick = () => {
    this.setState(
      ({ toggledOn }) => ({ toggledOn: !toggledOn }),
      (...args) => {
        this.props.onToggle(this.state.toggledOn);
      },
    );
  };

  render() {
    const { children } = this.props;
    const { toggledOn } = this.state;
    const active = toggledOn ? 'active' : '';
    return (
      <button
        className={`btn btn-primary ${active}`}
        onClick={this.handleToggleClick}
      >
        {children}
      </button>
    );
  }
}

export default Toggle;

Components!

import { PrimaryButton } from './css-buttons';

class Toggle extends Component {
  state = { toggledOn: false };

  handleToggleClick = () => {
    this.setState(
      ({ toggledOn }) => ({ toggledOn: !toggledOn }),
      (...args) => {
        this.props.onToggle(this.state.toggledOn);
      },
    );
  };

  render() {
    const { children } = this.props;
    const { toggledOn } = this.state;
    const active = toggledOn ? 'active' : '';
    return (
      <PrimaryButton
        active={active}
        onClick={this.handleToggleClick}
      >
        {children}
      </PrimaryButton>
    );
  }
}

export default Toggle;

Components!

function AppButton({ className = '', active, ...props }) {
  return (
    <button
      className={`btn ${className} ${active ? 'active' : ''}`}
      {...props}
    />
  );
}

function PrimaryButton({ className = '', ...props }) {
  return <AppButton className={`btn-primary ${className}`} {...props} />;
}

export default AppButton;
export { PrimaryButton };

CSS? Where are the styles?

CSS? 🤔

.btn {
  display: inline-block;
  font-weight: 400;
  line-height: 1.25;
  text-align: center;
  white-space: nowrap;
  vertical-align: middle;
  user-select: none;
  border: 1px solid transparent;
  padding: .5rem 1rem;
  font-size: 1rem;
  border-radius: .25rem;
  transition: all .2s ease-in-out;
}
.btn.btn-primary {
  color: #fff;
  background-color: #0275d8;
  border-color: #0275d8;
}
.btn-primary:hover, .btn-primary.active {
  background-color: #025aa5;
  border-color: #01549b;
}
  1. Conventions?
  2. Who's using these styles?
  3. Can I delete these styles?
  4. Can I change these styles?

CSS?

Pit of Success

a well-designed system makes it easy to do the right things and annoying (but not impossible) to do the wrong things.

Convention

Using conventions is just us compensating for not having a wide enough pit of success.

The P2P Funnel Page

Our CSS...

// import styles for funnel pages control, T2, T3, T4 and T5
.funnel_control,
.funnel_2A,
.funnel_2B,
.funnel_3,
.funnel_4,
.funnel_5_2dot5,
.funnel_5_xb,
.funnel_overseas {
	@import "./funnel";
}

// import styles for funnel page T6 and variants
.funnel_6_2dot5,
.funnel_6_xb,
.funnel_7_xb,
.funnel_8_xb,
.funnel_9_xb,
.funnel_10_xb,
.funnel_11_xb,
.funnel_12_xb,
.funnel_7_gb {
	@import "./funnelT6";
}

// import styles for ppme entry on funnel pages T5, T6, T7 and T8
.funnel_5_2dot5,
.funnel_5_xb,
.funnel_6_2dot5,
.funnel_6_xb,
.funnel_7_xb,
.funnel_8_xb,
.funnel_9_xb,
.funnel_10_xb,
.funnel_11_xb,
.funnel_12_xb,
.funnel_7_gb {
	@import './block-group-entry';
	@import './ppme-entry/ppme-entry';
	@import './ppme-entry/ppme-entry-buttons';
	@import './ppme-entry/ppme-entry-interaction';
}

// ...etc

What impact will my change have elsewhere?

Components

Default

Styled

Unstyled

Focus

Toggled

Components

What is a UI component made of?

HTML

CSS

JS

Component

Remember our CSS?

<style>
  .btn {
    display: inline-block;
    font-weight: 400;
    line-height: 1.25;
    text-align: center;
    white-space: nowrap;
    vertical-align: middle;
    user-select: none;
    border: 1px solid transparent;
    padding: .5rem 1rem;
    font-size: 1rem;
    border-radius: .25rem;
    transition: all .2s ease-in-out;
  }
  .btn.btn-primary {
    color: #fff;
    background-color: #0275d8;
    border-color: #0275d8;
  }
  .btn-primary:hover, .btn-primary.active {
    background-color: #025aa5;
    border-color: #01549b;
  }
</style>
<script>
  function AppButton({ className = '', active, ...props }) {
    return (
      <button
        className={`btn ${className} ${active ? 'active' : ''}`}
        {...props}
      />
    );
  }

  function PrimaryButton({ className = '', ...props }) {
    return <AppButton className={`btn-primary ${className}`} {...props} />;
  }

  export default AppButton;
  export { PrimaryButton };
</script>
import { css } from 'glamor';

const appButtonClassName = css({
  display: 'inline-block',
  fontWeight: '400',
  lineHeight: '1.25',
  textAlign: 'center',
  whiteSpace: 'nowrap',
  verticalAlign: 'middle',
  userSelect: 'none',
  border: '1px solid transparent',
  padding: '.5rem 1rem',
  fontSize: '1rem',
  borderRadius: '.25rem',
  transition: 'all .2s ease-in-out',
});

const highlightStyles = {
  backgroundColor: '#025aa5',
  borderColor: '#01549b',
};

const primaryButtonClassName = css({
  color: '#fff',
  backgroundColor: '#0275d8',
  borderColor: '#0275d8',
  ':hover': highlightStyles,
});

const activeClassName = css(highlightStyles);

function AppButton({ className = '', active, ...props }) {
  return (
    <button
      className={`${appButtonClassName} ${className} ${active ? activeClassName : ''}`}
      {...props}
    />
  );
}

function PrimaryButton({ className = '', ...props }) {
  return (
    <AppButton
      className={`${primaryButtonClassName} ${className}`}
      {...props}
    />
  );
}

export default AppButton;
export { PrimaryButton };

Enter glamorous 💄 

import glamorous from 'glamorous';

const AppButton = glamorous.button({
  display: 'inline-block',
  fontWeight: '400',
  lineHeight: '1.25',
  textAlign: 'center',
  whiteSpace: 'nowrap',
  verticalAlign: 'middle',
  userSelect: 'none',
  border: '1px solid transparent',
  padding: '.5rem 1rem',
  fontSize: '1rem',
  borderRadius: '.25rem',
  transition: 'all .2s ease-in-out',
});

const highlightStyles = {
  backgroundColor: '#025aa5',
  borderColor: '#01549b',
};

const PrimaryButton = glamorous(AppButton)(
  {
    color: '#fff',
    backgroundColor: '#0275d8',
    borderColor: '#0275d8',
    ':hover': highlightStyles,
  },
  ({ active }) => (active ? highlightStyles : null),
);

export default AppButton;
export { PrimaryButton };

jest-glamor-react

📸

+ Our codebase now

function Consumer(props) {
  return (
    <FunnelPage>
      <FunnelLinkGroup title={i18n('flexible.group.send')}>
        <ConsumerFunnelLink {...funnelProps.buyLink(props)} />
        <ConsumerFunnelLink {...funnelProps.sendLink(props)} />
        <ConsumerFunnelLink {...funnelProps.xbLink(props)} />
        <ConsumerFunnelLink {...funnelProps.giftLink(props)} />
        <ConsumerFunnelLink {...funnelProps.massPaymentLink(props)} />
      </FunnelLinkGroup>
      <FunnelLinkGroup {...funnelProps.requestGroup(props)}>
        <ConsumerFunnelLink {...funnelProps.requestLink(props)} />
        <ConsumerFunnelLink {...funnelProps.invoiceLink(props)} />
        <ConsumerFunnelLink {...funnelProps.poolsLink(props)} />
        <ConsumerFunnelLink {...funnelProps.ppmeLink(props)} />
      </FunnelLinkGroup>
    </FunnelPage>
  )
}

// glossing over some details...
function ConsumerFunnelLink(props) {
  return (
    <FunnelLink>
      <Anchor verticalSpacing={14} {...anchorProps}>
        <Icon icon={icon} svg={svg} />
        <Title>{title}</Title>
        <ConditionalDescription>
          {description}
        </ConditionalDescription>
      </Anchor>
      <DesktopBadge filler={!subtext}>{badge}</DesktopBadge>
      <ConsumerSubtext>{subtext}</ConsumerSubtext>
    </FunnelLink>
  )
}

// and here's the Anchor component:
import glamorousLink from '../../component-factories/glamorous-link'
import { mediaQueries } from '../../../../styles'
import {
  spaceChildrenVertically,
  spaceChildrenHorizontally,
} from '../../css-utils'

const { phoneLandscapeMin: desktop, phoneLandscapeMax: mobile } = mediaQueries
const onAttention = '&:hover, &:focus, &:active'

const Anchor = glamorousLink(
  {
    display: 'flex',
    '& > *:last-child': {
      flex: '0 1 auto',
    },
    minHeight: 0, // firefox weirdness
    [onAttention]: {
      textDecoration: 'none',
      '& .funnel-description': {
        color: '#333333',
      },
    },
    [mobile]: {
      padding: '22px 12px 20px 12px',
      flexDirection: 'row',
      '& > *:last-child': {
        flex: 1,
      },
      ...spaceChildrenHorizontally(30),
    },
    [desktop]: {
      marginTop: 12,
      flex: 1,
      flexDirection: 'column',
      justifyContent: 'flex-start',
      [onAttention]: {
        '& .icon, & .icon-svg': {
          color: '#ffffff',
          backgroundColor: '#0070ba',
        },
      },
    },
  },
  ({ verticalSpacing = 10 }) => ({
    [desktop]: {
      ...spaceChildrenVertically(verticalSpacing),
    },
  }),
)

export default Anchor

Now for the concerns...

Why is CSS in JS so concerning?

Familiarity

How would you solve this problem?

Workflow/IDE

  1. Create ESLint plugin for CSS in JS
  2. TypeScript/Flow support

How would you solve this problem?

Performance

Performance

How would you solve this problem?

Resources

Thank you!

Concerning CSS in JS

By Kent C. Dodds

Concerning CSS in JS

I no longer care about: specificity, CSS linters, CSS preprocessors, vendor prefixing, removing unused CSS, finding CSS dependencies and dependents. I now care more about: whether it’s fast enough, whether it’s small enough, whether it’s familiar enough. These are some of my trade-offs. Because I use CSS-in-JS. I’ve made trade-offs because I write HTML-in-JS. Despite these, I still do it, because the cost is minimal enough, and the benefit is great enough. Let’s tell stories, talk use-cases, explore trade-offs, and inspire more innovation to make the CSS-in-JS trade-offs less trade-offy.

  • 5,540