CSS-in-JS with near-zero runtime, SSR, multi-variant support, and a best-in-class developer experience.

  • By Modulz
  • Last update: Jan 9, 2023
  • Comments: 17
stitches

Stitches

Style your components with confidence

CSS-in-JS with near-zero runtime, SSR, multi-variant support, and a best-in-class developer experience.

Stitches Core

Framework-agnostic implementation.

npm install @stitches/core

Read more

Stitches React

React wrapper including the styled API.

npm install @stitches/react

Read more


Documentation

For full documentation, visit stitches.dev.

Contributing

Please follow our contributing guidelines.

Community

You can join the Stitches Discord to chat with other members of the community.

Here's a list of community-built projects:

Authors

License

Licensed under the MIT License, Copyright © 2021-present Modulz.

See LICENSE for more information.

Github

https://github.com/modulz/stitches

Comments(17)

  • 1

    [Feature] Dynamic variants

    This introduces a new set of features in stitches called dynamic variants.

    The purpose of this is to allow dynamic props to apply CSS values at static, server, and client-side.

    Example of dynamic spacer component.

    export const Spacer = styled("div", {
      $$spaceHeight: 0,
      $$spaceWidth: 0,
      
      minHeight: "$$spaceHeight",
      minWidth: "$$spaceWidth",
    
      variants: {
        size: (size: Stitches.ScaleValue<'space'>) => {
          $$spaceHeight: size,
          $$spaceWidth: size,
        },
        height: (value: Stitches.ScaleValue<'space'>) => {
          $$spaceHeight: value,
        },
        width: (value: Stitches.ScaleValue<'space'>) => {
          $$spaceWidth: value,
        }
      }
    })
    
  • 2

    React native support

    I really love what you guys have been doing with stitches and have been loving it so far!

    Any idea if react native is in the roadmap, I think the community would benefit a lot from this

  • 3

    Media queries inside variants

    We need to be able to place media queries inside a variant block.

    .Button.blue {
      @media (hover: hover) {
        // cool hover effect for the blue variant
      }
    }
    

    This is possible in all popular css-in-js libs and preprocessors.

    Without it, styling components becomes unmaintainable.

    This may be related to #79.

  • 4

    Incorrect style order

    Bug report

    Description

    Styles are applied in incorrect order depending on how styled component are composed.

    const StyledBase - styled(...)
    const Foo = styled(StyledBase, {...}) // works correctly
    
    const Base = (props) => <StyledBase {...props} />;
    const Bar = styled(Base, {...}) // StyledBase styles take precedence
    

    Things get even more unpredictable when using Foo & Bar on the same view. Depending on the order of Foo & Bar the styles might or might not apply correctly, see reproduction below.

    To Reproduce

    1. Open the following sandbox: https://codesandbox.io/s/nifty-blackburn-m792w?file=/src/App.js
    2. Observe behavior by uncommenting commented out variations one by one. Make sure to refresh the built-in csb browser each time.

    Expected behavior

    When using styled(Foo, styleObject), rules defined in styleObject should override Foo's existing styles.

    System information

    • OS: MacOS
    • Browser: Chrome, Safari, Firefox
    • Version of Stitches: 0.2+
  • 5

    'length' is declared here

    Bug report

    Describe the bug

    createStitches styled method requires length field to be set. length is part of String object.

    To Reproduce

    export const { styled } = createStitches({
      prefix: 'fluxui'
    });
    
    export const StyledIndicator = styled('div', {
      color: '$gray50',
      width: 15,
      height: 15
    });
    
    Argument of type '{ color: "$gray50"; width: number; height: number; }' is not assignable to parameter of type 'RemoveIndex<CSS<{}, { borderWeights: { light: string; normal: string; bold: string; extrabold: string; black: string; }; borderWidths: { default: string; 0: string; 2: string; 4: string; 8: string; }; breakpoints: { ...; }; ... 9 more ...; zIndices: { ...; }; }, DefaultThemeMap, {}>> & ... 6 more ... & { ...; }'.
      Property 'length' is missing in type '{ color: "$gray50"; width: number; height: number; }' but required in type '{ toString: () => string; readonly length: string | number | {} | CSS<{}, { borderWeights: { light: string; normal: string; bold: string; extrabold: string; black: string; }; borderWidths: { default: string; 0: string; 2: string; 4: string; 8: string; }; ... 10 more ...; zIndices: { ...; }; }, DefaultThemeMap, {}> |...'.ts(2345)
    lib.es5.d.ts(503, 14): 'length' is declared here.
    

    Expected behavior

    Not to have to enter length.

    System information

    • OS: Linux
    • Version of Stitches: 1.2.6
    • Version of Node.js: 17.4.0
  • 6

    :hover and :active specificity issue

    When trying to add :hover and :active pseudo states in CSS, we'd usually do:

    button:hover {}
    button:active {}
    

    This is so when you mousedown while hovered, the :active style takes precedence.

    Adding the same rules, in the same order, in Stitches (running in CRA), yields unexpected result.

    Repro: https://codesandbox.io/s/stitches-hoveractive-states-sezl2?file=/src/App.tsx:177-259

    In the Codesandbox above, notice how when you mousedown the background color remains red. It should change to blue.

    Out of curiosity, I tried to do the same with SSR (Next.js), and the SSR generated styles works as expected 😄

    SSR Demo: https://codesandbox.io/s/stitches-hoveractive-states-with-ssr-lyc6f?file=/pages/index.js

  • 7

    Add support for custom (human-friendly) class names

    Taking a stab at a different API to this, based on feedback in #916 (see details there).

    (Addresses #650)

    Proposed API

    Users can use a withName utility method to pass in a string as the custom component name, with their calls to styled, like so:

    // Renders as `c-Label-sOm3h4Sh`
    const Label = styled.withName("Label")("label", {...})
    

    This proposed solution could be combined with a babel plugin, to make friendly class names possible with no extra effort, for those using Babel.

    The existing syntax would be unchanged, plus this approach works just as well with css.withName("Xyz")(...), so it's not limited to React only.


    I feel that this functionality makes debugging much, much easier, so would love to work with the core team to find the right API to get this into production ❤️ .

  • 8

    SolidJS package

    This closes #818

    This package package is 98% copy paste from @stitches/react with some typing changes + a styled.js implementation. I'm fairly certain we could share types better between these two packages but not sure if that needs to be done right away.

    This PR isn't ready to merge yet, I need to update tests for solid but wanted to get the conversation going to see if I should press forward.

  • 9

    React.ComponentProps not working as expected

    Bug report

    Describe the bug

    When wanting to get the type of the props a stitches component receives, for example to be able to compose other components on top of it, React.ComponentProps<typeof StitchesComponent> is not working as expected — it's signaling weird errors and not providing correct typings. More context in this discussion.

    To Reproduce

    Steps to reproduce the behavior, please provide code snippets or a repository:

    1. Go to this sandbox.
    2. Scroll down to the implementetion of <LoadingButton />.
    3. See that <LoadingButton /> has type errors. (property as is required).
    4. Also, see that the props are not button-specific (see screenshot below).

    Expected behavior

    I expected LoadingButton to be error free with the props provided, and for prop suggestions to be button-specific.

    Screenshots

    CleanShot 2020-09-26 at 10 54 37@2x
  • 10

    IE11 support without Proxy, alternative API, and feedback

    Hey there,

    I was checking the code and it uses Proxy which doesn't have IE11 support. I initially thought that maybe I could use a Proxy polyfill. However, this kind of polyfills require to know all properties at the time the Proxy is created which isn't possible here.

    Would you consider also an alternative (additional) API? Something more close to CSS-in-JS syntax? For example:

    const { createCss } = require("@stitches/css");
    
    const css = createCss({
      screens: {
        desktop: (rule) => `@media (min-width: 700px) { ${rule} }`,
      },
    });
    
    const button = css.compose(
      css.color("gray"),
      css.padding("1rem"),
      css.color("black", ":hover"),
      css.desktop.backgroundColor("tomato", ":hover"),
      css.borderColor("red"),
      css.desktop.borderColor("black")
    );
    
    const alternative = {
      color: "gray",
      padding: "1rem",
      "&:hover": {
        color: "black",
        backgroundColor: { desktop: "tomato" },
      },
      borderColor: { default: "red", desktop: "black" },
    };
    

    Here button and alternative are equivalent. It's closer to CSS and it doesn't really require a Proxy to know which are utility or css props, for instance. It's also fairly easy to type in TS.

    A working example of the above:

    const toStitchesCSS = (obj, pseudo) => {
      const call = (cssIns, prop, value) => {
        return cssIns[prop](value, pseudo);
      };
      const keys = Object.keys(obj);
      const comp = [];
    
      for (const key of keys) {
        const value = obj[key];
        if (typeof value !== "object") comp.push(call(css, key, value));
        else if (key.startsWith("&:"))
          comp.push(toStitchesCSS(value, key.replace(/^&/, "")));
        else
          comp.push(
            ...Object.keys(value).map((screen) => {
              if (screen === "default") return call(css, key, value[screen]);
              return call(css[screen], key, value[screen]);
            })
          );
      }
    
      return css.compose.apply(css, comp);
    };
    

    Then you can:

    const sortAsc = classes => classes.split(' ').sort().join(' ') // Just to compare
    const buttonClasses = sortAsc(button.toString())
    const alternativeClasses = sortAsc(toStitchesCSS(alternative).toString())
    
    console.log({
      button: buttonClasses,
      alternative: alternativeClasses,
      isEqual: alternativeClasses === buttonClasses // true
    });
    

    Of course, this is an abstraction on top of the current implementation that uses Proxy but it's just to demonstrate that it could also work with an alternative (additional) API closer to CSS-in-JS object syntax (and it'd work in IE11 🙈)

  • 11

    Deprecate `defaultVariants` in favour of `props` and `defaultProps`

    After a long discussion internally and with many of our users, we've decided to allow users to create default props when creating a Stitches Component.

    Reasons:

    • People want the ability to define custom class names
    • People want the ability to set default attributes

    As a result, we'll rename defaultVariants to defaultProps, for the following reasons:

    • Variants are also props, so they can be set via defaultProps
    • Keep the API surface low
    • Allow people to define responsive default variants (this is currently not possible with defaultVariants)

    Additionally, there seems to be a lot of confusion around the fact that defaultVariants get inherited by compositions. This breaks most people expectations. There's an issue for it here https://github.com/modulz/stitches/issues/686.

    By renaming from defaultVariants to defaultProps we believe it'll make it clearer that the props will not be carried over - since each component can have different responsibilities and different type, meaning attributes have to be dependant on its type.

    This would also allow people to define an as prop. Would this be problematic in terms of types?

    For example:

    const Button = styled('button', {})
    const Link = styled(Button, {
      defaultProps: {
        as: "a",
        href: '' // <- would we get polymorphism here?
      }
    })
    

    About polymorphism, we need to understand how much this will stall the TS engine.

    Another solution to keep in mind, is to prevent the as prop in defaultProps, and instead force it to be defined as the type (1st argument)

    const Button = styled('button', {})
    const Link = styled('a', Button, {
      defaultProps: {
        href: '' // <- would we get polymorphism here?
      }
    })
    

    Are there any pros and cons?


    UPDATE:

    After some thought, I think the right course of action is to restrict the usage ofasindefauultProps`.

    It creates too many layers of polymorphism, and requires a lot of mental model to understand what's going on. Additionally, it opens up many doors to complex and confusing architecture.

    The idea here is that the as prop is only available on components returned by styled().


    UPDATE 2:

    After even more thoughts, chats, and calls, we've arrived at the following conclusion:

    • Carrying over props (variants, etc) to compositions by default is confusing and leads to unexpected expectations
    • Users want the ability decide which props do get carried over, as it can be a powerful architecture decision when done on purpose
    • Users want the ability to define props within the styled function (this is something supported by all styled()-like APIs, so it makes sense we support it too, for better interoperability and hopefully more users wanting to migrate over - with more ease)

    So if we consider this for a moment, I think we could make this work with the following objects:

    • props: your component will be initialised with these props and they will not carry over to compositions
    • defaultProps: your component will be initialised with these props and they will carry over to compositions
    const Button = styled('button', {
      variants: {
        outlined: {
          true: {},
        },
        shape: {
          square: {},
          round: {}
        },
        size: {
          small: {}
        }
      },
    
      props: {
        shape: 'square',
        outlined: true
      },
    
      defaultProps: {
        size: 'small'
      }
    })
    
    () => <Button />
    // square, outlined and small
    
    const RoundedButton = styled(Button, {
      ...styles,
      defaultProps: {
        shape: 'round'
      }
    })
    
    () => <RoundedButton />
    // small and square
    
    const IconButton = styled(RoundedButton, {
      ...styles,
      props: {
        outlined: true
      }
    })
    
    () => <IconButton />
    // small, square and outlined
    

    So, the actions for this issue are:

    • [ ] Deprecate defaultVariants
    • [ ] Introduce a props object: the component will be initialised with this, and this will not get carried over to compositions
    • [ ] Introduce a defaultProps object: the component will be initialised with this, and this will get carried over to compositions
    • [ ] Ensure as key is not allowed in props and defaultProps
  • 12

    Variants which contain numeric strings (e.g. "-12") and number keys (e.g. 12) are assigned a 'number' type when generating a union using the VariantProps type

    Let's say I create a component with a variant called 'margin', which has a mix of numeric and numeric string keys, like so:

    const StyledBox = styled('div', {
        margin: {
          '-12': { margin: -12 },
          12: { margin: 12 },
        },
    })
    

    I would expect, if I then passed StyledBox to Stitches.VariantProps (e.g. Stitches.VariantProps<typeof StyledBox>), the following type would be generated considering that the keys are both technically numeric:

    margin?: -12 | 12 | ({
        "@md"?: -12 | 12 | undefined;
        "@lg"?: -12 | 12 | undefined;
        "@initial"?: -12 | 12 | undefined;
    } & {
        [x: string]: -12 | 12 | undefined;
    }) | undefined
    

    However, the following type is generated, which contains the 'number' type:

    margin?: number | "-12" | "12" | ({
        "@md"?: number | "-12" | "12" | undefined;
        "@lg"?: number | "-12" | "12" | undefined;
        "@initial"?: number | "-12" | "12" | undefined;
    } & {
        [x: string]: number | "-12" | "12" | undefined;
    }) | undefined
    

    Naturally this is not ideal for strong typing as any number can be accepted as a variant key. However, interestingly, if I change the variant negative key (e.g. "-12") to a non numeric string (e.g. "neg12") the types are correct/how I would want them in my first example. For example:

    const StyledBox = styled('div', {
        margin: {
          'neg12': { margin: -12 },
          12: { margin: 12 },
        },
    })
    

    And the correctly generated types:

    margin?: 12 | "12" | "neg12" | ({
        "@md"?: 12 | "12" | "neg12" | undefined;
        "@lg"?: 12 | "12" | "neg12" | undefined;
        "@initial"?: 12 | "12" | "neg12" | undefined;
    } & {
        [x: string]: 12 | "12" | "neg12" | undefined;
    }) | undefined
    

    This is not a big issue as I can just change any numeric strings (e.g. "-12") to non-numeric strings (e.g. "neg12") in my variant keys, but I hope it's something that can be fixed in the future.

    Stitches version: 1.2.8

  • 13

    Token from theme object not assignable to util

    Bug report

    When trying to use utils in combination with tokens from theme I get a ts error.

    Describe the bug

    import { styled, theme } from '../../../styles/stitches.config'
    
    const Component = styled('span', {
      mv: theme.space.xs, // TypeError
    })
    
    image
    // stitches.config:
    
    export const {
      styled,
      theme,
    } = createStitches({
      utils: {
        mv: (value: PropertyValue<'margin'>) => ({ marginTop: value, marginBottom: value }),
      },
    })
    
    

    Due to the lack of information on this topic in docs, along with inability to solve it with neither PropertyValue nor ScaleValue and missing export of appropriate type, I assume this might be a bug.

    System information

    • Version of Stitches: ^1.2.8
    • Version of Node.js: 16.17.0
    • ts: 4.7.4
  • 14

    Page becomes unresponsive when using multiple custom stitches components

    Bug report

    Describe the bug

    I created a sandbox with React + Typescript + Stitches. I created some basic components like Box and Text with stitches, something like below

    const BaseBox = styled("div", {
      // Some variants
    });
    
    type BoxProps = ComponentProps<typeof BaseBox> & { testID?: string };
    
    function Box(props: BoxProps) {
      const { testID, ...restProps } = props;
      return <BaseBox {...restProps} data-testid={testID} />;
    }
    
    const BaseText = styled(BaseBox, {
      // Some variants
    });
    
    type TextProps = ComponentProps<typeof BaseText> & { testID?: string };
    
    function Text(props: TextProps) {
      const { testID, ...restProps } = props;
      return <Text {...restProps} data-testid={testID} />;
    }
    

    When I am using the above components my page is freezing and no UI is rendered

    To Reproduce

    Steps to reproduce the behavior:

    1. Go to the sandbox
    2. After the Codesandbox UI loading is complete, the preview section still appears blank

    Steps to see the preview section

    1. Remove stitches components usage in App.tsx
    2. Create a simpler UI with div and span
    3. Now you should see the preview

    Expected behavior

    When using stitches components should not freeze my page.

    Screenshots

    Firefox image

    System information

    • OS: Ubuntu
    • Browser: Chrome - 108.0.5359.124, Firefox - 108.0.1
    • Version of Stitches: 1.2.8
    • Version of Node.js: 14.18.2

    Additional context

  • 15

    not working from next.js 13 & need server side supports for next.js 13

    Bug report

    I tried on next.js 13 app dir and it seems styling is not working.

    I read the issue below and found couple of answers to figure it out example:

    "use client"
    
    import React from "react"
    import { useServerInsertedHTML } from "next/navigation"
    import { getCssText } from "../../stitches.config"
    
    export function ServerStylesheet({ children }) {
      useServerInsertedHTML(() => {
        return <style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
      })
    
      return children
    }
    

    #1109

    but still not working for me (even from client side). Any solutions suggested?

  • 16

    In a grid layout, gap configured in breakpoints won't work

    Bug report

    Describe the bug

    When writing a stitches object, the grid column/row object won't apply on the element.

    To Reproduce

    The object looks like this:

    const TablesGrid = styled('div', {
      display: 'grid',
    
      variants: {
        gap: {
          horizontal: {
            gap: 4,
            gridTemplateColumns: '1fr 1fr',
          },
          vertical: {
            gap: 16,
            gridTemplateColumns: '1fr',
          },
        },
      },
    });
    

    Expected behavior

    Expected that the gap will be applied based on different breakpoint.

    Screenshots

    CleanShot 2022-12-04 at 12 48 38@2x

    CleanShot 2022-12-04 at 12 48 31@2x

    System information

    • OS: macOS
    • Browser (if applies) Chrome
    • Version of Stitches: 1.2.8
    • Version of Node.js: v16.18.1
  • 17

    (Feature request): good name of components generated by stitches inside react dev tools

    Hi stiches team,

    is there a way that generated stitches components get the right component name inside react developper tools ? It's quite hard to read inside the react chrome inspector. In the examples bellow i would like to see ScrollableContent instead of Styled.div inside the react chrome inspector

    image

    image