question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Describe the feature

CSS in JS libraries are maintaining two seperate APIs, namely styled and css. There are good reasons for both, but in react projects, it is an unnecessary complication. I wish to show that with Linaria’s approach it is possible to get the best of both (composition, selection, prop interpolation, and automated css variable interpolations)!

Problem

Having a single consistant and powerful API is important. Using both together creates some edge cases. Exclusively using the current styled API can cause certain components (those that use Hooks) to not render and you quickly end up creating many components with deep nesting. An example of this is an input with a “clear” button. It must be wrapped in a div to contain and position the button but a styled component would cause the ref to refer to the parent div rather than the input as expected.

For this reason and others, Styled Components are also not currently suitable for building more complex components such as those found in UI libraries. It can be done, but it results in deep nesting. Because Linaria’s styled runtime size is near zero, Linaria is in a unique position to unify these two approaches.

Proposal

Step 1

The first step would be to support a render prop on the styled component, this would solve many issues with building reusable components:

  • The exported component can be used as a child selector for overriding styles.
  • refs could be passed down without forgoing the benefits that the styled function provides
  • default props (e.g. input type=“password”, button type=“button”) can be set easily. Styled Components has a seperate attr() function, but simply supplying a custom render function is a more powerful solution.
  • Components with hooks can render themselves (fixing that issue) if provided as a render prop.
  • shallower render trees. Exported components can include the styles required to function. No more of this:
// Styled(PasswordWrapper) This is done so that the exported module can be selected in a parent styled tag
<Input.Password>
    <PasswordWrapper> {/* Root customized component */}
          <ComponentRoot className="InputWrap_i17nyggi">
               <div className="InputWrap_i17nyggi">
                  <input ... />
                  <span onClick={}>Show</span>
               </div>
          </ComponentRoot>
     </PasswordWrapper>
</Input.Password>

This is a simple example, but it can get worse, e.g. the component consumer then wraps the component to customize the styles, or more children use styled. It was worse with emotion, where everything with a css prop was automatically wrapped in multiple Context.Consumers.

The styled function provides a lot of utility and adding a render prop would eliminate a case where css must be used instead.

Step 2

Introduce the styled.fragment. The styled API would also change slightly so that it can be used as a replacement for the css function in every case. The babel plugin could optionally would rewrite styled to css where appropriate though this is not required. This solves the composition issue with styled.

Fragments support interpolation and cannot be used alone if they contain references to component props. When called, they return their contents. This is unlike css, which returns a className. They do generate unique classnames. This allows elements that compose this element to be selected. To access the fragment’s generated classname directly, call its className property.

const shared = styled.fragment`
  background: beige;
`

const Typography = styled.fragment`
  color: black;
  line-height: 1.5;
  margin: 6px;
  /* We can interpolate here because CSS vars are always applied to */
  /* the element that composes this style */
  font-size: ${props => props.size}px;
`

// Styled fragments only return class name and styles prop. Which can compose
// Then we can compose like this:
const Article = styled.article`
  p {
    ${Typography}
  }
`

const Footer = styled.footer`
  ${Typography}
  font-size: 0.8em; /* Override what is specified in Typography */
`

const Header = styled.header`
  ${shared}
  color: blue;
`

const Card = styled.div`
  background: beige;
  padding: 2em;
  border-color: black;

  /* Selecting child elements with the shared class name applied */
  & .${shared.className} {
    /* Just like any JSX styled element this styled.fragment has a className */
    color: pink;
  }
`

Generated output:

.Article.Typography_fragment p,
.Footer.Typography_fragment {
  color: black;
  line-height: 1.5;
  margin: 6px;
  font-size: var(--Typography_fragment_fontSize-var);
}

/* Because we are nesting and expect an override, we have to match the specificity */
.Footer.Typography_fragment {
  /* The cool thing about variables here is that we are not using it here, even though it is set on footer.style */
  /* It may also be possible to statically determine that the var should not be applied to <Footer /> */
  font-size: 0.8em;
}

.Header {
  color: blue;
}

/* a demonstration of atomization */
.shared_fragment {
  background: beige;
}

