styled-components / styled-components

Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress 💅
https://styled-components.com
MIT License
40.45k stars 2.5k forks source link

Accessing props/theme in keyframes #397

Closed jaydenseric closed 7 years ago

jaydenseric commented 7 years ago

It was really hard to work out a way to access props in animation keyframes to make animations themeable. Eventually I came across this comment, which lead me to this approach:

import React from 'react'
import styled, {keyframes} from 'styled-components'

function animation (props) {
  return keyframes`
    to {
      color: ${props.theme.error};
    }
  `
}

// Contrived blinking error message example
export default styled.p`
  animation: ${props => animation} 0.5s infinite;
`

There must be a more elegant way. Whatever it is, lets add it to the keyframes and/or theming docs.

mxstbr commented 7 years ago

Interesting. I'm not sure it's possible to pass the theme to keyframes, as it's propagated through React context to components. (which keyframes isn't)

Maybe @geelen has an idea how we could achieve this?

k15a commented 7 years ago

If you change a little implementation detail this should work.

// Contrived blinking error message example
export default styled.p`
  animation: ${animation} 0.5s infinite;
`
mxstbr commented 7 years ago

This doesn't work I don't think @k15a, though we could make that work just like css maybe?

k15a commented 7 years ago

Why not? animation is a reference to a function and functions get called with props as the first argument?

Do I miss something?

k15a commented 7 years ago

Example: http://www.webpackbin.com/4ydwqilDG

mxstbr commented 7 years ago

Oh yeah, that's true!

What I was talking about is that we should pass props to functions in keyframes too, just like with css:

const rotate = keyframes`
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(${props => props.rotation || '360deg'});
  }
`

const RotatingBox = styled.div`
  animation: ${rotate} 10s linear infinite;
`

// Usage:

<RotatingBox rotation="180deg" />
k15a commented 7 years ago

Yeah that would be a bit more convenient.

diegohaz commented 7 years ago

I think we should support this for consistency.

techniq commented 7 years ago

I think I'm running into this as well trying to port one of my CSS loaders to use styled-components. I tried using @k15a's workaround and making my dash keyframes a function like so:

function dash(props) {
  return keyframes`
    0% {
      stroke-dasharray: ${getCircumference(props.radius)};
      stroke-dashoffset: ${getCircumference(props.radius) * 0.97};
      transform: rotate(-130deg);
    }
    100% {
      stroke-dasharray: ${getCircumference(props.radius)};
      stroke-dashoffset: ${getCircumference(props.radius) * 0.5};
      transform: rotate(-110deg);
    }
  `;
}

but it doesn't seem to have any affect.

geelen commented 7 years ago

Hmm, I kinda like the fact that keyframes just returns a string. It seems weird if props gets magically passed to it, when using it inside a function is so easy: http://www.webpackbin.com/V15uwsQwG

This is more consistent because keyframes, like injectGlobal has one and only one immediate side-effect—to inject some CSS into the page. styled.div returns a component that has a complex life cycle, including rerendering, so I don't really want to blur the lines between component-oriented and global CSS injection methods.

techniq commented 7 years ago

@geelen In my case I tried to use the function workaround for my keyframes but it didn't work - see this example.

This might be because I'm also deriving some of the other animation props (ex. duration) so animation is using a function as well.:

const Spinner = styled.circle`
  stroke: ${props => props.color};
  stroke-width: ${props => props.width};
  stroke-linecap: round;
  fill: none;
  transform-origin: 50%;
  animation: ${props => `
    ${dash} ${props.duration} ease-in-out infinite alternate,
    ${rotate} ${props.duration} linear infinite
  `}
`;

As I wrote this I realized "maybe I need to pass the props explicitly to dash now that it is within a function / template literal" like so...

const Spinner = styled.circle`
  stroke: ${props => props.color};
  stroke-width: ${props => props.width};
  stroke-linecap: round;
  fill: none;
  transform-origin: 50%;
  animation: ${props => `
    ${dash(props)} ${props.duration} ease-in-out infinite alternate,
    ${rotate} ${props.duration} linear infinite
  `}
`;

and it worked.


I guess (at least for me) it was unclear when props are passed to a function and when they not.

Passed "automatically"

function animation (props) {
  return keyframes`
    to {
      color: ${props.error};
    }
  `
}

const Text = styled.p`
  color: #222;
  font-size: 1.5em;
  font-family: Helvetica, Arial, sans-serif;
  text-align: center;
  animation: ${animation} 0.5s infinite;
`;

Must be passed explicitly

function animation (props) {
  return keyframes`
    to {
      color: ${props.error};
    }
  `
}

