CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tss-react

Type safe CSS-in-JS API heavily inspired by react-jss

Pending
Overview
Eval results
Files

withstyles-hoc.mddocs/

WithStyles HOC

The WithStyles Higher-Order Component (HOC) provides a pattern for injecting styles into React components. It supports both function and class components with full TypeScript integration and is compatible with Material-UI v4 withStyles patterns.

Capabilities

WithStyles Factory Function

Creates a withStyles HOC function with theme support and optional custom cache configuration.

/**
 * Creates a withStyles HOC function with theme support
 * @param params - Configuration object with theme provider and optional cache
 * @returns Object containing withStyles function
 */
function createWithStyles<Theme>(params: {
  useTheme: () => Theme;
  cache?: EmotionCache;
}): {
  withStyles<
    Component extends ReactComponent<any> | keyof ReactHTML,
    Props extends ComponentProps<Component>,
    CssObjectByRuleName extends Record<string, CSSObject>
  >(
    Component: Component,
    cssObjectByRuleNameOrGetCssObjectByRuleName:
      | CssObjectByRuleName
      | ((theme: Theme, props: Props, classes: Record<string, string>) => CssObjectByRuleName)
  ): ComponentType<Props>;
};

type ReactComponent<P> = ComponentType<P>;
type ComponentProps<T> = T extends ComponentType<infer P> ? P : T extends keyof ReactHTML ? ReactHTML[T] extends ComponentType<infer P> ? P : never : never;

Usage Examples:

import { useTheme } from "@mui/material/styles";
import { createWithStyles } from "tss-react";

// Create withStyles with MUI theme
const { withStyles } = createWithStyles({ useTheme });

// Custom cache configuration
import createCache from "@emotion/cache";

const customCache = createCache({
  key: "my-styles",
  prepend: true
});

const { withStyles: withStylesCustomCache } = createWithStyles({
  useTheme,
  cache: customCache
});

WithStyles HOC Usage

Higher-order component that wraps components with style injection capabilities.

/**
 * Higher-order component for injecting styles into React components
 * @param Component - React component to wrap (function, class, or HTML element)
 * @param cssObjectByRuleNameOrGetCssObjectByRuleName - Static styles object or function
 * @returns Enhanced component with injected classes prop
 */
function withStyles<
  Component extends ReactComponent<any> | keyof ReactHTML,
  Props extends ComponentProps<Component>,
  CssObjectByRuleName extends Record<string, CSSObject>
>(
  Component: Component,
  cssObjectByRuleNameOrGetCssObjectByRuleName:
    | CssObjectByRuleName
    | ((
        theme: Theme,
        props: Props,
        classes: Record<keyof CssObjectByRuleName, string>
      ) => CssObjectByRuleName)
): ComponentType<Props & { classes?: Partial<Record<keyof CssObjectByRuleName, string>> }>;

Usage Examples:

import React from "react";
import { useTheme } from "@mui/material/styles";
import { createWithStyles } from "tss-react";

const { withStyles } = createWithStyles({ useTheme });

// Function component with static styles
const Button = withStyles(
  ({ children, classes, ...props }: { 
    children: React.ReactNode; 
    classes?: { root?: string; label?: string };
  }) => (
    <button className={classes?.root} {...props}>
      <span className={classes?.label}>{children}</span>
    </button>
  ),
  {
    root: {
      backgroundColor: "blue",
      color: "white",
      border: "none",
      borderRadius: 4,
      padding: "8px 16px",
      cursor: "pointer",
      "&:hover": {
        backgroundColor: "darkblue"
      }
    },
    label: {
      fontWeight: "bold"
    }
  }
);

// Function component with dynamic styles
interface CardProps {
  title: string;
  elevated: boolean;
  classes?: { root?: string; title?: string; content?: string };
  children: React.ReactNode;
}

const Card = withStyles(
  ({ title, children, classes, elevated, ...props }: CardProps) => (
    <div className={classes?.root} {...props}>
      <h3 className={classes?.title}>{title}</h3>
      <div className={classes?.content}>{children}</div>
    </div>
  ),
  (theme, { elevated }) => ({
    root: {
      backgroundColor: theme.palette.background.paper,
      borderRadius: theme.shape.borderRadius,
      padding: theme.spacing(2),
      boxShadow: elevated ? theme.shadows[4] : theme.shadows[1],
      transition: theme.transitions.create("box-shadow")
    },
    title: {
      margin: 0,
      marginBottom: theme.spacing(1),
      color: theme.palette.text.primary,
      fontSize: theme.typography.h6.fontSize
    },
    content: {
      color: theme.palette.text.secondary
    }
  })
);

// HTML element enhancement
const StyledDiv = withStyles(
  "div",
  theme => ({
    root: {
      backgroundColor: theme.palette.background.default,
      padding: theme.spacing(3),
      minHeight: "100vh"
    }
  })
);

// Usage
function App() {
  return (
    <StyledDiv>
      <Card title="Welcome" elevated={true}>
        <p>This is a styled card component.</p>
        <Button>Click me</Button>
      </Card>
    </StyledDiv>
  );
}

Class Component Support

