2.0.0-prerelease.5 • Published 4 years ago

tsstyled v2.0.0-prerelease.5

Weekly downloads
-
License
ISC
Repository
-
Last release
4 years ago

homepage bundle size github stars npm version build status coverage status

A lean CSS-in-JS solution for React with first-class TypeScript support.

  • Fast: Faster than styled-components (benchmarks included).
  • Tiny: Less than 4kb (minified and gzipped) and no dependencies.
  • Simple: A minimal and intuitive API.
  • Typed: Written in TypeScript with a focus on type safety and clarity.

Table of contents


Getting Started

Installation

Install the tsstyled package and its react peer dependency.

# With NPM
npm add tsstyled react

# With Yarn
yarn add tsstyled react

This library uses semantic versioning. Breaking changes will only be introduced in major version updates.

Compatibility

  • React >= 16.14.0
  • IE >= 11

Community

Do you have questions, suggestions, or issues? Join the Discord server!

Motivation

I wanted a small CSS-in-JS solution for React, with zero dependencies, and strong types. I love the styled-components pattern, but Styled Components itself is too big and it's typings are not the best. Emotion is smaller and generally better, but still not ideal. Then I found Goober which nearly convincing me not to write this library. If you're looking for a good solution, check it out too.

I still wrote this because it was just a fun side project, and because there were some different design choices I wanted to make. I've included a comparison of what I consider to be the key features of any CSS-in-JS library.

Compared with other libraries

  • ✅ Supported
  • 🟡 Partially supported
  • 🛑 Not supported
  • ❓ Not documented
Library FeatureTSStyledGooberStyled ComponentsEmotion
Bundle size (minify+gzip kB)✅ (3.2)✅ (1.3)🛑 (12.7)🟡 (4.8)
Zero dependencies🛑🛑
TypeScript native🛑
API FeatureTSStyledGooberStyled ComponentsEmotion
Styled component pattern
Dynamic styles
Tagged template styles
Object styles🛑
Global styles
Polymorphism (as)🛑
Property mapping (attrs)🛑🛑🛑
Style mixins 1🟡
Theming 2🟡🟡
SSR
Non-global config🛑
Style FeatureTSStyledGooberStyled ComponentsEmotion
CSS @media
CSS @keyframes
CSS @font-face
CSS @import🛑
Other CSS @ rules
Vendor prefixing 3🛑🟡
Rule nesting
Parent selectors (&)
Styled component selectors
  • 1 Goober doesn't provide a css utility for creating mixins, but it does support function values in tagged templates.
  • 2 Styled Components and Emotion support only a single theme, which must be typed using declaration merging.
  • 3 Goober allows a prefix callback function to be configured, but does not provide automatic vendor prefixing.

TSStyled omits three key features supported by other libraries: Polymorphism using the as property, vendor prefixing, and object styles.

The automatic inclusion of the as property with support for any element or component, is inherently type unsafe. Full (non-styled) components can implement an as property and limited polymorphism safely, because they can limit the range of polymorphism. Strong typing is a key TSStyled goal, so this feature just doesn't fit.

Vendor prefixing isn't as necessary as it used to be. There are still a some uncommon cases where it might be needed, but prefixes can always be manually included. Style helpers are also a pretty good fit for this. The overhead and maintenance required to implement this didn't seem worth it.

Omitting object styling is a purely stylistic choice. In my opinion, tagged templates provide a better experience in the following ways...

  • Cutting and pasting styles is simpler with tagged templates.
  • Learning tagged templates is easier because it's closer to vanilla CSS.
  • Intellisense and syntax checking are (arguably) better with tagged templates.
  • Defining multiple CSS property "fallback" values is cleaner with tagged templates.
  • Property ordering is guaranteed with tagged templates, whereas object property ordering is not part of the JS specification (especially when merging two objects).

Benchmarks

You can run benchmarks by cloning the TSStyled repository, and running the npm start command. The benchmark application will be served at https://localhost:3000.

The Basics

First, create the styled API.

import { createStyled } from 'tsstyled';

const styled = createStyled();

Styling HTML elements and components

Style any HTML element type by using the tag name. The styled component supports all of the same props (included refs, which are forwarded) that the HTML element supports.

const StyledDiv = styled('div')`
  color: black;
`;

Style any React component which accepts a className string property.

const StyledComponent = styled(Component)`
  color: black;
`;

Extend the styling of an already styled component.

const ReStyledComponent = styled(StyledComponent)`
  color: gray;
`;

Extending the properties type

The tagged template function returned by styled is generic. Any type passed to this the type parameter will extend (not replace) the styled component's properties.