.Card {
  padding: 2em;
  border-color: black;
  /* background style removed as it is shared (as determined by compiler) */
}

/* Our override */
.Card .shared_fragment {
   color: pink;
}

Generated markup:

<article
  class="Article Typography_fragment"
  style="--Typography_fragment_fontSize-var: '16px';"
>
  <p>lorem</p>
  <p>ipsum</p>
</article>

<header class="Header shared_fragment">Welcome to the site!</header>

<footer
  class="Footer Typography_fragment"
  style="--Typography_fragment_fontSize-var: '16px';"
>
  Copyright
</footer>

<div class="Card shared_fragment">Shuffle</div>

If styled is called without an argument, fragment could be assumed. But doing so means no css syntax highlighting currently.

<button
  className={cx(
    styled()`
      color: black;
      border: red;
    `,
    'btn'
  )}
>
  Click
</button>

Step 3

Proposed syntax where styled accepts a function as its render prop:

  1. receives props and ref React.forwardRef
  2. Styled component adds classname and styles to filteredProps
  3. Styled component sets displayName and __linaria object on Root
  4. Styled helper acccepts render function (tag, filteredProps)
export const Input = styled.input`
  margin: 0;
  width: 100%;
  height: 32px;
  padding: 4px 11px;
  color: rgba(0, 0, 0, 0.65);
  font-size: 14px;
  line-height: 1.5;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
  transition: all 0.3s;
`((filteredProps, ref, tag) => {
  // I have moved tag to the end here so it can be ignored.
  const { styles, className } = filteredProps
  // Use hooks here etc...
  return (
    <div {...className} {...styles}>
      <input ref={ref} {...filteredProps} />
    </div>
  )
})

Fragments without prop interpolations are allowed as a css replacement as such, this could be done:

<article
  className={cx(
    styled.fragment`
      color: red;
    `,
    importedFragment
  )}
>
  Composed Styles!
</article>

Related Issues

This should fix: https://github.com/callstack/linaria/issues/244 and https://github.com/callstack/linaria/issues/234 plus https://github.com/callstack/linaria/issues/418.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:7
  • Comments:8 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
brandonkalcommented, May 30, 2019

I’ve been thinking about this a lot, so I’d be interested in hearing thoughts. It is clear to me that using a classname only approach for styling descendent selectors does introduce complexity though that is a risk a developer takes when using descendant selectors.

I do believe using classnames is the right approach. The styled component is a nice abstraction as you don’t have to think about classnames but with the exception of the styles for variables, they really do the same thing:

const Button = styled.button`
    color: red;
`
<Button />
<button className={css`
    color: red;
`} />

This is why I am not a fan of this:

const ButtonGroup = styled.div`
  ${Button} {
    margin: 0;
  }
  /* vs */
  .${Button} {
    margin: 0;
  }
`

Two different ways to select an applied className. You have to look at the implementation of Button to determine if a leading dot should be used. Both methods do the same thing from the application developer’s perspective. It appears the reason for the difference is that the first came from styled components which chose the first. The second came from emotion because emotion’s css function used to return a classname.

Pros for using styled:

  • Define inline and it is still selectable. With the className approach, this is not possible.

Pros for using css:

  • Because css is compiled at build, there is no penalty to having it within a render function vs lifting its scope. This is quite convenient for keeping styles closer to what they modify.
  • Natural composition Cons of using css:
  • Things are done twice: CSS rules defined + rules for classname composition must be repeated. styled already has access to all component props, so this composition logic can be generated at build time.

Modifier Classes

Astroturf supports arbitrary modifier classes defined in the css function. This creates a problem when you wrap components: https://github.com/callstack/linaria/issues/234#issuecomment-426421254.

One solution is to use an arbitary prefix such as $. I don’t like this approach because then there is no easy way to also pass through these properties. Also, JSX may eventually support something like this: <Box ${modifier} /> which looks too similar. I would propose to instead determine if a prop should pass through where it is defined. After all, we can think of our classes as functions of state:

const Button = styled.button`
  color: black;
  border: 1px solid black;
  background-color: white;

  &[props|primary] {
    color: blue;
    border: 1px solid blue;
  }

  &[props|color=green--] {
    color: green;
  }
`;