WithStyles works seamlessly with React class components:

import React, { Component } from "react";

interface MyClassComponentProps {
  title: string;
  classes?: {
    root?: string;
    title?: string;
    button?: string;
  };
}

interface MyClassComponentState {
  count: number;
}

class MyClassComponent extends Component<MyClassComponentProps, MyClassComponentState> {
  state = { count: 0 };

  handleClick = () => {
    this.setState(prev => ({ count: prev.count + 1 }));
  };

  render() {
    const { title, classes } = this.props;
    const { count } = this.state;

    return (
      <div className={classes?.root}>
        <h2 className={classes?.title}>{title}</h2>
        <p>Count: {count}</p>
        <button className={classes?.button} onClick={this.handleClick}>
          Increment
        </button>
      </div>
    );
  }
}

const StyledClassComponent = withStyles(
  MyClassComponent,
  theme => ({
    root: {
      padding: theme.spacing(2),
      backgroundColor: theme.palette.background.paper,
      borderRadius: theme.shape.borderRadius
    },
    title: {
      color: theme.palette.primary.main,
      marginBottom: theme.spacing(1)
    },
    button: {
      backgroundColor: theme.palette.secondary.main,
      color: theme.palette.secondary.contrastText,
      border: "none",
      padding: theme.spacing(1, 2),
      borderRadius: theme.shape.borderRadius,
      cursor: "pointer"
    }
  })
);

Style Overrides

Components wrapped with withStyles accept a classes prop for style customization:

function CustomizedCard() {
  return (
    <Card
      title="Custom Card"
      elevated={false}
      classes={{
        root: "my-custom-root-class",
        title: "my-custom-title-class"
      }}
    >
      <p>This card has custom styling applied.</p>
    </Card>
  );
}

// CSS-in-JS style overrides
const useOverrideStyles = makeStyles()(theme => ({
  customRoot: {
    backgroundColor: theme.palette.warning.light,
    border: `2px solid ${theme.palette.warning.main}`
  },
  customTitle: {
    color: theme.palette.warning.contrastText,
    textTransform: "uppercase"
  }
}));

function CssInJsOverrides() {
  const { classes } = useOverrideStyles();
  
  return (
    <Card
      title="CSS-in-JS Overrides"
      elevated={false}
      classes={{
        root: classes.customRoot,
        title: classes.customTitle
      }}
    >
      <p>Styled with CSS-in-JS overrides.</p>
    </Card>
  );
}

Migration from Material-UI v4

The withStyles API provides seamless migration from @material-ui/core v4:

// Before (Material-UI v4)
import { withStyles } from "@material-ui/core/styles";

const StyledComponent = withStyles(theme => ({
  root: {
    backgroundColor: theme.palette.background.paper,
    padding: theme.spacing(2)
  }
}))(({ classes }) => (
  <div className={classes.root}>Content</div>
));

// After (TSS-React)
import { createWithStyles } from "tss-react";
import { useTheme } from "@mui/material/styles";

const { withStyles } = createWithStyles({ useTheme });

const StyledComponent = withStyles(
  ({ classes }: { classes?: { root?: string } }) => (
    <div className={classes?.root}>Content</div>
  ),
  theme => ({
    root: {
      backgroundColor: theme.palette.background.paper,
      padding: theme.spacing(2)
    }
  })
);

Advanced Patterns

Nested Selectors with Classes Reference

const NestedComponent = withStyles(
  ({ classes }: { classes?: { root?: string; item?: string; selected?: string } }) => (
    <div className={classes?.root}>
      <div className={classes?.item}>Item 1</div>
      <div className={`${classes?.item} ${classes?.selected}`}>Item 2 (Selected)</div>
    </div>
  ),
  (theme, props, classes) => ({
    root: {
      padding: theme.spacing(2),
      [`& .${classes.item}`]: {
        padding: theme.spacing(1),
        borderBottom: `1px solid ${theme.palette.divider}`,
        "&:last-child": {
          borderBottom: "none"
        }
      },
      [`& .${classes.selected}`]: {
        backgroundColor: theme.palette.action.selected,
        fontWeight: theme.typography.fontWeightBold
      }
    },
    item: {},
    selected: {}
  })
);

Conditional Styling with Props

interface AlertProps {
  severity: "info" | "warning" | "error" | "success";
  message: string;
  classes?: { root?: string; icon?: string; message?: string };
}

const Alert = withStyles(
  ({ severity, message, classes }: AlertProps) => (
    <div className={classes?.root}>
      <span className={classes?.icon}>⚠️</span>
      <span className={classes?.message}>{message}</span>
    </div>
  ),
  (theme, { severity }) => {
    const colors = {
      info: theme.palette.info,
      warning: theme.palette.warning,
      error: theme.palette.error,
      success: theme.palette.success
    };
    
    const color = colors[severity];
    
    return {
      root: {
        display: "flex",
        alignItems: "center",
        padding: theme.spacing(1, 2),
        backgroundColor: color.light,
        color: color.contrastText,
        borderRadius: theme.shape.borderRadius,
        border: `1px solid ${color.main}`
      },
      icon: {
        marginRight: theme.spacing(1),
        fontSize: "1.2em"
      },
      message: {
        flex: 1
      }
    };
  }
);