const Text = styled.p`
  color: #222;
  font-size: 1.5em;
  font-family: Helvetica, Arial, sans-serif;
  text-align: center;
  animation: ${props => `${animation(props)} 0.5s infinite`};
`;
techniq commented 7 years ago

I see arrow functions work as well, which basically means you just need to inject (props) => between your variable name and keyframes

BEFORE

const RADIUS = 50;
const dash = keyframes`
  0% {
    stroke-dasharray: ${getCircumference(RADIUS)};
    stroke-dashoffset: ${getCircumference(RADIUS) * 0.97};
    transform: rotate(-130deg);
  }
  100% {
    stroke-dasharray: ${getCircumference(RADIUS)};
    stroke-dashoffset: ${getCircumference(RADIUS) * 0.5};
    transform: rotate(-110deg);
  }
`;

AFTER

const dash = (props) => keyframes`
  0% {
    stroke-dasharray: ${getCircumference(props.radius)};
    stroke-dashoffset: ${getCircumference(props.radius) * 0.97};
    transform: rotate(-130deg);
  }
  100% {
    stroke-dasharray: ${getCircumference(RADIUS)};
    stroke-dashoffset: ${getCircumference(RADIUS) * 0.5};
    transform: rotate(-110deg);
  }
`;
geelen commented 7 years ago

Yeah exactly. This is one of the confusing things about tagged template literals. In your previous example:

const Spinner = styled.circle`
  ...
  animation: ${props => `
    ${dash} ${props.duration} ease-in-out infinite alternate,
    ${rotate} ${props.duration} linear infinite
  `}
`;

You're actually using a normal (untagged) template literal. If you used css, it'd work.

const Spinner = styled.circle`
  ...
  animation: ${props => css`
    ${dash} ${props.duration} ease-in-out infinite alternate,
    ${rotate} ${props.duration} linear infinite
  `}
`;

If you leave it off it's effectively calling toString on everything you pass in, including dash which will return function dash() { keyframes(" ... ", ...) } or whatever babel compiled it to. Using css stops those functions from being toString-ed, and instead calls them with props at render time. See the docs here: https://github.com/styled-components/styled-components/blob/master/docs/tagged-template-literals.md

techniq commented 7 years ago

Awe, thanks for the clarification. I remember reading about that early on, but until I encountered it, it didn't really sink in.

On Thu, Jan 26, 2017, 11:54 PM Glen Maddern notifications@github.com wrote:

Yeah exactly. This is one of the confusing things about tagged template literals. In your previous example:

const Spinner = styled.circle... animation: ${props => ${dash} ${props.duration} ease-in-out infinite alternate, ${rotate} ${props.duration} linear infinite };

You're actually using a normal (untagged) template literal. If you used css, it'd work.

const Spinner = styled.circle... animation: ${props => css ${dash} ${props.duration} ease-in-out infinite alternate, ${rotate} ${props.duration} linear infinite };

If you leave it off it's effectively calling toString on everything you pass in, including dash which will return function dash() { keyframes(" ... ", ...) } or whatever babel compiled it to. Using css stops those functions from being toString-ed, and instead calls them with props at render time. See the docs here: https://github.com/styled-components/styled-components/blob/master/docs/tagged-template-literals.md

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/styled-components/styled-components/issues/397#issuecomment-275588876, or mute the thread https://github.com/notifications/unsubscribe-auth/AAK1RMr8M_pp2ncYkqPIN715lWGIwEw8ks5rWXiFgaJpZM4LqURs .

quanganh1202 commented 7 years ago

`import styled, { keyframes } from 'styled-components';

const move = keyframes 0% {top: 0px; left: 0px; background: ${(props) => props.color};} 25% {top: 0px; left: 100px; background: blue;} ;

const AnimationDiv = styled.div width: 100px; height: 100px; border: 1px solid ${(props) => props.color}; &:hover { animation: ${move} 5s infinite; } ;

export default AnimationDiv;` I'm using this, but the coverage can't pass through the line ${(props) => props.color} in keyframes Anyone can help?

kitten commented 7 years ago

@quanganh1202 first of all, it's always better to turn to spectrum, stackoverflow, or a new issue, instead of commenting on an old and closed one :wink:

keyframes are created separately since they're a global. We generate a hash for their name and return it as a result of calling the tag. This means that they cannot be used like an interpolation, since they just return a static name.

If you write p => keyframes as an interpolation instead and use it as a factory, it will work.

It's even mentioned on a comment before: https://github.com/styled-components/styled-components/issues/397#issuecomment-275581243