Graduate Program KB

Module 3 - Modern Component Architecture

Types of ways to write CSS

Vanilla CSS

  • Pros:

    • No tooling, no runtime performance cost
    • Modern CSS features like CSS Custom Properties make tooling features redundant
  • Cons:

    • Global and unscoped
    • Remember to add vendor prefixes for many CSS properties, checking for browser support
    • Can't easily share data between CSS and JS
    • CSS is built for documents, not apps

Sass / Less

  • A preprocessor that compiles to vanilla CSS. All valid CSS is valid Sass, and Sass compiles to CSS at build-time

  • Added features like nesting, variables and iteration

  • Less is similar to Sass, the main noticeable difference is syntax for variables

    @var: 10
    
    $var: 10
    
  • Pros:

    • Includes powerful tools like for-loops, mixins and nesting
    • High developer satisfaction compared to vanilla CSS
  • Cons:

    • Requires a build step
    • Global by nature because it compiles to CSS
    • Can't react to real-time events because everything happens at build time
    • Requires nativerr dependencies that become out of date
    • Conflicting naming conventions with CSS
    • Fading community, less support, less resources, more trouble

Aphrodite

  • Styles written in JS and used by React components

    import { StyleSheet, css } from 'aphrodite';
    
    function App() {
        return (
            <div className={css(styles.wrapper)}>
                Hello World!
            </div>
        );
    }
    
    const styles = StyleSheet.create({
        wrapper: {
                backgroundColor: 'gray',
                minHeight: 200,
                '@media (min-width: 1025px)': {
                fontSize: '1.25rem',
            },
        },
    });
    
  • Pros:

    • Scoping and specificity issues disappear
    • Can use JS within CSS
    • Encourages good habits
    • Adds vendor prefixes automatically
    • Similar API to React Native improves compatibility
  • Cons:

    • Requires a build step
    • Easy to forget css() function call
    • Cost in terms of bundle size and runtime execution, though relatively small
    • Styles must be writtein in JS, properties are camelCased and values must be quoted
    • Weird to write global styles
    • Not active anymore

CSS Modules

  • Tool that allows you to write vanilla CSS and import it into a JS file

  • Not a built-in feature

    import styles from "./App.css";
    
  • Pros:

    • Solves scoping and specificity
    • Familiar with users, just writing CSS
    • Offers a composes feature to extend existing CSS classes
  • Cons:

    • Lack of modern convenience features like autoprefixing
    • Hard to share data between CSS and JS

Single-file components

  • Create scoped CSS in the same file as your component definition with Vue and Svelte
    <template>
        <p>Hello World</p>
    </template>
    
    <style scoped>
        p {
            background-color: gray;
            min-height: 200px;
        }
    
        @media (min-width: 1025px) {
            p {
                font-size: 1.25rem;
            }
        }
    </style>
    

styled-components

  • A solution where every style is a React component, can write CSS and JS together

  • Pros:

    • Solves scoping and specificity elegantly
    • Familiarity writing CSS with some QoL features from Sass/Less
    • Offers good solutions for animations and global styles
    • High developer satisfaction
    • Best-in-class performance
    • Large community, lots of development and engagement, plenty of resources
  • Cons:

    • Requires a build system
    • Primarily a React tool
    • Obfuscates the underlying markup tags

Emotion

  • Very similar API to styled-components

  • Pros:

    • Can be used without React (with different syntax, though)
    • Slightly smaller bundle size compared to styled-components
  • Cons:

    • Not as popular as styled-components, smaller community
    • emotion/styled takes a different approach towards dynamic props which is less flexible

Tailwind

  • A utility-first CSS framework

  • "Utilities" mean short CSS classes that map to specific styles

  • Pros:

    • Solves scoping and specificity
    • Encourages good habits for following a design system
    • Includes built-in design tokens
    • Faster to write over time
    • Not specific towards React
    • Growing popularity
  • Cons:

    • Steep learning curve compared to other tools
    • Not all CSS can be turned to utilities, will need another system for writing traditional CSS
    • Lots of bulk added to markup, reducing readability

