Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The journey of a new interface

Jepser
December 05, 2018

The journey of a new interface

One year ago, Typeform released the first conversational article, a chat-like interface that was embedded into an article giving contextual information while collecting data in a seamless way.

In April we launched the project as a product, a React powered interface. How an embeddable app is made, how did we manage a heavy animated interface, how did we make it feel natural, which tools we used and why?

Let’s take a look at the journey of a new interface.

Jepser

December 05, 2018
Tweet

Other Decks in Programming

Transcript

  1. Once upon a time — a new app was created.

    This was the start of CUI.
  2. $ — an easy to embed app based on Typeform’s

    API should feel part of the content an MVP that needs to feel polished
  3. $ — should to be able to interact with sibling

    embeds must handle orchestrated animations must be customizable should be extendable
  4. • Embed script that adds the app script • App

    script that loads the application • the application mounts an iframe into a DOM element • the iframe contains the app, styles and communication are managed by the app script • React portals
  5. • Embed script mounts an iframe in a DOM element

    • the iframe is the app, which contains 
 the styles and app logic
  6. Receiver - actions const actions = { [CUI_RESIZE_FRAME]: ({ frame,

    height }) => { frame.height = height }, [CUI_INITIALIZED]: ({ frame }) => { frame.parentNode.classList.remove(CUI_IS_LOADING) } /* ... */ }
  7. app events const actions = { /* ... */, [CUI_EVENT]:

    ({ frame, name, detail }) => { const event = createEvent(name, detail) frame.parentNode.dispatchEvent(event) } } export const createEvent = (name, detail = {}) => { const event = new CustomEvent(name, { detail }) return event }
  8. import posed from 'react-pose' const AnimatedComponent = posed.div({ enter: {},

    exit: {} })) export default ({ condition }) => ( <AnimatedComponent pose={condition ? 'enter' : ‘exit'} /> )
  9. import posed from 'react-pose' const AnimatedComponent = posed.div({ enter: {},

    exit: {} })) const StyledAndAnimatedComponent = styled(AnimatedComponent)` /* some styles */ ` export default ({ condition }) => ( <StyledAndAnimatedComponent pose={condition ? 'enter' : ‘exit'} /> )
  10. import { Motion } from ‘react-motion' <Motion defaultStyle={{x: 0}} style={{x:

    spring(10)}}> {interpolatingStyle => <div style={interpolatingStyle} />} </Motion>
  11. import { Spring } from ‘react-spring’ <Spring from={{ opacity: 0

    }} to={{ opacity: 1 }}> {props => <div style={props}>hello</div>} </Spring>
  12. const AnimatedComponent = posed.div({ enter: { opacity: 1, y: 0,

    transition: { y: { type: ‘spring', stiffness: 100 } } }, exit: { opacity: 0, y: 24 } })
  13. const AnimatedComponent = posed.div({ enter: { opacity: 1, y: 0,

    transition: { y: { type: ‘spring', stiffness: 100 } }, delay: ({ i }) => { return i * 300; } }, exit: { opacity: 0, y: 24 } })
  14. import { StaggeredMotion } from ‘react-motion' <StaggeredMotion defaultStyles={[{o: 0}, {o:

    0}]} styles={prevInterpolatedStyles => prevInterpolatedStyles.map((_, i) => { return i === 0 ? {o: spring(100)} : {o: spring(prevInterpolatedStyles[i - 1].o)} })}> {interpolatingStyles => <div> {interpolatingStyles.map((style, i) => <div key={i} style={{opacity: style.o}} />) } </div> } </StaggeredMotion>
  15. import { Trail } from ‘react-spring’ const items = [...]

    <Trail items={items} keys={item => item.key} from={{ transform: ‘translate3d(0,24px,0)’, opacity: 0 }} to={{ transform: ‘translate3d(0,0px,0)’, opacity: 1 }}> {item => props => <span style={props}>{item.text}</span> } </Trail>
  16. const AnimatedParent = posed.div({ enter: { x: 0 }, exit:

    { x: -24 } }) const AnimatedChild = posed.div({ enter: { opacity: 1, y: 0, }, exit: { opacity: 0, y: 24 } }) <AnimatedParent posed={condition ? 'enter' : 'exit'} > <AnimatedChild>text 1</AnimatedChild> <AnimatedChild>text 2</AnimatedChild> </AnimatedParent>
  17. const AnimatedParent = posed.div({ enter: { x: 0, staggerChildren: 300,

    delayChildren: 300 }, exit: { x: -24 } }) const AnimatedChild = posed.div({ enter: { opacity: 1, y: 0, }, exit: { opacity: 0, y: 24 } }) <AnimatedParent posed={condition ? 'enter' : 'exit'} > <AnimatedChild>text 1</AnimatedChild> <AnimatedChild>text 2</AnimatedChild> </AnimatedParent>
  18. import { PoseGroup } from 'react-pose' const items = [...]

    <PoseGroup onRest={() => { /* */}} > {items.map(item => ( <Child key={item} onPoseComplete={() => { /* */ }} > {item} </Child> ))} </PoseGroup>
  19. const MessageWrap = posed.div({ enter: { opacity: 1, y: 0,

    maxHeight: 200, transition: props => tween({ ...props, duration: 320, ease: props.key === 'opacity' ? easing.linear : easing.backOut }) }, exit: { delay: 800, opacity: 0, y: 24, maxHeight: 0 }, expand: { maxHeight: '1000px' }, })