Styling React Components
with glamorous ๐ย
Kent C. Dodds
Utah
1 wife, 3 kids
PayPal, Inc.
@kentcdodds

Please Stand...
if you are able

What this talk is
- Brief intro to CSS-in-JS
- Story of why glamorous is a thing
- Elicit feedback from you all

What this talk is not
- An attempt to get you to use glamorous

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;
}- Conventions?
- Who's using these styles?
- Can I delete these styles?
- Can I change these styles?
CSS?
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';
}
// ...etcWhat impact will my change have elsewhere?
Components


Default
Styled
Unstyled

Focus
Toggled



Components
What is a UI component made of?
HTML
CSS
JS
Component
Enter CSS-in-JS

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 };
Solving CSS problems
- Global Namespace โ
- Dependenciesย โ
- Dead Code Eliminationย โ
- Minificationย โ
- Sharing Constantsย โ
- Non-deterministicย Resolution โ
- Isolationย โ
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>
  )
}
// funnelProps are pretty neat-o, here's the buy.js one
import { canSendMoney } from './utils'
const staticProps = {
  id: 'nemo_buyLink',
  icon: 'register',
  descriptionKey: 'flexible.buy.description',
  badgeHTMLKey: 'flexible.buy.protectionBadge',
}
export default buy
function buy(props: FunnelProps): {} {
  const { country, paymentTypes, redirectToClassicInfo: { shouldRedirectToClassic, classicSendUrl } } = props
  const shouldRender = canSendMoney(props) && paymentTypes.includes('PURCHASE')
  if (!shouldRender) {
    return { shouldRender }
  }
  const titleKey = country === 'DE' ? 'iWantTo.payDE' : 'flexible.buy.title'
  const dynamicProps = shouldRedirectToClassic ? ({
    titleKey,
    href: classicSendUrl,
    name: '',
    'data-pagename': '',
    'data-pagename2': '',
  }) : ({
    titleKey,
    to: '/myaccount/transfer/buy',
    name: 'goodsStart',
    'data-pagename': 'main:walletweb:transfer:buy:start',
    'data-pagename2': 'main:walletweb:transfer:buy:start:::',
  })
  return { ...staticProps, ...dynamicProps }
}
// and tests are pretty cool:
import { propsTester } from './helpers/utils'
import buyLink from '../buy'
const defaults = {
  input: {
    businessAccountInfo: { isSecondaryUser: false, privileges: [] },
    paymentTypes: ['PURCHASE'],
    redirectToClassicInfo: {
      shouldRedirectToClassic: false,
    },
  },
  result: {
    id: 'nemo_buyLink',
    icon: 'register',
    descriptionKey: 'flexible.buy.description',
    badgeHTMLKey: 'flexible.buy.protectionBadge',
  },
}
propsTester({
  fn: buyLink,
  fnTests: [
    {
      input: { ...defaults.input },
      result: {
        ...defaults.result,
        titleKey: 'flexible.buy.title',
        to: '/myaccount/transfer/buy',
        name: 'goodsStart',
        'data-pagename': 'main:walletweb:transfer:buy:start',
        'data-pagename2': 'main:walletweb:transfer:buy:start:::',
      },
    },
    {
      input: {
        ...defaults.input,
        businessAccountInfo: {
          isSecondaryUser: true,
          privileges: ['send_money'],
        },
      },
      result: {
        ...defaults.result,
        titleKey: 'flexible.buy.title',
        to: '/myaccount/transfer/buy',
        name: 'goodsStart',
        'data-pagename': 'main:walletweb:transfer:buy:start',
        'data-pagename2': 'main:walletweb:transfer:buy:start:::',
      },
    },
  ]
})
// glossing over some details...
function ConsumerFunnelLink() {
  return (
    <FunnelLink shouldRender={shouldRender}>
      <Anchor verticalSpacing={verticalSpacing} {...anchorProps}>
        <Icon icon={icon} svg={svg} />
        <Title>{title}</Title>
        <ConditionalDescription shouldRender={renderDescription}>
          {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 it's a funnel


And code reuse is ๐
const dataPagenameOverrides = {
  'data-pagename': 'main:p2p:send::funnel',
  'data-pagename2': 'main:p2p:send::funnel:node::',
}
function SendMoney(props) {
  return (
    <NewUserFunnelPage>
      <BackLink />
      <glamorous.Div display="flex" justifyContent="space-between">
        <div>
          <PageTitle>{i18n('newUser.sendMoney.title')}</PageTitle>
          <PageSubtitle />
        </div>
        <SendSVG />
      </glamorous.Div>
      <LinkContainer>
        <LinkCard
          {...funnelProps.buyLink(props)}
          descriptionHTMLKey="newUser.sendMoney.goodsAndServices.description"
          {...dataPagenameOverrides}
        />
        <LinkCard {...funnelProps.sendLink(props)} {...dataPagenameOverrides} />
        <LinkCard {...funnelProps.xbLink(props)} {...dataPagenameOverrides} />
        <LinkCard {...funnelProps.giftLink(props)} {...dataPagenameOverrides} />
        <Protection>
          <div
            dangerouslySetInnerHTML={{
              __html: i18n('newUser.sendMoney.purchaseProtection'),
            }}
          />
        </Protection>
      </LinkContainer>
    </NewUserFunnelPage>
  )
}
Resources
- CSS in JS - Vjeux - "The original"
- glamor - Sunil Pai - css in your javascript
- glamorous - PayPalย - React component Styling Solved ๐ย
- jest-glamor-react - Kent C. Dodds - Jest utilities for Glamor and React
- Code Sandbox - the examples from these slides in an interactive code editor in your browser.

Thank you!

Styling React Components
By Kent C. Dodds
Styling React Components
The CSS-in-JS movement is strong in the community and we're still figuring things out. But one thing is clear: keeping your UI logic, markup, and styles together to make a single component is a great way to build applications.
- 6,960













 
   
   
  