TypeScript Integration

WithStyles provides full TypeScript support with proper prop inference:

// Component with strict typing
interface StrictButtonProps {
  variant: "primary" | "secondary";
  size: "small" | "medium" | "large";
  disabled?: boolean;
  children: React.ReactNode;
  onClick?: () => void;
  classes?: {
    root?: string;
    label?: string;
  };
}

const StrictButton = withStyles(
  ({ variant, size, disabled, children, classes, onClick }: StrictButtonProps) => (
    <button
      className={classes?.root}
      disabled={disabled}
      onClick={onClick}
    >
      <span className={classes?.label}>{children}</span>
    </button>
  ),
  (theme, { variant, size, disabled }) => ({
    root: {
      backgroundColor: variant === "primary" ? theme.palette.primary.main : theme.palette.secondary.main,
      color: variant === "primary" ? theme.palette.primary.contrastText : theme.palette.secondary.contrastText,
      padding: {
        small: theme.spacing(0.5, 1),
        medium: theme.spacing(1, 2),
        large: theme.spacing(1.5, 3)
      }[size],
      fontSize: {
        small: theme.typography.body2.fontSize,
        medium: theme.typography.body1.fontSize,
        large: theme.typography.h6.fontSize
      }[size],
      opacity: disabled ? 0.5 : 1,
      cursor: disabled ? "not-allowed" : "pointer",
      border: "none",
      borderRadius: theme.shape.borderRadius,
      transition: theme.transitions.create(["background-color", "opacity"])
    },
    label: {
      fontWeight: theme.typography.fontWeightMedium
    }
  })
);

// Usage with full type checking
function TypedExample() {
  return (
    <StrictButton
      variant="primary"
      size="medium"
      onClick={() => console.log("Clicked!")}
    >
      Click me
    </StrictButton>
  );
}

GetClasses Utility

The withStyles HOC includes a getClasses utility function for accessing generated class names within component render functions. This is particularly useful for programmatic access to styles.

/**
 * Utility function attached to withStyles for accessing class names
 * @param props - Component props containing className and classes
 * @returns Generated class names object
 */
withStyles.getClasses = function getClasses<Classes>(props: {
  className?: string;
  classes?: Classes;
}): Classes extends Record<string, unknown>
  ? Classes extends Partial<Record<infer K, any>>
    ? Record<K, string>
    : Classes
  : { root: string };

Usage Examples:

import { createWithStyles } from "tss-react";
import { useTheme } from "@mui/material/styles";

const { withStyles } = createWithStyles({ useTheme });

// Component that uses getClasses utility
interface CardProps {
  title: string;
  content: string;
  className?: string;
  classes?: {
    root?: string;
    header?: string;
    title?: string;
    content?: string;
    footer?: string;
  };
}

const Card = withStyles(
  (props: CardProps) => {
    // Access classes programmatically
    const classes = withStyles.getClasses(props);
    
    return (
      <div className={classes.root}>
        <header className={classes.header}>
          <h2 className={classes.title}>{props.title}</h2>
        </header>
        <div className={classes.content}>
          {props.content}
        </div>
        <footer className={classes.footer}>
          <button>Action</button>
        </footer>
      </div>
    );
  },
  theme => ({
    root: {
      backgroundColor: theme.palette.background.paper,
      borderRadius: theme.shape.borderRadius,
      boxShadow: theme.shadows[2],
      overflow: "hidden"
    },
    header: {
      backgroundColor: theme.palette.primary.main,
      color: theme.palette.primary.contrastText,
      padding: theme.spacing(2)
    },
    title: {
      margin: 0,
      fontSize: theme.typography.h5.fontSize,
      fontWeight: theme.typography.fontWeightMedium
    },
    content: {
      padding: theme.spacing(2),
      color: theme.palette.text.primary
    },
    footer: {
      padding: theme.spacing(1, 2),
      borderTop: `1px solid ${theme.palette.divider}`,
      backgroundColor: theme.palette.background.default
    }
  })
);

// Usage
function App() {
  return (
    <Card
      title="My Card"
      content="This is the card content"
      classes={{
        root: "custom-card-root",
        title: "custom-card-title"
      }}
    />
  );
}

Important Notes:

  • getClasses should only be used within components wrapped by withStyles
  • The function expects props to contain a classes object provided by withStyles
  • It returns the actual generated CSS class names for programmatic use
  • Useful for complex conditional styling logic that needs access to class names
  • The returned classes object maintains type safety based on the component's classes interface

Error Handling:

// getClasses will throw an error if used incorrectly
try {
  const classes = withStyles.getClasses({ classes: undefined });
} catch (error) {
  console.error("getClasses should only be used in conjunction with withStyles");
}

Install with Tessl CLI

npx tessl i tessl/npm-tss-react

docs

compatibility.md

core-tss-api.md

css-utilities.md

dsfr-integration.md

global-styles-keyframes.md

index.md

makestyles-api.md

mui-integration.md

nextjs-ssr.md

withstyles-hoc.md

tile.json