const StyledDiv = styled('div')<{ $font?: string }>`
  font-family: ${(props) => props.$font};
`;

Note: Any property name which starts with the $ character will not be passed through to the underlying HTML element. So, the above div will not have a $font attribute when rendered.

Setting default property values

React has the defaultProps static property which can be used with styled components. However, there are some drawbacks...

  • It is deprecated for function components as part of an initiative to update React's createElement function.
  • It is applied before propTypes which can lead to default values throwing errors.

TSStyled provides an alternative custom implementation called propDefaults. This cannot be deprecated by React, and it is applied after propTypes.

const StyledButton = styled('button')``;

StyledButton.propDefaults = {
  type: 'submit',
};

Creating global styles

Use the styled.global utility to create global style components.

const GlobalStyle = styled.global`
  body, html {
    margin: 0;
    padding: 0;
  }
`;

Extend the component properties type using the tagged template generic parameter.

const GlobalStyle = styled.global<{ $font?: string }>`
  body, html {
    font-family: ${(props) => props.$font};
  }
`;

Defining keyframes and fonts

Defining keyframes or font-faces is the same as defining any other style. Since they are not scoped to any particular component, they should probably only be used in global styles. To prevent name collisions, use the getId utility to generate unique names.

const openSansFont = getId('Open Sans');
const slideInAnimation = getId('slideIn');

const GlobalStyle = styled.global`
  @font-face {
    font-family: ${openSansFont};
    src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"),
         url("/fonts/OpenSans-Regular-webfont.woff") format("woff");
  }

  @keyframes ${slideInAnimation} {
    from {
      transform: translateX(0%);
    }

    to {
      transform: translateX(100%);
    }
  }
`;

const StyledDiv = styled('div')`
  font-family: ${openSansFont};
  animation-name: ${slideInAnimation};
`;

Theming

A theme creation utility is provided instead of a single built-in theme. This makes themes strongly typed. It also loosely couples the theme implementation, so that third party theming is supported.

Creating a theme

Themes are just values made available via a React context, and preferably using a React hook. The createTheme utility is provided to make that a one step process. It accepts the default theme value, and returns a theme hook function and provider component.

const [useTheme, ThemeProvider] = createTheme({
  fgColor: 'black';
  bgColor: 'white';
});

Using a theme

Pass the useTheme hook to createStyled when creating the styled API. The returned styled API will now expose a props.theme in styled template callbacks.

const styled = createStyled(useTheme);

const ThemedDiv = styled('div')`
  color: ${(props) => props.theme.fgColor};
  background-color: ${(props) => props.theme.bgColor};
`;

Note: Any function that returns a theme can be used, not just the hook returned by createTheme.

Overriding theme values

The provider returned by the createTheme utility allows theme values to be (all or partially) overridden. The following example inverts the fore and background colors.

<ThemeProvider
  value={(current) => ({
    fgColor: current.bgColor,
    bgColor: current.fgColor,
  })}
>
  <ThemedDiv />
</ThemeProvider>

Style syntax

Style syntax is CSS-like, and all CSS properties, selectors, and at-rules are supported. In addition, SCSS-like nesting is supported with parent selector references (&).

Styling self

To apply styles directly to the HTML element or component being styled, use CSS properties at the top-level of the tagged template (no surrounding block).

const StyledDiv = styled('div')`
  color: red;
`;

Top-level CSS properties will be wrapped in a dynamic styled class selector

._s7y13d {
  color: red;
}

Styling children

Use CSS rule blocks to style children of the styled component.

const StyledDiv = styled('div')`
  .child {
    color: blue;
  }
`

The styled dynamic class will be automatically prepended to all selectors to make them "scoped".

._s7y13d .child {
  color: blue;
}

Styling other styled components

Every styled component (not global styles) has a static selector property which contains a unique class selector string that can be used to style that specific styled component.

const StyledDiv = styled('div')`
  ${StyledOther.selector} {
    color: red;
  }
`;

The selector value is a simple string (eg. ".tss_s7y13d"), so the tagged template interpolates it like any other string value, and it behaves just like the literal .child selector in the prevous example.

._s7y13d .tss_s7y13d {
  color: red;
}

Note: This is slightly different than other styled-components libraries which will usually let you use the component itself as the template value (eg. ${StyledOther} instead of ${StyledOther.selector}). The static selector property improves type safety, clarity, and makes the selector string publicly accessible.

Nesting rules

Nest rule blocks to create more complex selectors.

const StyledDiv = styled('div')`
  .child {
    color: blue;
    .grandchild {
      color: green;
    }
  }
`;

Just like the styled dynamic class is prepended to top-level selectors, so too are parent selectors prepended to child selectors.

