Search code examples
reactjstheme-ui

Theme UI sx prop ignored when importing a storybook component


I have setup and published to a private server a StoryBook design system using ThemeUI that contains components. One such component is the button shown below.

import React, { FC } from "react";
import { Button as ButtonUI, SxStyleProp } from "theme-ui";

export const Button: FC<ButtonProps> = ({ onClick, children, variant, sx, disabled, onMouseOver, onMouseOut }) => {
    return (
        <ButtonUI
            disabled={disabled || false}
            variant={variant || "primary"}
            onClick={onClick}
            sx={{...sx}}
            onMouseOver={onMouseOver || (() => {})}
            onMouseOut={onMouseOut || (() => {})}
        >
            {children}
        </ButtonUI>
    );
};

export type ButtonProps = {
    /**
     * The action to perform when the button is clicked
     */
    onClick: () => void;
    /**
     * The contents of the button
     */
    children?: any;
    /**
     * The type of button
     */
    variant?: string;
    /**
     * custom styles for the button
     */
    sx?: SxStyleProp;
    /**
     * If the button is disabled
     */
    disabled?: boolean;
    /**
     * The action to perform if the mouse moves over the button
     */
    onMouseOver?: () => void;
    /**
     * The action to perform if the mouse moves off the button
     */
    onMouseOut?: () => void;
};

Button.defaultProps = {
    variant: "primary",
    disabled: false
};

When I import this component into my React App in a separate project the component renders but all properties in the sx prop are ignored...

/** @jsx jsx */
import { Flex, jsx } from "theme-ui";
import Modal from "../Modal";
import { Button } from "<<<PRIVATE SERVER>>>";

/**
 * Renders a popup the gives the player the option to logout
 * @param title the heading of the modal
 * @param confirmString the text for the confirm button
 * @param cancelString the text for the cancel button
 * @param confirmAction the action to perform when the user confirms
 * @param cancelAction the action to perform when the user cancels
 */
export default function LogoutModal({ title, confirmString, cancelString, confirmAction, cancelAction }: Props) {

    return (
        <Modal
            title={title}
            closeAction={cancelAction}
            children={
                <Flex>
                    <Button
                        onClick={confirmAction}
                        sx={{
                            mr: 1
                        }}
                        variant="negative">{confirmString}</Button>
                    <Button
                        onClick={cancelAction}
                        sx={{
                            ml: 1
                        }}
                        variant="secondary">{cancelString}</Button>
                </Flex>
            }
        />
    );
}

interface Props {
    title: string;
    confirmString: string;
    cancelString: string;
    confirmAction: () => void;
    cancelAction: () => void;
}

So the button renders but without the applicable margin (or any other styles I add in the sx prop. Does anyone have any idea why this would be the case?

Interestingly if I call the component somewhere else in Storybook (rather than when imported into a seperate project) the sx prop works as expected.


Solution

  • TL;DR sx gets transformed to a className by theme-ui

    See the working CodeSandbox demo, then continue reading.


    From the documentation:

    Under the hood, Theme UI uses a custom pragma comment that converts a theme-aware sx prop into a style object and passes it to Emotion's jsx functions. The sx prop only works in modules that have defined a custom pragma at the top of the file, which replaces the default React jsx functions. This means you can control which modules in your application opt into this feature without the need for a Babel plugin or additional configuration. This is intended as a complete replacement for the Emotion custom JSX pragma.

    Use the pragma on every file

    So it's first needed to have the pragma comment at the top of the file, and import the right jsx function:

    /** @jsx jsx */
    import { jsx, Button as ButtonUI, SxStyleProp } from 'theme-ui';
    

    The sx prop isn't passed down to the component, it's in fact converted to CSS and a className is auto-generated and applied to the component.

    Pass down the className

    So if you'd like the parent component applying custom style to the Button component via the sx prop, you need to pass down the className prop.

    export const Button: FC<ButtonProps> = ({
      onClick,
      children,
      variant,
      // This does nothing...
      // sx,
      disabled,
      onMouseOver,
      onMouseOut,
      // Make sure to pass the className down to the button.
      className, // or ...props
    }) => (
      <ButtonUI
        className={className}
        // {...props} // if you want any other props
        sx={{ /* any other style can be applied here as well */ }}
    
        disabled={disabled || false}
        variant={variant || 'primary'}
        onClick={onClick}
        onMouseOver={onMouseOver || (() => {})}
        onMouseOut={onMouseOut || (() => {})}
      >
        {children}
      </ButtonUI>
    );
    

    Personal recommendation is to not use some kind of hack passing down sx content down. I've been working on a huge project professionally that uses theme-ui and never have we ever needed anything else than pass the className down to merge styles automatically.