Introduction to styled-components

  • This library uses a JS feature called tagged template literals

    • Here's a basic example, styled.button is a function passing in a template string as an argument
    • The styled object has methods for every HTML tag typically used within the <body>
    const Button = styled.button`
        font-size: 32px;
    `;
    
  • A Sass-like preprocessor called stylis is used to automatically add vendor prefixes for maximum browser compatibility

  • The & character can be used to reference a soon-to-be-created class

    • Component names don't have to be globally unique, a unique hash is generated when a styled-component creates a class
    • The & can be thought of as a placeholder, it will be replaced by a class name once generated
    • Here's an example of the component and CSS produced
    const Button = styled.button`
        display: flex;
    
        &:hover {
            color: red;
        }
    `;
    
    .abc123 {
    /* Vendor prefixes for legacy browsers: */
        display: -webkit-box;
        display: -webkit-flex;
        display: -ms-flexbox;
        display: flex;
    }
    
    /* Plucks out the `hover` pseudo-class: */
    .abc123:hover {
        color: red;
    }
    
  • CSS prop is an alternative writing method

    const Title = ({ id, children }) => {
        return (
            <h1
                id={id}
                css={`
                    font-size: 2rem;
                    font-weight: bold;
                `}
            >{children}</h1>
        )
    };
    

Installation and Setup

  • Installation of styled-components

    npm install styled-components
    
  • Babel plugin

    • At runtime, the styled component you wrote will transform that component into a CSS class with a randomly generated name

    • A Babel plugin was released to solve this issue by picking semantically-meaningful class names in development

    • Class names are structured in the format: Filename_componentName-hash

      • If the HTML is in <header class="ShoeIndex__Header-sc-123">, you can look for the const Header inside ShoeIndex.js
    • If you use Create React App, the build configuration is hidden unless you eject

    • However, it supports Babel macros so the next couple steps provide a solution

    npm install --save-dev babel-plugin-styled-components
    
    • Importing the macro gives the benefits of the Babel plugin without need to eject or mess with the build configuration
    • In the React application, change imports to match the following:
    // From this:
    import styled from 'styled-components';
    
    // ...to this:
    import styled from 'styled-components/macro';
    
  • Editor integrations

    • Tagged template literals are the core of styled-components
    • But by default, the styles are treated like any other colourless string
    • The vscode-styled-components plugin can add syntax highlighting and even proper auto-complete
  • Server-side Rendering

    • styled-components run in the browser and has server-side rendering support, which means the initial HTML/CSS is generated beforehand
    • Even if JS was disabled on the browser, most of the site could still work and all the styles would still appear

Global Styles

  • styled-components has an API for creating global styles
    • When GlobalStyles is rendered, all its CSS will inject into the <head> of the document
    • Doesn't really matter where this component is rendered
    import { createGlobalStyle } from 'styled-components';
    
    const GlobalStyles = createGlobalStyle`
        *, *::before, *::after {
            box-sizing: border-box;
        }
    
        html {
            font-size: 1.125rem;
        }
    
        body {
            background-color: hsl(0deg 0% 95%);
        }
    `;
    
    export default GlobalStyles;
    
    import GlobalStyles from '../GlobalStyles';
    
    function App() {
        return (
            <Wrapper>
                <Router>
                    {/* An entire app here! */}
                </Router>
                <GlobalStyles />
            </Wrapper>
        )
    }
    
    export default App;
    

Dynamic Styles

  • Inline styles are quick and easy to add but have disadvantages

    • CSS being split up between the component and the style attribute can cause confusion
    • Not compatible with media queries, pseudo-classes and any CSS that's not a property/value
    const Button = ({ color, onClick, children }) => {
        return (
            <Wrapper onClick={onClick} style={{ color }}>
                {children}
            </Wrapper>
        );
    }
    
    const Wrapper = styled.button`
        color: black;
        padding: 16px 24px;
    `;
    
    • Setting properties in JavaScript requires a camelCase version of the property name
    <input
        style={{
            borderRadius: "8px",
            textDecoration: "none"
        }}
    />
    
  • Interpolation functions are the recommended way to manage dynamic styles in styled-components

    • CSS values are passed as props
    ...
        <Wrapper onClick={onClick} color={color}>
            {children}
        </Wrapper>
    ...
    
    const Wrapper = styled.button`
        color: ${props => props.color};
        padding: 16px 24px;
    `;
    
  • CSS variables allow the value of a property to be stored as a key in an object passed through the style attribute

    • Less flexible, can only be used to provide property values, which is limiting with media queries
    ...
        <Wrapper onClick={onClick} style={{ '--color': color }}>
            {children}
        </Wrapper>
    ...
    
    const Wrapper = styled.button`
        color: var(--color);
        padding: 16px 24px;
    `;
    