<Button primary color="green">

I am using the props namespace here to make it clear that these are functions of props. It feels more like CSS, but the following could work with some work:

${props => (props.primary && generateClassName(&, css.fragment`
    color: blue;
    border: 1px solid blue;
`), true)}

where generateClassName is a function that takes a prefix and a css string.

Because rule blocks are a function of state, we can pass three arguments in the CSS:

  1. Condition
  2. passThrough
  3. The rule template string

So above, the first rule has the condition that props.primary is truthy. The -- suffix tells styled to not pass through the property. Props are passed through to custom components by default (unless the render function it is a styled DOM node where valid props are known). Simply include a -- to opt out of this for a specific prop.

This avoids something like this:

<MyStyledButton $primary primary />

When compiling, linaria would transform each rule block into its own const = Button_propsPrimary = css call. Perhaps something like this:

function css(prefix = "", suffix = "", stateCondition, passThrough = true)`rule` {}

const ButtonBase = css()`
     color: black;
      border: 1px solid black;
      background-color: white;
`

const Button_PropsPrimary = css(ButtonBase, "", (props) => !!props.primary, true)`
    color: blue;
    border: 1px solid blue;
`

const Button_PropsColorGreen = css(ButtonBase, "", (props) => props.color === "green", false)`
    color: green;
`

const ButtonClassName = cx(ButtonBase, Button_PropsPrimary, Button_PropsColorGreen)
<button className={ButtonClassName}>

Why

  • With this approach Styled handles the concatentation of classnames as a function of state. It feels weird repeating this logic manually.
  • Linaria can statically analyze the css text and determine which props to read.
  • This approach is similar to fela, but using CSS syntax.
  • Currently descendent selectors are not tree shakeable. Linaria would require complete knowledge of the DOM to safely tree shake unused styles. By splitting descendent selectors to their own variable, the bundler can instead handle these things.
  • We can use CSS features in cases where passing theme via context was used. i.e. you can define a dark theme in the leaf styled components. This is possible: .theme-dark & {color: white;} but the “theme-dark” className must be manually added rather than using component state.
  • Styled currently cannot react to props to conditionally apply rule blocks. It does a great job with individual rules (via CSS variables). Combined with this approach, styled could be used exclusively to get the best of both worlds.
  • More flexible prop names. The developer can still choose to prefix props that modify styles with a dollar sign, but this is not at all required. Instead forwarding is declared where the rest of styling logic is applied (inside the css).

We are currently doing everything twice.:

const buttonBase = css`
     color: black;
    background: white;
   padding: 5px;
`
const buttonPrimary = css`
    background: blue;
    color: white;
`
let buttonSize = {}
buttonSize.large = css`
    font-size: 3em;
`

function Button({ size, primary, ...props}) {
      const styles = cx([
          buttonBase,
          primary && buttonPrimary,
          size && buttonSize[size] && buttonSize[size]
     ])
     <button className={styles} {...props} />
}

This repetition is unnecessary. Let’s define all styles in CSS, including reacting to props or state!

0reactions
pitopscommented, Oct 16, 2021

any update?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Unifi Controller API - Where is it? | Ubiquiti Community
Hi,. Brand new to Unifi as a whole and looking to put together a web application that links to the Unifi controller set...
Read more >
products:software:unifi-controller:api [Ubiquiti Community Wiki]
Documentation of API endpoints on the UniFi controller software. This is a reverse engineering project that is based on browser captures, jar dumps, ......
Read more >
Art-of-WiFi/UniFi-API-browser: Tool to browse data ... - GitHub
This tool is for browsing data that is exposed through Ubiquiti's UniFi Controller API, written in PHP, JavaScript and the Bootstrap CSS framework....
Read more >
UNIFI API
UNIFI Labs facilitates common data environments for the Building Life Cycle. We journey with our customers and each other as teammates. All Customer...
Read more >
Unified APIs - Search and discover Unified APIs and simplify ...
Unified APIs aggregate cloud providers in specific software categories, offering a single endpoint to areas like banking, accounting, CRM, leads, calendar, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found