[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
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
?
- add
- 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
orem
modified units should returnpx
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:
-
I haven't seen
Excite
,Overshoot
andAnticipate
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. -
I think the top three should either be called
in-out
,in
andout
to be more consistent, ordefault
,disappear
andappear
to imply usage. I'm leaning to the latter. You should always be able to default todefault
and feel good about it. -
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