._s7y13d .child {
  color: blue;
}
._s7y13d .child .grandchild {
  color: green;
}

Using parent selector references

As noted above, the parent selector is automatically prepended to child selectors. This behavior can be overridden by using a parent selector reference (&) to inject the parent selector anywhere in the child selector. This includes injecting the parent selector multiple times to increase specificity, and is necessary when applying pseudo selectors (eg. :hover) directly to the styled component and not to a child element.

const StyledDiv = styled('div')`
  && {
    color: red;
  }
  &:hover {
    color: blue;
  }
  .parent & {
    color: green;
  }
`

For any selector that contains &, the parent selector will replace the & character, and the parent selector will not be automatically prepended.

._s7y13d._s7y13d {
  color: red;
}
._s7y13d:hover {
  color: blue;
}
.parent ._s7y13d {
  color: green;
}

Using at-rules

All CSS at-rules are supported (except @charset which isn't allowed in <style> elements).

const StyledDiv = styled('div')`
  @media screen and (min-width: 900px) {
    color: red
  }
  .child {
    @media screen and (min-width: 600px) {
      .grandchild {
        color: blue;
        .adopted & {
          color: green;
        }
      }
    }
  }
`;

At-rules will be hoisted as necessary, and parent selectors will be handled the same way they would be without the intervening at-rule.

@media screen and (min-width: 900px) {
  ._s7y13d {
    color: red;
  }
}
@media screen and (min-width: 600px) {
  ._s7y13d .child .grandchild {
    color: blue;
  }
  .adopted ._s7y13d .child .grandchild {
    color: green;
  }
}

Using empty values

If a CSS property value is an empty string or null-ish (null or undefined), then the whole property will be omitted from the style.

const StyledDiv = styled('div')`
  color: ${null};
  background-color: red;
`;

The color property is not included because it has no value.

._s7y13d {
  background-color: red;
}

Commenting

Styles can contain both block (/* */) and line comments (//). Comments are never included in rendered stylesheets.

const StyledDiv = styled('div')`
  // This is a comment.
  /* And so...
     ...is this. */
`;

Style mixins (helpers)

The css tagged template utility returns a mixin (AKA: helper) function. When the returned function is called, it returns a style string with all values interpolated.

Creating simple mixins

Mixins do not accept any parameters by default.

const font = css`
  font-family: Arial, sans-serif;
  font-weight: 400;
  font-size: 1rem;
`;

const StyledDiv = styled('div')`
  color: red;
  ${font}
`;

Note: The above font constant is a function which accepts no arguments (ie. () => string).

Creating parametric mixins

Helpers which accept parameters can be created by setting the generic type of the css tagged template utility.

const font = css<{ scale?: number }>`
  font-family: Arial, sans-serif;
  font-weight: 400;
  font-size: ${(props) => props.scale || 1}rem;
`;

const StyledDiv = styled('div')`
  ${font({ scale: 2 })}
  color: red;
`;

Server Side Rendering (SSR)

During SSR, there is no DOM and therefore no document global. TSStyled detects this and uses a very minimal "virtual" DOM behind the scenes. So, after rendering the document body, use the renderStylesToString utility to get all of the <style> elements (generated by TSStyled components) as a string.

const appHtml = renderToString(<App />);
const stylesHtml = renderStylesToString();
const html = `
<!doctype HTML>
<html>
<head>
  ${stylesHtml}
</head>
<body>
  <div id="root">${appHtml}</div>
</body>
</html>

Testing

During testing, there may be a DOM (eg. jsdom) and a document global. However, the NODE_ENV environment variable should also be set to test (Jest sets this automatically). If it is, the SSR implementation is used. So, use the same renderStylesToString utility used for SSR style rendering to check (eg. Jest snapshot test) your styles.

expect(renderStylesToString()).toMatchSnapshot();
3.0.10

3 years ago

3.0.8

3 years ago

3.0.7

3 years ago

3.0.9

3 years ago

3.0.4

4 years ago

3.0.3

4 years ago

3.0.2

4 years ago

3.0.1

4 years ago

3.0.6

4 years ago

3.0.5

4 years ago

3.0.0

4 years ago

2.1.1

4 years ago

2.1.0

4 years ago

2.0.1

4 years ago

2.0.0

4 years ago

1.2.8

4 years ago

1.2.0

4 years ago

1.2.7

4 years ago

1.2.6

4 years ago

1.2.5

4 years ago

1.2.4

4 years ago

1.2.3

4 years ago

1.2.2

4 years ago

1.2.1

4 years ago

1.1.1

4 years ago

1.1.4

4 years ago

1.1.3

4 years ago

1.1.2

4 years ago

1.1.0

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago