From 22230781409992a0227d92f34605de33a37a7450 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Thu, 22 Oct 2026 15:42:03 -0300 Subject: [PATCH 1/2] motion: four durations and one easing, each movement once The site's landing animates now, and each part picked its own timing: 22 ms a character, 650 ms a layer, 700 ms a stroke, three easings. The palette gives them one set, beside the radii and the targets - fast for a hover or a press, base for a card or a menu, slow for a section coming into view, draw for a line of a drawing - and the rule that goes with them: a movement says what something does, runs once, and is its end state under reduced motion. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 +++ cmd/palette/main.go | 4 ++++ cmd/palette/outputs.go | 5 ++++- dist/tokens.css | 5 +++++ dist/tokens.json | 7 +++++++ palette.toml | 11 +++++++++++ 6 files changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd3a0e8..e62880f 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,9 @@ task check # what CI runs: fails on a pair below its floor, or a stale dist Type and layout are tokens too: IBM Plex Sans and Mono (`fonts/`, OFL), a 16 px reading size (below it iOS zooms into a focused input) and 14 px for the dashboard's tables, a 72ch measure, 24 px for any pointer target and 44 px on a coarse pointer (WCAG 2.2, 2.5.8 and 2.5.5). +Motion is four durations and one easing (`--spin-duration-*`, `--spin-ease`): a movement says +what something does or that it answered a hand, runs once, and is its end state under reduced +motion. ## Where it goes diff --git a/cmd/palette/main.go b/cmd/palette/main.go index 8730120..af6de62 100644 --- a/cmd/palette/main.go +++ b/cmd/palette/main.go @@ -53,6 +53,10 @@ type palette struct { TargetTouch int `toml:"target_touch"` Phone, Tablet, Desktop int } + Motion struct { + Fast, Base, Slow, Draw int + Ease string + } } func main() { diff --git a/cmd/palette/outputs.go b/cmd/palette/outputs.go index 9c24af6..6508bae 100644 --- a/cmd/palette/outputs.go +++ b/cmd/palette/outputs.go @@ -22,7 +22,7 @@ func outputs(p palette, results []result) (map[string][]byte, error) { j, err := json.MarshalIndent(map[string]any{ "brand": p.Brand, "dark": hexes(dark), "light": hexes(light), - "tint": p.Tint, "type": p.Type, "layout": p.Layout, + "tint": p.Tint, "type": p.Type, "layout": p.Layout, "motion": p.Motion, }, "", " ") if err != nil { return nil, err @@ -88,6 +88,9 @@ func tokensCSS(p palette, dark, light map[string]rgb) string { fmt.Fprintf(&b, "\t--spin-measure: %dch;\n\t--spin-sidebar: %drem;\n", p.Layout.Measure, p.Layout.Sidebar) fmt.Fprintf(&b, "\t--spin-radius-sm: %dpx;\n\t--spin-radius: %dpx;\n\t--spin-radius-lg: %dpx;\n", p.Layout.RadiusSm, p.Layout.Radius, p.Layout.RadiusLg) fmt.Fprintf(&b, "\t--spin-target: %dpx;\n\t--spin-target-touch: %dpx;\n", p.Layout.Target, p.Layout.TargetTouch) + fmt.Fprintf(&b, "\t--spin-duration-fast: %dms;\n\t--spin-duration: %dms;\n", p.Motion.Fast, p.Motion.Base) + fmt.Fprintf(&b, "\t--spin-duration-slow: %dms;\n\t--spin-duration-draw: %dms;\n", p.Motion.Slow, p.Motion.Draw) + fmt.Fprintf(&b, "\t--spin-ease: %s;\n", p.Motion.Ease) b.WriteString("}\n") return b.String() } diff --git a/dist/tokens.css b/dist/tokens.css index 89472e6..aaa2dca 100644 --- a/dist/tokens.css +++ b/dist/tokens.css @@ -60,4 +60,9 @@ --spin-radius-lg: 12px; --spin-target: 24px; --spin-target-touch: 44px; + --spin-duration-fast: 120ms; + --spin-duration: 200ms; + --spin-duration-slow: 400ms; + --spin-duration-draw: 700ms; + --spin-ease: cubic-bezier(0.2, 0.7, 0.2, 1); } diff --git a/dist/tokens.json b/dist/tokens.json index bf4a35a..878d2c6 100644 --- a/dist/tokens.json +++ b/dist/tokens.json @@ -55,6 +55,13 @@ "text": "#111827", "warning": "#8A5A00" }, + "motion": { + "Fast": 120, + "Base": 200, + "Slow": 400, + "Draw": 700, + "Ease": "cubic-bezier(0.2, 0.7, 0.2, 1)" + }, "tint": { "Hover": 0.1, "Selected": 0.22 diff --git a/palette.toml b/palette.toml index a36ea44..831a307 100644 --- a/palette.toml +++ b/palette.toml @@ -89,3 +89,14 @@ target_touch = 44 # px: a target on a coarse pointer (WCAG 2.5.5, Apple's phone = 390 # px: the narrow width every page is drawn at in preview.html tablet = 768 desktop = 1280 + +# Motion. A movement says what something does - a layer sealed on the one under it, a diff drawn +# between two - or that it answered a hand; one that only decorates is left out. Each runs once: +# nothing moves on its own for longer than 5 s (WCAG 2.2.2), and reduced motion asked for is +# every one at its end state. +[motion] +fast = 120 # ms: a hover, a press, a focus ring +base = 200 # ms: a card, a tab, a menu opening +slow = 400 # ms: a section coming into view +draw = 700 # ms: a line of a drawing being drawn +ease = "cubic-bezier(0.2, 0.7, 0.2, 1)" # out, quick then settling: it arrives rather than bounces From 2cf2b2da12e8d0abb9669f8fe75ace9d35fe5b15 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Thu, 22 Oct 2026 15:45:54 -0300 Subject: [PATCH 2/2] tokens: .spin-dark, the dark roles for a part that is dark in either theme The landing's terminal and its last section are the navy in both themes, and wrote the dark values out by hand - some of them, and four that are no role at all. A .spin-dark part now takes the dark roles from tokens.css. Co-Authored-By: Claude Opus 5.5 --- cmd/palette/outputs.go | 8 +++++--- dist/tokens.css | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/palette/outputs.go b/cmd/palette/outputs.go index 6508bae..d282867 100644 --- a/cmd/palette/outputs.go +++ b/cmd/palette/outputs.go @@ -55,8 +55,8 @@ func sortedKeys(m map[string]rgb) []string { return keys } -// tokensCSS is every role as --spin-: dark on :root, light under data-theme="light", and -// the type and layout tokens beside them. +// tokensCSS is every role as --spin-: dark on :root and .spin-dark, light under +// data-theme="light", and the type, layout and motion tokens beside them. func tokensCSS(p palette, dark, light map[string]rgb) string { var b strings.Builder b.WriteString(header) @@ -69,7 +69,9 @@ func tokensCSS(p palette, dark, light map[string]rgb) string { fmt.Fprintf(&b, "\t--spin-accent-selected: %s;\n", c["accent"].over(c["page"], p.Tint.Selected).hex()) b.WriteString("}\n") } - block(":root", dark) + // .spin-dark is a part that is dark in either theme - a terminal, a drawing on the navy - and + // takes the dark roles in place of the page's. + block(":root, .spin-dark", dark) block(":root[data-theme='light']", light) b.WriteString(":root {\n") names := make([]string, 0, len(p.Brand)) diff --git a/dist/tokens.css b/dist/tokens.css index aaa2dca..6ccfcd5 100644 --- a/dist/tokens.css +++ b/dist/tokens.css @@ -1,5 +1,5 @@ /* Written by `task palette` from palette.toml (spin-stack/brand). Edit palette.toml, not this file. */ -:root { +:root, .spin-dark { --spin-accent: #27BFE8; --spin-accent-text: #27BFE8; --spin-border: #33435F;