Component Libraries

  • A component library is a collection of generic, reusable components that can be used in multiple applications
  • A design system is made up of a suite of vector graphics produced in tools like Adobe or Figma
    • It may contain components like Button, Modal, etc.
    • Combine design components into mockups and map them neatly to the component library
    • Design tokens are values used to specify scales for sizes or colours
  • Helpful navgiation links often found near the top of product listing pages

    • Show hierarchy of content's structure
    • Quickly navigate between parent or grandparent category page
  • Some notes on using styled-components to implement this:

    • Can use negation logic to add styling to all elements that's not the first element
    • Same concept can be applied to the last element with last-of-type
    &:not(:first-of-type) { }
    
    • Can use nesting in this example which essentially equates to &:not(:first-of-type)::before
    &:not(:first-of-type) { 
        &::before {
            content: '/';
            opacity: 0.25;
        }
    }
    
    • If there are two or more places which can scale dynamically with some property value, use a variable
    • Now we only need to make an update to one variable if the spacing needs to change
    --spacing: 12px;
    
    &:not(:first-of-type) { 
        margin-left: var(--spacing);
    
        &::before {
            content: '/';
            opacity: 0.25;
            margin-right: var(--spacing);
        }
    }
    
    • Our navigation bar now has links separated by slashes with even spacing between the headers

Composition

  • If we need multiple variants of a component, we can create one component to serve as the base for another

    const Base = styled.button`
        font-size: 21px;
    `;
    
    const PrimaryButton = styled(Base)`
        background: blue;
        color: white;
    `;
    
  • In the custom component, we can receive new props such as variant or size which determine what styled component we want to render

    const Button = ({ variant, size, children }) => {
        const styles = SIZES[size];
    
        let Component;
        if (variant === "fill") {
            Component = FillButton;
        } else if (variant === "outline") {
            Component = OutlineButton;
        } else if (variant === "ghost") {
            Component = GhostButton;
        } else {
            throw new Error(`Unrecognized Button variant: ${variant}`);
        }
    
        return (
            <Component style={styles}>
                {children}
            </Component>
        );
    };
    

Dynamic tags

  • Use polymorphic as prop to dynamically change the tag that styled-components renders
    • In the following example, if href is empty then it will render a button element, otherwise, an anchor element is rendered
    • Can also render components as well, a common one to use is React Router's Link component
    function Button({ href, children }) {
        return (
            <Wrapper href={href} as={href ? 'a' : 'button'}>
                {children}
            </Wrapper>
        );
    }
    
    const Wrapper = styled.button`
        /* Styles */
    `;
    
    export const App = () => (
        <Button href="/">Hello</Button>
    );
    

Escape Hatches

  • Sometimes, when using a component in our library it needs to be tweaked slightly to accommodate the situation

  • For example, an alert that has a button that needs to be red instead of some other default colour it's been set

    • Usually, there are colour variants to represent different statuses

      • Blue: Information
      • Green: Success
      • Alert: Yellow
      • Danger: Red
    • To implement this, the styled-component could take in a new prop that adjusts the colour of the element based off the property value

  • If there were events such as Christmas or Halloween and you wanted to add some new styling to match the theme for a day, we can develop one-off components

    • We don't want to add more props like before because it adds significant complexity, it's not scalable and this will rarely get used
    import Button from './Button';
    
    const SpookyButton = styled(Button)`
        font-family: 'Spooky Halloween Font';
        background-color: orange;
        color: black;
    `;
    
    • Here, SpookyButton is based off the base Button component, but *Button isn't a styled-component
    • Custom components can be composed by adding a className prop and passing it through to the rendered component
    • Extending from the example in the composition section, className is an added prop passed to Button then passed through the rendered component
    const Button = ({ variant, size, children, className }) => {
        const styles = SIZES[size];
    
        let Component;
        if (variant === "fill") {
            Component = FillButton;
        } else if (variant === "outline") {
            Component = OutlineButton;
        } else if (variant === "ghost") {
            Component = GhostButton;
        } else {
            throw new Error(`Unrecognized Button variant: ${variant}`);
        }
    
        return (
            <Component style={styles} className={className}>
                {children}
            </Component>
        );
    };
    
    • This works because the styled-component we created will have its declarations extracted into a class, then appended to the <head> and apply that class to the element
    • Therefore, Button will receive the generated class name which is added to the rendered component
  • If you want to apply direct HTML, you can do so like here. The second option just uses a llower-friction API

    <div
        dangerouslySetInnerHTML={{
            __html: 'First &middot; Second'
        }}
    />;
    
    <div rawHtml="First &middot; Second" />
    

Single Source of Styles

  • We want to allow components to have different styles depending on context without affecting the styling of other elements

  • Consider a element that's used in multiple contexts and wrapped by different parent elements, we might want to apply a different style in each scenario

    • For the following example, we want the anchor tag to be black and underlined only within a Quote parent component
    const TextLink = styled.a`
        ...     /* Normal anchor styles */
    
        ${QuoteContent} & {
            color: black;
            text-decoration: revert;
        }
    `;
    
    • This is similar to the concept of inversion of control, where the parent element is nested rather than the child element being nested in the parent component
    • The downside is it could bloat the bundles, since we have to import the parent component to reference the styles

Extra Mini Components

  • Custom progress bar

    export default const ProgressBar = ({ value, size }) => {
        // Declare style object based off bar size
    
        return (
            <Wrapper
                role="progressbar"
                aria-valuenow={value}
                aria-valuemin="0"
                aria-valuemax="100"
                style={{
                    '--padding': styles.padding + 'px',
                    '--radius': styles.radius + 'px',
                }}
            >
                <VisuallyHidden>{value}%</VisuallyHidden>
                <BarWrapper>
                    <Bar
                        style={{
                            '--width': value + '%',
                            '--height': styles.height + 'px',
                        }}
                    />
                </BarWrapper>
            </Wrapper>
        );
    };
    
    const Wrapper = styled.div`
        background-color: ${COLORS.transparentGray15};
        box-shadow: inset 0px 2px 4px ${COLORS.transparentGray35};
        border-radius: var(--radius);
        padding: var(--padding);
    `;
    
    const BarWrapper = styled.div`
        border-radius: 4px;
    
        /* Trim off corners when progress bar is near-full. */
        overflow: hidden;
    `;
    
    const Bar = styled.div`
        width: var(--width);
        height: var(--height);
        background-color: ${COLORS.primary};
        border-radius: 4px 0 0 4px;
    `;
    
  • Custom select component

    const Select = ({ id, value, onChange, children }) => {
        const displayedValue = getDisplayedValue(value, children);
    
        return (
            <Wrapper>
            <NativeSelect id={id} value={value} onChange={onChange}>
                {children}
            </NativeSelect>
            <PresentationalBit>
                {displayedValue}
                <IconWrapper style={{ '--size': 24 + 'px' }}>
                <Icon id="chevron-down" strokeWidth={1} size={24} />
                </IconWrapper>
            </PresentationalBit>
            </Wrapper>
        );
    };
    
    const Wrapper = styled.div`
        position: relative;
        width: max-content;
    `;
    
    const NativeSelect = styled.select`
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        opacity: 0;
        /* Allow the select to span the full height in Safari */
        -webkit-appearance: none;
    `;
    
    const PresentationalBit = styled.div`
        color: ${COLORS.gray700};
        background-color: ${COLORS.transparentGray15};
        font-size: ${16 / 16}rem;
        padding: 12px 16px;
        padding-right: 52px;
        border-radius: 8px;
    
        ${NativeSelect}:focus + & {
            outline: 1px dotted #212121;
            outline: 5px auto -webkit-focus-ring-color;
        }
    
        ${NativeSelect}:hover + & {
            color: ${COLORS.black};
        }
    `;
    
    const IconWrapper = styled.div`
        position: absolute;
        top: 0;
        bottom: 0;
        right: 10px;
        margin: auto;
        width: var(--size);
        height: var(--size);
        pointer-events: none;
    `;