Shopify/polaris-tokens

[Color system] Move Sass variables to polaris-tokens

alex-page opened this issue Β· 19 comments

Context

For context on why we're doing this, see this issue: Shopify/polaris#1982

Why is this part of the Color system project?

Good question. This work unlocks us (starting in version 5) to make changes to our Sass API without an impact on consumers. It will force consumers to stop relying on Polaris for Sass, and to use Tokens and their own internal Sass instead. Basically it paves the road for consumers to start using the Color system as CSS Custom properties, instead of using the color function (which will be removed) provided by Polaris Sass.

Principles

  • tokens should not be aliases to other tokens
  • tokens should not do math for the consumer
  • tokens should return pixel values and be transformed on the consumer
  • tokens should convey only globally-useful data

⚠️ Names used in audit do not represent final names for tokens

Audit: Global data

Border radius

  • base: 3px
  • large: 6px

Border width

  • base: rem(1px)
  • thick: rem(2px)
  • thicker: rem(3px)

Border

  • base: border-width() solid color('sky')
  • dark: border-width() solid color('sky', 'dark')
  • transparent: border-width() solid transparent

Easing

  • base: cubic-bezier(0.64, 0, 0.35, 1)
  • in: cubic-bezier(0.36, 0, 1, 1)
  • out: cubic-bezier(0, 0, 0.42, 1)
  • excite: cubic-bezier(0.18, 0.67, 0.6, 1.22)
  • overshoot: cubic-bezier(0.07, 0.28, 0.32, 1.22)
  • anticipate: cubic-bezier(0.38, -0.4, 0.88, 0.65)

Shadows

  • faint: ( 0 1px 0 0 rgba(22, 29, 37, 0.05), )
  • base: ( 0 0 0 1px rgba(63, 63, 68, 0.05), 0 1px 3px 0 rgba(63, 63, 68, 0.15), )
  • deep: ( 0 0 0 1px rgba(6, 44, 82, 0.1), 0 2px 16px rgba(33, 43, 54, 0.08), )
  • layer: ( 0 31px 41px 0 rgba(32, 42, 53, 0.2), 0 2px 16px 0 rgba(32, 42, 54, 0.08), )
  • transparent: 0 0 0 0 transparent

Line-height

  • caption-base: rem(20px)
  • caption-large-screen: rem(16px)
  • heading-base: rem(24px)
  • subheading-base: rem(16px)
  • input-base: rem(24px)
  • body-base: rem(20px)
  • button-base: rem(16px)
  • button-large-base: rem(20px)
  • display-x-large-base: rem(36px)
  • display-x-large-large-screen: rem(44px)
  • display-large-base: rem(28px)
  • display-large-large-screen: rem(32px)
  • display-medium-base: rem(28px)
  • display-medium-large-screen: rem(32px)
  • display-small-base: rem(24px)
  • display-small-large-screen: rem(28px)

Font-size

  • caption-base: rem(13px)
  • caption-large-screen: rem(12px)
  • heading-base: rem(17px)
  • heading-large-screen: rem(16px)
  • subheading-base: rem(13px)
  • subheading-large-screen: rem(12px)
  • input-base: rem(16px)
  • input-large-screen: rem(14px)
  • body-base: rem(15px)
  • body-large-screen: rem(14px)
  • button-base: rem(15px)
  • button-large-screen: rem(14px)
  • button-large-base: rem(17px)
  • button-large-large-screen: rem(16px)
  • display-x-large-base: rem(27px)
  • display-x-large-large-screen: rem(42px)
  • display-large-base: rem(24px)
  • display-large-large-screen: rem(28px)
  • display-medium-base: rem(21px)
  • display-medium-large-screen: rem(26px)
  • display-small-base: rem(16px)
  • display-small-large-screen: rem(20px)

Z-index

  • global-ribbon: 510
  • loading-bar: 511
  • top-bar: 512
  • context-bar: 513
  • small-screen-loading-bar: 514
  • nav-backdrop: 515
  • nav: 516
  • skip-to-content: 517
  • backdrop: 518
  • modal: 519
  • toast: 520
  • dev-ui: 521

