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

  • By Modulz
  • Last update: Nov 28, 2022
  • Comments: 16
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(16)

  • 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

    (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

  • 13

    Hydration mismatch when targeting existing Stitches component?!

    Bug report

    Describe the bug

    As I was refactoring my personal website using Stitches v1.2.8 and NextJS v12, I got a classic Warning: PropclassNamedid not match. Server: "c-golZua" Client: "c-boJmFo" message, which means hydration error... But there is no use of window or anything fancy in my component, except maybe the fact that it targets another Stitches component...:

    // styles.ts
    import { styled, theme } from 'stitches.config'
    
    import { Text } from '@components/content'
    
    export const ArticleLink = styled('a', {
      position: 'relative',
      display: 'grid',
      gridTemplateColumns: 'auto max-content',
      borderTop: '1px solid',
      borderBottom: '1px solid',
      borderColor: theme.colors.border,
      textDecoration: 'none',
      py: theme.space[1],
      marginTop: '-1px',
    
      [`${Text}`]: { transition: 'color 300ms' },
    
      [`&:hover ${Text}`]: { color: theme.colors.primary },
    })
    
    // index.tsx
    import type { ReactElement } from 'react'
    import NextLink, { LinkProps as NextLinkProps } from 'next/link'
    
    import { Text } from '@components/content'
    
    import { ArticleLink, Aside } from './styles'
    
    interface ArticleProps extends Pick<NextLinkProps, 'href'> {
      title?: string
      body?: string
      metas?: string[]
    }
    
    const Article = ({ title, body, metas, href }: ArticleProps): ReactElement => (
      <NextLink href={href} passHref>
        <ArticleLink title={title} tabIndex={0} role="link">
          {...}
        </ArticleLink>
      </NextLink>
    )
    
    export default Article
    

    Expected behavior

    Styling targeting Stitches components should not be a problem :question: :confused:

    Screenshots

    image

    System information

    • OS: Ubuntu 22.04
    • Browser (if applies) Firefox (latest), Brave (latest)
    • Version of Stitches: 1.2.8
    • Version of Node.js: 14.21.1

    Additional context

    As I was looking for some explanation, I tried removing the references to the other Stitches component, and hydration mismatch error were gone! This is why I'm guessing it could be related, even if I don't understand the reason... :exploding_head:

    Also in case it could help, here is a link to my WIP branch: https://gitlab.com/soykje/soykje.gitlab.io/-/blob/reboot-with-stitches/src/components/content/Article/styles.ts#L16

    Thx in advance for your help :pray:

  • 14

    CSS type does not provide theme tokens intellisense

    Bug report

    Describe the bug

    When leveraging the CSS type to create a custom css prop, the resulting type does not include any tokens from the Stitches theme.

    To Reproduce

    1. Create a stitches config
      const Config = createStitches({
          theme: {
              colors: {
                  primary: 'red',
              },
          },
      });
      
    2. Declare a type for CSS
      type CSSProp = CSS<typeof Config>;
      
    3. Leverage this prop in a component
      const MyComp = ({ css }: { css: CSSProp }) => {
          const className = Config.css(css);
          return <p className={className()}>Hello, World!</p>;
      }
      
    4. Attempt to pass the css prop to MyComp, using theme tokens
      <MyComp
          css={{
              color: '$...',
          }}
      />
      

    Expected behavior

    • Typing $ provides a list of valid tokens
    • Providing a token works, and renders the appropriate CSS value

    Actual behavior

    • Typing $ provides no suggestions
    • Providing the token manually works, and renders the appropriate CSS value

    Repro

    Code Sandbox: https://codesandbox.io/s/polished-glitter-j5decj?file=/src/App.tsx

  • 15

    Update deprecated CSSRule.type property.

    The CSSRule.type property is deprecated. The recommended alternative is to reference the constructor.name property.

    This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Avoid using it, and update existing code if possible..

    See CSSRule.type - Web APIs | MDN


    Btw, this is my 1st ever contribution to an open source project. So, I apologize if I did something wrong!

  • 16

    Support for CSS layers

    One of the problems with CSS-in-JS frameworks generally is that it's hard to control the cascade order. This often results in styles overriding each other in undesirable ways, for example your 'hover' style taking a higher precedence than your 'disabled' style. There are various workarounds, such as adding extra && to up the specificity, but these are just hacks.

    The solution is to support CSS layers. The way I envision it, the list of layers would be passed into the createStitches() constructor. It would take a list of names in camel case, for example:

    createStitches({
      layers: ['uiBase', 'uiVariants', 'uiStates'],
      ... etc.
    )}
    

    Once defined, the layers can then be used in any call to css():

    const myCss = css({
      uiBase: {
        borderColor: 'red',
      },
    
      uiStates: {
        '&:hover': {
          borderColor: 'blue',
        }
      }
    

    The CSS properties within the layers can use any of the syntax that is normally allowed in the css() call. From an implementation standpoint, this means that they layer names are merged in with the CSS argument keys just like variants and such. It is the responsibility of the caller to ensure that layer names don't conflict with any CSS property names.

    CSS properties not wrapped in layers will work just as they do today. Because of the way CSS layers work, classes not wrapped in a layer have a higher precedence than any explicitly-named layer, which is what we want.

    When the stylesheet is generated, layers would have the following effects:

    • The layer names would be converted to kebab-case.
    • The stylesheet would include a definition of layer order:
    @layer ui-base, ui-variants, ui-states;
    
    • Class definitions would be output multiple times, once for the default layer (if there are any non-layered props) and once for each layer mentioned. All layers would be merged so that the @layer name would only appear once in the output.
    .xx-1234 {
      border-color: blue;
    }
    
    .xx-4567 {
      border-color: red;
    }
    
    @layer ui-base {
      .xx-1234 {
        background-color: #ff0000;
      }
    
      .xx-4567 {
        background-color: #ff0000;
      }
    }
    
    @layer ui-states {
      .xx-1234:hover {
        filter: brightness(1.1);
      }
    
      .xx-4567[disabled] {
        opacity: 0.5;
      }
    }
    

    Note: while it is possible to use layers currently by simply treating them as media queries, it is not very efficient, because the @layer definition is treated like a selector expression, and is output separately for each individual CSS class that uses a layer - in other words, the layers are not merged, so the @layer is repeated many times.