Audit: Global data already covered by tokens

  • Color
  • Duration
    • add $skeleton-shimmer-duration: duration(slower) * 2 ?
  • Filters
  • Spacing
  • Font-family

Audit: Utility

  • Color-multiply()
  • Ms-high-contrast-color()
  • Rem()
  • Px()
  • Em()
  • Available-names()

Audit: Shared

Accessibility

  • Visually-hidden

Button

  • high-contrast-button-outline
  • button-base
  • base-button-disabled
  • button-filled
  • button-filled-disabled
  • button-outline
  • button-outline-disabled
  • button-full-width
  • plain-button-background()
    • removed after color system
  • plain-button-backdrop
  • unstyled-button

Breakpoints

  • breakpoint()
  • page-content-breakpoint-before
  • page-content-breakpoint-after
  • breakpoint-after
  • breakpoint-before
  • frame-with-nav-when-not-max-width
  • page-when-not-max-width
  • page-content-when-layout-stacked
  • page-content-when-layout-not-stacked
  • page-content-when-partially-condensed
  • page-content-when-not-partially-condensed
  • page-content-when-fully-condensed
  • page-content-when-not-fully-condensed
  • frame-when-nav-displayed
  • frame-when-nav-hidden

Controls

  • control-backdrop

Forms

  • unstyled-input
  • range-track-selectors
  • range-thumb-selectors

Icons

  • recolor-icon
  • color-icon

Interaction State

  • state
    • removed after color system

Layout

  • layout-flex-fix
  • safe-area-for
  • after-topbar-sheet

Links

  • unstyled-link

Lists

  • unstyled-list

Page

  • page-padding-not-fully-condensed
  • page-padding-not-partially-condensed
  • page-layout
  • page-content-layout
  • page-title-layout
  • page-header-layout
  • page-header-has-navigation
  • page-header-without-navigation
  • page-header-has-secondary-actions
  • page-actions-layout

Printing

  • when-printing
  • when-not-printing
  • hidden-when-printing

Skeleton

  • skeleton-shimmer
  • skeleton-content
  • skeleton-page-secondary-actions-layout
  • skeleton-page-header-has-secondary-actions
  • skeleton-page-header-layout

Typography

  • when-typography-not-condensed (media query)
  • when-typography-condensed (media query)
  • text-style-caption
  • text-style-heading
  • text-style-subheading
  • text-style-input
  • text-style-body
  • text-style-button
  • text-style-button-large
  • text-style-display-x-large
  • text-style-display-large
  • text-style-display-medium
  • text-style-display-small
  • text-emphasis-placeholder
  • text-emphasis-subdued
  • text-emphasis-strong
  • text-emphasis-normal
  • text-breakword
  • truncate
  • print-hidden

Audit: Data used in component-specific mixins and functions

Layout

  • primary-min: rem(480px)
  • primary-max: rem(662px)
  • secondary-min: rem(240px)
  • secondary-max: rem(320px)
  • one-half-width-base: rem(450px)
  • one-third-width-base: rem(240px)
  • nav-base: rem(240px)
  • page-with-nav-base: rem(769px)
  • page-content-not-condensed: rem(680px)
  • page-content-partially-condensed: rem(450px)
  • inner-spacing-base: spacing()
    • alias, not very useful
  • outer-spacing-min: spacing(loose)
    • alias, not very useful
  • outer-spacing-max: spacing(extra-loose)
    • alias, not very useful

Breakpoints

(Each of these either represent math done on layout values, or em conversions of layout values, so they don't really need to be tokens)

  • page-max-width: layout-width(primary, max) + layout-width(secondary, max) + layout-width(inner-spacing)
  • frame-with-nav-max-width: layout-width(nav) + $page-max-width
  • stacked-content: em(layout-width(primary, min) + layout-width(secondary, min) + layout-width(inner-spacing))
  • not-condensed-content: em(layout-width(page-content, not-condensed))
  • partially-condensed-content: em(layout-width(page-content, partially-condensed))
  • not-condensed-outer-spacing: em(2 * layout-width(outer-spacing, max))
  • partially-condensed-outer-spacing: em(2 * layout-width(outer-spacing, min))
  • not-condensed-min-page: $not-condensed-content + $not-condensed-outer-spacing
  • partially-condensed-min-page: $partially-condensed-content + $partially-condensed-outer-spacing
  • nav-size: em(layout-width(nav))
  • nav-min-window: em(layout-width(page-with-nav))

Controls

  • control-height: rem(36px)
  • control-slim-height: rem(28px)
  • control-vertical-padding: (control-height() - line-height(input) - rem(2px)) / 2
    • math done on control height and line height
  • control-icon-transition: transform duration(fast) easing(in) easing(out)
    • alias for token values

Icons

  • icon-size: rem(20px)

Page

  • actions-vertical-spacing: spacing(tight)

Skeleton (actually thumbnail)

  • small-thumbnail-size: rem(40px)
  • medium-thumbnail-size: rem(60px)
  • large-thumbnail-size: rem(80px)

Typography

  • typography-condensed: em(640px) (used in breakpoints)

Audit: Component-specific

Badge

  • pip-color

Banner

  • banner-attributes
  • banner-variants

Navigation

  • nav()
  • nav-animation()
  • nav-item-attributes
  • nav-item-icon-attributes
  • nav-listitem-attributes
  • nav-item-text-attributes
  • usermenu-section-attributes

ResouceItem

  • action-hide
  • action-unhide

ResourceList

  • disabled-pointer-events
  • resource-list-overlay

SkeletonDisplayText

  • skeleton-display-text-height

SkeletonThumbnail

  • skeleton-thumbnail-size

Spinner

  • spinnerSize

Stack

  • stack-spacing

TextContainer

  • text-container-spacing

Questions

how do we handle returning rems without a rem function?

  • I think each component that returns rem or em modified units should return px and allow the modification to happen on the consumer side

how should we handle tokens that depend on other tokens?

  • unless the above applies, this feels like a smell that they should not be tokens, and are just aliases for combined token values that live in a project

should utility functions and shared mixins be deprecated and then undeprecated after sass API removal?

I really don't know how to handle this one

The plan is to duplicate all of our common mixins and functions (our internal Sass API) into a common directory, and use the _common file as the place to import all internal Sass from. We'll deprecate everything in the shared and foundation directories, but not the duplicates in common. That will stop us from seeing a mess of deprecation warnings in our internal work, but will warn consumers that these sass functions/mixins that they depend on will go away. The advice for them will be to use tokens where they can, and copy those functions/mixins into their project where they can't.

should utility functions and shared mixins be deprecated and then undeprecated after sass API removal?

I think we should eat our own dogfood and update our components to use the new tokens instead of the utility functions. At which point we're not using them internally and are free to delete the functions making them unavailable for external use when it comes to v5

I think we should eat our own dogfood and update our components to use the new tokens instead of the utility functions. At which point we're not using them internally and are free to delete the functions making them unavailable for external use when it comes to v5

I agree for things that tokens do well, like conveying constants (color, spacing, easing, duration, etc.) but for everything listed in this comment and this comment, we're not going to get anywhere close to tokens serving these functions (rem, em, px, etc., and all the component-specific and shared mixins). We can't get away from a lot of that Sass. So the question is geared towards what we do with those.

I agree that we should burn down everything tokens can do, and just use the tokens. But things like mixins and utility functions can't be done in tokens.

I had a 1:1 conversation with @BPScott to address this question:

should utility functions and shared mixins be deprecated and then undeprecated after sass API removal?

The plan is to duplicate all of our common mixins and functions (our internal Sass API) into a common directory, and use the _common file as the place to import all internal Sass from. We'll deprecate everything in the shared and foundation directories, but not the duplicates in common. That will stop us from seeing a mess of deprecation warnings in our internal work, but will warn consumers that these sass functions/mixins that they depend on will go away. The advice for them will be to use tokens where they can, and copy those functions/mixins into their project where they can't.

First pass at which values should be included in tokens, with proposed names

{
  "border-radius": {
    "base": "3px",
    "large": "6px"
  },
  "border-width": {
    "base": "1px",
    "thick": "2px",
    "thicker": "3px"
  },
  "easing": {
    "base": "cubic-bezier(0.64, 0, 0.35, 1)",
    "in": "cubic-bezier(0.36, 0, 1, 1)",
    "out": "cubic-bezier(0, 0, 0.42, 1)",
    "excite": "cubic-bezier(0.18, 0.67, 0.6, 1.22)",
    "overshoot": "cubic-bezier(0.07, 0.28, 0.32, 1.22)",
    "anticipate": "cubic-bezier(0.38, -0.4, 0.88, 0.65)"
  },
  "shadows": {
    "faint": "( 0 1px 0 0 rgba(22, 29, 37, 0.05), )",
    "base": "( 0 0 0 1px rgba(63, 63, 68, 0.05), 0 1px 3px 0 rgba(63, 63, 68, 0.15), )",
    "deep": "( 0 0 0 1px rgba(6, 44, 82, 0.1), 0 2px 16px rgba(33, 43, 54, 0.08), )",
    "layer": "( 0 31px 41px 0 rgba(32, 42, 53, 0.2), 0 2px 16px 0 rgba(32, 42, 54, 0.08), )",
    "transparent": "0 0 0 0 transparent"
  },
  "line-height": {
    "body": "20px",
    "button": "16px",
    "buttonLarge": "20px",
    "caption": "20px",
    "caption-desktop": "16px",
    "displaySmall": "24px",
    "displaySmall-desktop": "28px",
    "displayMedium": "28px",
    "displayMedium-desktop": "32px",
    "displayLarge": "28px",
    "displayLarge-desktop": "32px",
    "displayExtraLarge": "36p",
    "displayExtraLarge-desktop": "44px",
    "heading": "24px",
    "input": "24px",
    "subheading": "16px"
  },
  "font-size": {
    "body": "15px",
    "body-desktop": "14px",
    "button": "15px",
    "button-desktop": "14px",
    "buttonLarge": "17px",
    "buttonLarge-desktop": "16px",
    "caption": "13px",
    "caption-desktop": "12px",
    "displaySmall": "16px",
    "displaySmall-desktop": "20px",
    "displayMedium": "21px",
    "displayMedium-desktop": "26px",
    "displayLarge": "24px",
    "displayLarge-desktop": "28px",
    "displayExtraLarge": "27px",
    "displayExtraLarge-desktop": "42px",
    "heading": "17px",
    "heading-desktop": "16px",
    "input": "16px",
    "input-desktop": "14px",
    "subheading": "13px",
    "subheading-desktop": "12px"
  },
  "z-index": {
    "backdrop": 518,
    "contextBar": 513,
    "devUi": 521,
    "globalRibbon": 510,
    "loadingBar": 514,
    "loadingBar-desktop": 511,
    "modal": 519,
    "nav": 516,
    "navBackdrop": 515,
    "skipToContent": 517,
    "toast": 520,
    "topBar": 512
  },
  "dimensions": {
    "navigationWidth": "240px",
    "layoutWidth-oneHalf": "450px",
    "layoutWidth-oneThird": "240px",
    "layoutMinWidth-primary": "480px",
    "layoutMaxWidth-primary": "662px",
    "layoutMinWidth-secondary": "240px",
    "layoutMaxWidth-secondary": "320px",
    "controlHeight": "36px",
    "controlHeight-slim": "28px"
  },
  "breakpoints": {
    "pageContent-isPartiallyCondensed": "450px",
    "pageContent-isNotCondensed": "680px",
    "typographyIsNotCondensed": "640px",
    "navigationIsVisible": "769px"
  }
}

About tokens that are component-specific (button, caption, input…): the question you'll want to ask is "do I really want to have to update an external dependency to be able to update that value in the Polaris React library?".

Without going too deep in the nuance of what Theo/Design Tokens can bring, in a nutshell: if you'd rather have control over it easily in Polaris React, or if nobody ever asked to this value to be available outside of Polaris React, then it should probably live in Polaris React.

My assumption is that the tokens are sensible defaults that can be overridden in polaris-react. polaris-tokens creates the structure and naming conventions, the values can be changed if necessary.

I am not sure if I assosiciate base with default. Maybe there could be something else?

Font sizes and z-index should just be done in a numeric way. Associating them with semantic values adds little value here.

I am also wondering about base, large, thick, thicker. These don't scale and I am unsure about what the future values could be added. It might be smart to move these to numeric values as well. I would look at other implementations of these values at scale. Theme specification is tried and tested. Some of the key values might be worth considering changing as well ( border-radius => radii makes it more useful in other situations ).

@johanstromqvist had some thoughts on the easing key name as well, last time I worked with him.

@kaelig and @alex-page based on this feedback, I think our first round should include a smaller set of tokens. We'll omit values for font-size, and line-height for sure, since changing those in an external repo would be painful, and no one has necessarily asked for them to be shared. This will also allow us to make future decisions around themable/generative font sizes and line heights.

I've updated the key names to be more in line with Theme Specification, and have used numerical keys for radii and borderWidths. My concern with radii is that we only ever have two radius values, and that in the master brand, they change to 4 and 8. So a numeric system here may be setting us up for failure as it will imminently get new values.

I'm somewhat hesitant about even including zIndeces, since they are values that could change. My only rationale for including them is that web also uses them in building UIs that respect our stacking context. But I am leaning towards not including them in tokens. If we do include them, numeric names don't seem super useful, as they are literally tied to specific components in their application. I'm going to omit them for now, unless anyone has a strong rationale for including them.

Anything we don't include now, but add later, could be done in a minor release of tokens, so I feel safe starting with a limited set of tokens and reserving the right to expand at a future date.

Regarding the name base, it doesn't bother me much, and I don't think it should be associated with default. root would be my proposed alternative.

Proposed tokens and names round 2

{
  "radii": {
    3: "3px",
    6: "6px"
  },
  "borderWidths": {
    1: "1px",
    2: "2px",
    3: "3px"
  },
  "easing": {
    "base": "cubic-bezier(0.64, 0, 0.35, 1)",
    "in": "cubic-bezier(0.36, 0, 1, 1)",
    "out": "cubic-bezier(0, 0, 0.42, 1)",
    "excite": "cubic-bezier(0.18, 0.67, 0.6, 1.22)",
    "overshoot": "cubic-bezier(0.07, 0.28, 0.32, 1.22)",
    "anticipate": "cubic-bezier(0.38, -0.4, 0.88, 0.65)"
  },
  "shadows": {
    "faint": "( 0 1px 0 0 rgba(22, 29, 37, 0.05), )",
    "base": "( 0 0 0 1px rgba(63, 63, 68, 0.05), 0 1px 3px 0 rgba(63, 63, 68, 0.15), )",
    "deep": "( 0 0 0 1px rgba(6, 44, 82, 0.1), 0 2px 16px rgba(33, 43, 54, 0.08), )",
    "layer": "( 0 31px 41px 0 rgba(32, 42, 53, 0.2), 0 2px 16px 0 rgba(32, 42, 54, 0.08), )",
    "transparent": "0 0 0 0 transparent"
  },
  "sizes": {
    "navigationWidth": "240px",
    "layoutWidth-oneHalf": "450px",
    "layoutWidth-oneThird": "240px",
    "layoutMinWidth-primary": "480px",
    "layoutMaxWidth-primary": "662px",
    "layoutMinWidth-secondary": "240px",
    "layoutMaxWidth-secondary": "320px",
    "pageIsPartiallyCondensed": "450px",
    "pageIsNotCondensed": "680px",
    "typographyIsCondensed": "640px",
    "navigationIsVisible": "769px"
  }
}

Regarding easing:

  1. I haven't seen Excite, Overshoot and Anticipate being applied anywhere. Would it be possible to remove them? We want to avoid bezier curves with values <0 or >1 for now as they don't convey the measured and restrained style we're going for. We might want to add either of these in the future, but in that case with specific intention and documentation.

  2. I think the top three should either be called in-out, in and out to be more consistent, or default, disappear and appear to imply usage. I'm leaning to the latter. You should always be able to default to default and feel good about it.

  3. In POS Next and any other animation work I've done, I've been using these following timing functions. They have good buy-in and are likely to stick around. Reference.

Default (ease-in-out): cubic-bezier(0.4, 0.22, 0.28, 1)
Appear (ease-out): cubic-bezier(0, 0, 0.13, 1)
Disappear (ease-in): cubic-bezier(0.5, 0.1, 1, 1)

We use base in our other tokens to indicate default. I think I prefer default, but I prefer consistency in naming more. How do we feel about base instead of default?

base feels like it grows or shrinks from this value. Which isn't always logically correct. I would prefer calling it default when it is default.

base feels like it grows or shrinks from this value.

That makes perfect sense!

Is it meaningful to call all base/default values either base or default?

I think default is great as long as it's actually a default choice, i.e. when you don't know what token to choose, or if you don't have a specific reason to chose a specific one, you should be able to trust that default is a safe choice.

If that's not the case, then I don't think it should be called default.

A base font size from which all fonts scale doesn't make sense to call default imo. Base is a great name for that.

Final

Review here: #76

BorderWidths

props:
  - name: borderWidth-1
    value: 1px
  - name: borderWidth-2
    value: 2px
  - name: borderWidth-3
    value: 3px
global:
  type: number
  category: sizing

Easings

props:
  - name: easing-default
    value: 'cubic-bezier(0.4, 0.22, 0.28, 1)'
  - name: easing-appear
    value: 'cubic-bezier(0, 0, 0.13, 1)'
  - name: easing-disappear
    value: 'cubic-bezier(0.5, 0.1, 1, 1)'
global:
  type: string
  category: easing

Radii

props:
  - name: radius-3
    value: 3px
  - name: radius-6
    value: 6px
global:
  type: number
  category: radius

Shadows

props:
  - name: shadow-faint
    value: '( 0 1px 0 0 rgba(22, 29, 37, 0.05), )'
  - name: shadow-default
    value: '( 0 0 0 1px rgba(63, 63, 68, 0.05), 0 1px 3px 0 rgba(63, 63, 68, 0.15), )'
  - name: shadow-deep
    value: '( 0 0 0 1px rgba(6, 44, 82, 0.1), 0 2px 16px rgba(33, 43, 54, 0.08), )'
  - name: shadow-layer
    value: '( 0 31px 41px 0 rgba(32, 42, 53, 0.2), 0 2px 16px 0 rgba(32, 42, 54, 0.08), )'
  - name: shadow-transparent
    value: '0 0 0 0 transparent'
global:
  type: string
  category: drop-shadow

Sizes

props:
  - name: size-navigationWidth
    value: 240px
  - name: size-layoutWidth-oneHalf
    value: 450px
  - name: size-layoutWidth-oneThird
    value: 240px
  - name: size-layoutMinWidth-primary
    value: 480px
  - name: size-layoutMaxWidth-primary
    value: 662px
  - name: size-layoutMinWidth-secondary
    value: 240px
  - name: size-layoutMaxWidth-secondary
    value: 320px
  - name: size-pageIsPartiallyCondensed
    value: 450px
  - name: size-pageIsNotCondensed
    value: 680px
  - name: size-typographyIsCondensed
    value: 640px
  - name: size-navigationIsVisible
    value: 769px
global:
  type: number
  category: sizing