From 5fe5741ea5317cefc2e361d00f6849ef027b8a45 Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Thu, 8 Oct 2026 03:59:10 +0700 Subject: [PATCH 1/6] docs(skill): Add good-css design guidance to cssforge skill Add a Design Guidance section to SKILL.md linking to good-css and its mapping to CSS Forge features. Create good-css-mapping.md documenting how each good-css rule (OKLCH grays, derived colors with mix, fluid type checks, motion tokens, nested radius, etc.) maps to CSS Forge configuration. Update config templates to follow good-css rules: grays with none hue, mix-derived hover variants, lightDark themes, and goodCssEasings in motion tokens. Closes #84 Co-Authored-By: Claude Haiku 4.5 --- skills/cssforge/SKILL.md | 18 +- .../assets/config-templates/minimal.ts | 10 +- .../assets/config-templates/tailwind.ts | 61 ++- .../assets/config-templates/vanilla-css.ts | 42 ++- skills/cssforge/references/docs-index.md | 3 + .../cssforge/references/good-css-mapping.md | 346 ++++++++++++++++++ 6 files changed, 473 insertions(+), 7 deletions(-) create mode 100644 skills/cssforge/references/good-css-mapping.md diff --git a/skills/cssforge/SKILL.md b/skills/cssforge/SKILL.md index d0ba496..0d56495 100644 --- a/skills/cssforge/SKILL.md +++ b/skills/cssforge/SKILL.md @@ -8,15 +8,27 @@ description: Use CSS Forge documentation and source schema to create, update, an Use this skill when a user wants to use CSS Forge itself: author token configs, generate artifacts, resolve variable references, or debug generation issues. +## Design Guidance + +Follow **good-css** foundations and motion rules when authoring tokens. Good CSS documents modern CSS techniques that replace breakpoints and scripts—such as OKLCH color with `none` for grays, derived hover colors with `color-mix()`, fluid type in `clamp()`, and motion tokens with no `ease-in`. + +- **good-css:** https://good-css.com +- **good-css skill:** https://good-css.com/skills/good-css/SKILL.md +- **good-css foundations reference:** https://good-css.com/references/foundations.md +- **CSS Forge mapping:** `references/good-css-mapping.md` (how to implement each good-css rule in CSS Forge config) + +Read the mapping before authoring colors, typography, spacing, or motion tokens. + ## Source of truth Always prioritize official project docs and source types: 1. `references/docs-index.md` 2. `README.md` (project root) -3. `references/source-schema-map.md` -4. `references/cli-reference.md` -5. `references/troubleshooting.md` +3. `references/good-css-mapping.md` (design guidance) +4. `references/source-schema-map.md` +5. `references/cli-reference.md` +6. `references/troubleshooting.md` Do not invent schema fields that are not present in docs or source types. diff --git a/skills/cssforge/assets/config-templates/minimal.ts b/skills/cssforge/assets/config-templates/minimal.ts index 320521f..352eff5 100644 --- a/skills/cssforge/assets/config-templates/minimal.ts +++ b/skills/cssforge/assets/config-templates/minimal.ts @@ -4,11 +4,19 @@ export default defineConfig({ colors: { palette: { value: { + neutral: { + value: { + white: "oklch(100% 0 none)", + gray: "oklch(50% 0 none)", + black: "oklch(0% 0 none)", + }, + }, brand: { value: { primary: "#1d4ed8", + primaryHover: { mix: { from: "palette.brand.primary", with: "black", amount: 15 } }, accent: "#f97316", - surface: "#0f172a", + accentHover: { mix: { from: "palette.brand.accent", with: "black", amount: 15 } }, }, }, }, diff --git a/skills/cssforge/assets/config-templates/tailwind.ts b/skills/cssforge/assets/config-templates/tailwind.ts index b8dc5cb..e31b49f 100644 --- a/skills/cssforge/assets/config-templates/tailwind.ts +++ b/skills/cssforge/assets/config-templates/tailwind.ts @@ -1,17 +1,60 @@ -import { defineConfig } from "@hebilicious/cssforge"; +import { defineConfig, goodCssEasings } from "@hebilicious/cssforge"; export default defineConfig({ colors: { palette: { value: { + neutral: { + value: { + white: "oklch(100% 0 none)", + gray: "oklch(50% 0 none)", + black: "oklch(0% 0 none)", + }, + }, brand: { value: { primary: "#1d4ed8", + primaryHover: { mix: { from: "palette.brand.primary", with: "black", amount: 15 } }, accent: "#f97316", - surface: "#0f172a", + accentHover: { mix: { from: "palette.brand.accent", with: "black", amount: 15 } }, + accentSubtle: { mix: { from: "palette.brand.accent", with: "transparent", amount: 88 } }, + }, + }, + }, + }, + theme: { + value: { + light: { + value: { + background: { + value: { primary: "var(--white)", secondary: "var(--gray)" }, + variables: { white: "palette.neutral.white", gray: "palette.neutral.gray" }, + }, + text: { + value: { body: "var(--black)", muted: "var(--gray)" }, + variables: { black: "palette.neutral.black", gray: "palette.neutral.gray" }, + }, + }, + }, + dark: { + value: { + background: { + value: { primary: "var(--black)", secondary: "var(--gray)" }, + variables: { black: "palette.neutral.black", gray: "palette.neutral.gray" }, + }, + text: { + value: { body: "var(--white)", muted: "var(--gray)" }, + variables: { white: "palette.neutral.white", gray: "palette.neutral.gray" }, + }, }, }, }, + settings: { + lightDark: { + light: "light", + dark: "dark", + }, + }, }, }, spacing: { @@ -43,4 +86,18 @@ export default defineConfig({ }, }, }, + motion: { + duration: { + ui: { + value: { + press: "100ms", + tooltip: "150ms", + dropdown: "200ms", + }, + }, + }, + easing: { + ui: { value: { ...goodCssEasings } }, + }, + }, }); diff --git a/skills/cssforge/assets/config-templates/vanilla-css.ts b/skills/cssforge/assets/config-templates/vanilla-css.ts index b8744c2..fee94cf 100644 --- a/skills/cssforge/assets/config-templates/vanilla-css.ts +++ b/skills/cssforge/assets/config-templates/vanilla-css.ts @@ -4,12 +4,52 @@ export default defineConfig({ colors: { palette: { value: { + neutral: { + value: { + white: "oklch(100% 0 none)", + ink: "oklch(15% 0 none)", + }, + }, brand: { value: { primary: "#1d4ed8", - surface: "#0f172a", + primaryHover: { mix: { from: "palette.brand.primary", with: "black", amount: 15 } }, + }, + }, + }, + }, + theme: { + value: { + light: { + value: { + background: { + value: { primary: "var(--white)" }, + variables: { white: "palette.neutral.white" }, + }, + text: { + value: { body: "var(--ink)" }, + variables: { ink: "palette.neutral.ink" }, + }, }, }, + dark: { + value: { + background: { + value: { primary: "var(--ink)" }, + variables: { ink: "palette.neutral.ink" }, + }, + text: { + value: { body: "var(--white)" }, + variables: { white: "palette.neutral.white" }, + }, + }, + }, + }, + settings: { + lightDark: { + light: "light", + dark: "dark", + }, }, }, }, diff --git a/skills/cssforge/references/docs-index.md b/skills/cssforge/references/docs-index.md index ea563d9..35d3d92 100644 --- a/skills/cssforge/references/docs-index.md +++ b/skills/cssforge/references/docs-index.md @@ -9,6 +9,9 @@ This file maps user requests to official CSS Forge documentation. ## What to read for each task +- Design guidance and best practices: + - `references/good-css-mapping.md` (how to implement good-css rules in CSS Forge) + - https://good-css.com (the good-css design guidelines) - Installation and setup: - `README.md` -> `## Installation`, `## Quick Start` - Token schema and examples: diff --git a/skills/cssforge/references/good-css-mapping.md b/skills/cssforge/references/good-css-mapping.md new file mode 100644 index 0000000..c376225 --- /dev/null +++ b/skills/cssforge/references/good-css-mapping.md @@ -0,0 +1,346 @@ +# Good CSS to CSS Forge Mapping + +This reference maps good-css foundations and motion rules to CSS Forge configuration. + +## Color System + +### OKLCH with None Hue for Grays + +Good CSS rule: Write grays, white and black with `none` as the hue to prevent unintended hue shifts when mixing. + +CSS Forge implementation: Automatic. The palette converts all colors to OKLCH, and set a color like: + +```typescript +colors: { + palette: { + value: { + neutral: { + value: { + white: "oklch(100% 0 none)", + gray: "oklch(50% 0 none)", + black: "oklch(0% 0 none)", + }, + }, + }, + }, +} +``` + +When you pass a hex or other format for a gray, CSS Forge converts it to OKLCH with `none` for the hue automatically. + +### Derived Colors with color-mix() + +Good CSS rule: Derive hover, tint, and transparent variants from a base color with `color-mix(in oklch, ...)` instead of hand-tuning near-duplicates. + +CSS Forge implementation: Use the `mix` feature on palette or theme colors: + +```typescript +colors: { + palette: { + value: { + accent: { + base: "oklch(55% 0.2 264)", + hover: { mix: { from: "palette.accent.base", with: "black", amount: 15 } }, + subtle: { mix: { from: "palette.accent.base", with: "transparent", amount: 88 } }, + }, + }, + }, +} +``` + +The generated CSS uses `color-mix(in oklch, ...)` and the browser evaluates it at paint time. + +### One Token per Color with light-dark() + +Good CSS rule: Use `light-dark()` to emit one set of color tokens that adapt to the device's color scheme. + +CSS Forge implementation: Use `theme.settings.lightDark`: + +```typescript +colors: { + palette: { + value: { + neutral: { white: "#ffffff", ink: "#1a1a1a" }, + }, + }, + theme: { + value: { + light: { + value: { + background: { + value: { primary: "var(--white)" }, + variables: { white: "palette.neutral.white" }, + }, + }, + }, + dark: { + value: { + background: { + value: { primary: "var(--ink)" }, + variables: { ink: "palette.neutral.ink" }, + }, + }, + }, + }, + settings: { + lightDark: { + light: "light", + dark: "dark", + colorScheme: { light: '[data-theme="light"]', dark: '[data-theme="dark"]' }, + }, + }, + }, +} +``` + +Generated CSS: +```css +:root { + color-scheme: light dark; + --theme-background-primary: light-dark(var(--palette-neutral-white), var(--palette-neutral-ink)); +} +[data-theme="light"] { color-scheme: light; } +[data-theme="dark"] { color-scheme: dark; } +``` + +**Manual step:** Add `` to the page ``. + +## Typography + +### Fluid Sizes with clamp() + +Good CSS rule: Use `clamp(min, preferred, max)` for font sizes that grow with the screen. Keep the maximum at or below 2.5 times the minimum to ensure WCAG 1.4.4 compliance at 500% zoom. + +CSS Forge implementation: Fluid typography generates `clamp()` values and checks the 2.5× limit: + +```typescript +typography: { + fluid: { + base: { + value: { + minWidth: 320, + minFontSize: 16, + minTypeScale: 1.2, + maxWidth: 1280, + maxFontSize: 18, + maxTypeScale: 1.25, + positiveSteps: 3, + negativeSteps: 1, + }, + }, + }, +} +``` + +Generated CSS: +```css +--typography_fluid-base-xl: clamp(1.28rem, 1.216rem + 0.32vw, 1.44rem); +``` + +### Fluid Typography Checks + +CSS Forge performs automatic validation: + +- **Error: more than 2.5× growth.** A step whose max is more than 2.5× its min fails the build. Lower `maxFontSize` or reduce `positiveSteps`. +- **Warning: static scale.** When every step changes by less than 10% across the viewport, its `clamp()` is effectively static. +- **Warning: below legibility floor.** When a step's minimum size is under `settings.minLegibleSize` (default 12px), a warning is issued: + +```typescript +typography: { + fluid: { + caption: { + value: { + minWidth: 320, + minFontSize: 11, + minTypeScale: 1.2, + maxWidth: 1280, + maxFontSize: 13, + maxTypeScale: 1.25, + positiveSteps: 1, + negativeSteps: 0, + }, + settings: { minLegibleSize: 10 }, + }, + }, +} +``` + +Set `minLegibleSize: false` to turn the floor off. + +### Fluid Typography with pow() + +Good CSS rule: Define a scale once and derive every step from six numbers using `pow()`, tunable live in DevTools without recompiling. + +CSS Forge implementation: Use `settings.output: "pow"` to emit the scale inputs instead of computed `clamp()` values: + +```typescript +typography: { + fluid: { + body: { + value: { + minWidth: 320, + minFontSize: 18, + minTypeScale: 1.2, + maxWidth: 1240, + maxFontSize: 20, + maxTypeScale: 1.25, + positiveSteps: 2, + negativeSteps: 1, + relativeTo: "container", + }, + settings: { output: "pow" }, + }, + }, +} +``` + +Generated CSS: +```css +--typography_fluid-body-narrow: 20; +--typography_fluid-body-wide: 77.5; +--typography_fluid-body-size-narrow: 1.125; +--typography_fluid-body-size-wide: 1.25; +--typography_fluid-body-ratio-narrow: 1.2; +--typography_fluid-body-ratio-wide: 1.25; +--typography_fluid-body-fluid: clamp(0rem, (100cqi - var(--typography_fluid-body-narrow) * 1rem) / (var(--typography_fluid-body-wide) - var(--typography_fluid-body-narrow)), 1rem); +--typography_fluid-body-xl: calc(var(--typography_fluid-body-at-narrow) * pow(var(--typography_fluid-body-ratio-narrow), 2) + var(--typography_fluid-body-at-wide) * pow(var(--typography_fluid-body-ratio-wide), 2)); +``` + +Set `relativeTo: "container"` to follow a container's width (requires `pow()` for `100cqi`). + +## Spacing + +### Pixel to Rem Conversion + +Good CSS rule: Keep spacing values in units that follow the reader's font size setting. + +CSS Forge implementation: By default, all top-level `px` lengths are converted to `rem`. Values inside CSS functions like `calc()` or `var()` are left alone: + +```typescript +spacing: { + custom: { + size: { + value: { + 1: "4px", // becomes 0.25rem + 2: "8px", // becomes 0.5rem + pill: "calc(infinity * 1px)", // left as-is, for border-radius + }, + }, + }, +} +``` + +Set `settings: { pxToRem: false }` to keep pixels as-is. + +### Nested Rounded Corners + +Good CSS rule: Derive the outer radius of a padded rounded element from the inner radius using `calc()` to avoid reaching zero. + +CSS Forge implementation: Use primitives with variables referencing spacing tokens: + +```typescript +spacing: { + custom: { + size: { value: { pad: "8px", radius: "12px" } }, + }, +}, +primitives: { + card: { + value: { + outer: { + value: "calc(var(--r) + var(--pad))", + variables: { + r: "spacing.custom.size.radius", + pad: "spacing.custom.size.pad", + }, + }, + }, + }, +} +``` + +Generated CSS: +```css +--primitives-card-outer: calc(var(--spacing-size-radius) + var(--spacing-size-pad)); +``` + +## Motion + +### Motion Tokens + +Good CSS rule: Define easing curves and durations once, then reference them from transitions. Never use `ease-in` on UI. + +CSS Forge implementation: Define motion duration and easing groups. Use the exported `goodCssEasings` for the recommended curves: + +```typescript +import { goodCssEasings } from "@hebilicious/cssforge"; + +export default defineConfig({ + motion: { + duration: { + ui: { + value: { + press: "100ms", + tooltip: "150ms", + dropdown: "200ms", + }, + }, + overlay: { + value: { modal: "400ms" }, + settings: { long: true }, + }, + }, + easing: { + ui: { value: { ...goodCssEasings } }, + }, + }, +}); +``` + +Generated CSS: +```css +--motion-duration-ui-press: 100ms; +--motion-duration-ui-tooltip: 150ms; +--motion-easing-ui-out: cubic-bezier(0.23, 1, 0.32, 1); +--motion-easing-ui-inOut: cubic-bezier(0.77, 0, 0.175, 1); +``` + +### Motion Duration Limits + +Good CSS rule: Keep UI transitions at 300ms or less. Modals and drawers may take 200–500ms. + +CSS Forge implementation: CSS Forge warns when a duration exceeds 300ms. Set `settings.long: true` on duration groups for overlays like modals to raise the limit to 500ms: + +```typescript +motion: { + duration: { + ui: { value: { quick: "150ms" } }, // limit 300ms + overlay: { + value: { drawer: "400ms" }, + settings: { long: true }, // limit 500ms + }, + }, +} +``` + +Warnings are printed to stderr and do not fail the build. + +### Easing Curve Validation + +CSS Forge warns when a curve looks like `ease-in` (starts slow, ends fast), because it reads as lag on UI: + +- `ease-in` warns +- `cubic-bezier()` whose slope at the start is below 1 and at the end is above 1 warns +- `ease`, `ease-out`, `ease-in-out`, `linear`, `steps()` and `linear()` do not warn + +## Manual Implementation + +These good-css rules stay in your stylesheet or HTML: + +- **color-scheme meta tag:** Add `` to `` so the browser picks the scheme before CSS loads. +- **Logical properties:** Write `padding-inline`, `margin-block-start` instead of `padding-left`, `margin-top`. +- **Hover media query:** Wrap `:hover` rules in `@media (hover: hover) and (pointer: fine)`. +- **Focus styling:** Use `:focus-visible` with `outline`, never `outline: none`. +- **Active state:** Add `:active` to every pressable element. +- **Transitions:** Wrap motion inside `@media (prefers-reduced-motion: no-preference)`, name the properties (never `all`). +- **Overflow:** Cut off with `overflow: clip`; keep `overflow: hidden` only where a script scrolls. From db01a4be3bb0ec322a267a53e3511dc6a64670e0 Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Thu, 8 Oct 2026 03:59:58 +0700 Subject: [PATCH 2/6] docs(skill): container scales work in both fluid output modes Refs #84 Co-Authored-By: Claude Opus 5.5 --- skills/cssforge/references/good-css-mapping.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/cssforge/references/good-css-mapping.md b/skills/cssforge/references/good-css-mapping.md index c376225..66e2f5e 100644 --- a/skills/cssforge/references/good-css-mapping.md +++ b/skills/cssforge/references/good-css-mapping.md @@ -206,7 +206,7 @@ Generated CSS: --typography_fluid-body-xl: calc(var(--typography_fluid-body-at-narrow) * pow(var(--typography_fluid-body-ratio-narrow), 2) + var(--typography_fluid-body-at-wide) * pow(var(--typography_fluid-body-ratio-wide), 2)); ``` -Set `relativeTo: "container"` to follow a container's width (requires `pow()` for `100cqi`). +Set `relativeTo: "container"` to size the scale by its container (`cqi`) instead of the viewport. It works in both `clamp` and `pow` output. ## Spacing From c63e88beab29b281c14dfb702afc82cebd07708d Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Thu, 8 Oct 2026 05:38:17 +0700 Subject: [PATCH 3/6] docs: credit good-css in the README and vendor its skill for agents The cssforge skill documents how to use CSS Forge, so it no longer points at good-css or carries a good-css mapping. good-css is credited next to Utopia in the README, and its skill is vendored under .agents/skills for agents working in this repository. Refs #84 Co-Authored-By: Claude Opus 5.5 --- .agents/skills/good-css/SKILL.md | 53 +++ .../skills/good-css/references/foundations.md | 209 +++++++++++ .../skills/good-css/references/interaction.md | 208 +++++++++++ .agents/skills/good-css/references/layout.md | 199 ++++++++++ .agents/skills/good-css/references/motion.md | 210 +++++++++++ .../references/scroll-and-viewport.md | 222 +++++++++++ .../good-css/references/show-and-hide.md | 156 ++++++++ .../good-css/references/spacing-and-shape.md | 117 ++++++ .../good-css/references/text-and-media.md | 159 ++++++++ README.md | 7 + packages/cssforge/README.md | 7 + skills/cssforge/SKILL.md | 18 +- skills/cssforge/references/docs-index.md | 3 - .../cssforge/references/good-css-mapping.md | 346 ------------------ 14 files changed, 1550 insertions(+), 364 deletions(-) create mode 100644 .agents/skills/good-css/SKILL.md create mode 100644 .agents/skills/good-css/references/foundations.md create mode 100644 .agents/skills/good-css/references/interaction.md create mode 100644 .agents/skills/good-css/references/layout.md create mode 100644 .agents/skills/good-css/references/motion.md create mode 100644 .agents/skills/good-css/references/scroll-and-viewport.md create mode 100644 .agents/skills/good-css/references/show-and-hide.md create mode 100644 .agents/skills/good-css/references/spacing-and-shape.md create mode 100644 .agents/skills/good-css/references/text-and-media.md delete mode 100644 skills/cssforge/references/good-css-mapping.md diff --git a/.agents/skills/good-css/SKILL.md b/.agents/skills/good-css/SKILL.md new file mode 100644 index 0000000..fd94d09 --- /dev/null +++ b/.agents/skills/good-css/SKILL.md @@ -0,0 +1,53 @@ +--- +name: good-css +description: Modern CSS techniques that replace breakpoint ladders, wrapper elements and scripts. Use whenever you write, edit or review styles in any form, including plain CSS, Tailwind classes, StyleX, CSS-in-JS and inline styles, and whenever you build or restyle a page or component, even if the user never mentions CSS. +metadata: + source: https://good-css.com/skills/good-css/SKILL.md + fetched: 2026-10-08 +--- + +# Good CSS + +One declaration that adapts on its own beats a set of breakpoints, and a CSS feature beats a script. Use JavaScript only where it makes a result nicer that already works without it. + +Each technique is a set of properties and values, so it works in any authoring system. Class names in the examples are placeholders. Write the declarations in what the project already uses, whether that is a stylesheet, Tailwind utilities or StyleX objects. Where that system has no shorthand for a declaration, write it the long way, as a Tailwind arbitrary property or a rule in the project's CSS file. Do not swap the technique for a breakpoint. + +## In all CSS + +These need no file. + +- Write `inline` and `block` properties in place of left, right, top and bottom. Tailwind has them too, as in `mbs-`, `pe-` and `inset-bs-`, so never write `mt-`, `pr-` or `top-`. +- Write colors in `oklch()`, with `none` as the hue of a gray, white or black. Derive a hover, tint or transparent version with `color-mix(in oklch, …)`. +- Put sizes that grow with the screen in one `clamp()` token. +- Put every `:hover` rule inside `@media (hover: hover) and (pointer: fine)`. +- Style focus with `:focus-visible` and `outline`. Never write `outline: none`. +- Give everything pressable an `:active` state. +- Put a transition that moves or scales something inside `@media (prefers-reduced-motion: no-preference)`. Name its properties, never `all`, and never use `ease-in`. +- Cut off overflow with `overflow: clip`. Keep `hidden` for an element a script scrolls. + +## Read the entry before you write + +Each file holds entries with the CSS and its rules. The rules are the conditions the CSS needs to work, so keep all of them. Read only the files whose row matches what you are about to write. One component matches two or three, and a whole page matches most of them. + +| Read | Before you write | +| --- | --- | +| `references/foundations.md` | a reset or base stylesheet, a set of color tokens, dark mode, a type or spacing scale | +| `references/layout.md` | a page container or wrapper, a grid of cards, cards whose parts line up, a sidebar, a component placed in slots of different widths, overlapping layers, centered content that can overflow | +| `references/spacing-and-shape.md` | spacing between sections, elements stacked one above another, one item pushed to the far end of a row or column, nested rounded corners | +| `references/text-and-media.md` | text or images that come from a user or a CMS, such as names, titles, excerpts, thumbnails, avatars and embeds, an icon next to a label, a label that looks off-center, numbers in a column | +| `references/interaction.md` | the focus, hover and press states of a button, link, input or card, a small tap target, a clickable card, form validation, a textarea, a parent styled by what it contains, page scroll locked behind a modal | +| `references/motion.md` | easing and duration tokens, one state change that drives several values, a shadow that changes on hover, a transition between pages, the indicator under an active tab | +| `references/show-and-hide.md` | a dialog, popover, dropdown menu or accordion | +| `references/scroll-and-viewport.md` | a carousel, a row that may overflow, a modal body, chat list or drawer that scrolls between a header and a footer, in-page links under a sticky header, an app shell on desktop or on a phone | + +## Left out on purpose + +Never add `text-box` trim on `*`, `text-rendering: optimizeLegibility`, or a reset that puts `display: contents` or one shared grid cell on every element. `references/foundations.md` has the reasons. + +## Browser support + +Most entries end with a `Support:` line. This skill sets no browser floor. Weigh the line against the browsers the project targets, and tell the user when something you used is missing from one of them. + +## Motion + +Whether something should animate, the design or review of motion, springs, gestures and drag, and checks on a real phone belong to Emil Kowalski's skills `animate`, `review-animations`, `improve-animations`, `find-animation-opportunities` and `mobile-native`. When they are installed, use them for those jobs, and where one disagrees with a value here, use its value. When they are not, use the entries as written and add no motion beyond what an entry or the task calls for. diff --git a/.agents/skills/good-css/references/foundations.md b/.agents/skills/good-css/references/foundations.md new file mode 100644 index 0000000..3e213a8 --- /dev/null +++ b/.agents/skills/good-css/references/foundations.md @@ -0,0 +1,209 @@ + + +## The reset + +Start every new project with it. In an existing project add one rule at a time and look at the result, because `min-width: 0` and the font rules change how things already render. + +```css +*, +*::before, +*::after { + box-sizing: border-box; + min-width: 0; +} + +:root { + interpolate-size: allow-keywords; + scrollbar-gutter: stable; + text-wrap: pretty; + overflow-wrap: break-word; + font-synthesis: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: transparent; +} + +body { + margin: 0; + min-height: 100svh; +} + +h1, h2, h3, h4 { text-wrap: balance; } + +img, svg, video { + display: block; + max-width: 100%; + height: auto; +} + +input, textarea, select { font-size: max(16px, 1rem); } + +button, a, [role="button"] { touch-action: manipulation; } + +button, [role="button"] { + user-select: none; + -webkit-user-select: none; +} +``` + +Rules: + +- Load every weight and style the design uses. With `font-synthesis: none` a missing bold renders as regular. +- With `min-width: 0` on everything, a sized image, icon or avatar in a flex row shrinks to make room for the text beside it. Give it `flex: none`. +- The reset removes the tap highlight, so every pressable element needs its own `:active` state. "Press feedback" (`interaction.md`) has it. +- Never fix input zoom with `user-scalable=no` or `maximum-scale=1`. That takes zoom away from people who need it. +- Keep `user-select: none` to controls. Never set it on `body` or on links, because people copy text. +- Text the user types inherits `pretty`, in `textarea` and `contenteditable`. If lines shift while typing, set `text-wrap: stable` on that element. +- The reserved gutter puts centered content slightly off-center on a page too short to scroll. Chris Coyier documents this. Accept it. +- `svh` is for documents and heroes. An app shell with UI pinned to the bottom uses `height: 100dvh`, so it tracks the toolbar. Emil Kowalski draws this line in his mobile-native skill. + +Support: `text-wrap: pretty` in Chrome 117 and Safari 26, ignored by Firefox. `scrollbar-gutter` in Chrome 94, Firefox 97, Safari 18.2. `interpolate-size` in Chrome 129 only. The rest works everywhere. + +## Logical properties + +Use them in place of every left, right, top and bottom, in spacing, borders, offsets and alignment. + +```css +.card { + position: relative; + padding-inline: 1.5rem; + margin-block-end: 2rem; + border-inline-start: 4px solid; + text-align: start; +} + +.card .close { + position: absolute; + inset-block-start: 1rem; + inset-inline-end: 1rem; +} +``` + +Rules: + +- The four-value `margin`, `padding` and `inset` shorthands stay physical. Use the `-inline` and `-block` shorthands. + +Support: Chrome 87, Firefox 66, Safari 14.1. + +## OKLCH color + +Write colors in `oklch()` and derive every related color from a base with `color-mix(in oklch, …)`. Never hardcode a second hex or an `rgba()` for a hover, tint or transparent version. + +```css +:root { + --primary: oklch(55% 0.15 250); + --primary-hover: color-mix(in oklch, var(--primary), black 15%); + --primary-subtle: color-mix(in oklch, var(--primary) 12%, transparent); +} +``` + +Rules: + +- Write grays, white and black with `none` as the hue, as in `oklch(98% 0 none)`. A written hue of `0` is red, and it pulls every mix toward red. Half white `oklch(1 0 0)` and half a blue at hue 264 came out at hue 312 in Chrome and Safari. With `none`, or with the keyword `white`, it stayed at 264. + +## One set of color tokens for light and dark + +Use it on any site with a dark mode. Each token holds both values, so there is no second block of variables under a media query or a `.dark` class. + +```css +:root { + color-scheme: light dark; + --background: light-dark(oklch(1 0 none), oklch(0.145 0 none)); + --foreground: light-dark(oklch(0.145 0 none), oklch(0.985 0 none)); +} + +[data-theme="light"] { color-scheme: light; } +[data-theme="dark"] { color-scheme: dark; } +``` + +Rules: + +- Switch themes by setting `color-scheme`. Never redefine the tokens. +- The grays use `none` for the hue, so they can be mixed with a color later without drifting toward red. "OKLCH color" has the measurement. +- `light-dark()` takes two colors. Anything else that differs by theme, such as an image, needs its own rule. +- Add `` so the browser paints the right background before the CSS loads. + +Support: Chrome 123, Firefox 120, Safari 17.5. Baseline since May 2024. + +## Fluid sizes with `clamp()` + +Use it for anything that should grow with the screen, starting with font sizes and section spacing. One declaration replaces a ladder of breakpoint overrides. + +```css +:root { + --text-body: clamp(1rem, 0.75rem + 1vw, 1.75rem); + --space-section: clamp(3rem, 1rem + 6vw, 8rem); +} +``` + +Rules: + +- Write the preferred value as `rem + vw`, never `vw` alone. `clamp(1rem, 1vw, 1.75rem)` stays at 16px on every screen narrower than 1600px, because 1vw is smaller than 1rem until then. A bare `vw` value also ignores the reader's font size setting. +- Work the preferred value out from two points. The slope is `(max - min) / (wide - narrow)`, in px over px, and times 100 it is the `vw` number. The `rem` part is `min - slope × narrow`. +- Keep both bounds in `rem` so they follow the reader's font size. +- For a font size, keep the maximum at or below 2.5 times the minimum. The `vw` part does not grow when the reader zooms, so fluid text grows less than the page. Past 2.5 times, 500% zoom cannot double the text at some widths, and that fails WCAG 1.4.4. +- Put fluid values in tokens. Components use the token and never repeat the math. +- For more than two or three sizes, derive a whole scale as "One fluid scale for type and space" does. + +## One fluid scale for type and space + +Use it when a design has more than two or three font sizes. Six numbers describe every size, and the browser derives the steps. It replaces a table of sizes per breakpoint, and it replaces a generator that someone has to find and run again. + +```css +:root { + --narrow: 20; + --wide: 77.5; + --size-narrow: 1.125; + --size-wide: 1.25; + --ratio-narrow: 1.2; + --ratio-wide: 1.25; + + --fluid: clamp(0rem, (100vw - var(--narrow) * 1rem) / (var(--wide) - var(--narrow)), 1rem); + --at-narrow: calc(var(--size-narrow) * (1rem - var(--fluid))); + --at-wide: calc(var(--size-wide) * var(--fluid)); + + --step--1: calc(var(--step-0) / var(--ratio-narrow)); + --step-0: calc(var(--at-narrow) + var(--at-wide)); + --step-1: calc(var(--at-narrow) * var(--ratio-narrow) + var(--at-wide) * var(--ratio-wide)); + --step-2: calc(var(--at-narrow) * pow(var(--ratio-narrow), 2) + var(--at-wide) * pow(var(--ratio-wide), 2)); + --step-3: calc(var(--at-narrow) * pow(var(--ratio-narrow), 3) + var(--at-wide) * pow(var(--ratio-wide), 3)); + --step-4: calc(var(--at-narrow) * pow(var(--ratio-narrow), 4) + var(--at-wide) * pow(var(--ratio-wide), 4)); + + --space-s: var(--step-0); + --space-m: calc(1.5 * var(--step-0)); + --space-l: calc(2 * var(--step-0)); + --space-s-l: calc(var(--at-narrow) + 2 * var(--at-wide)); +} + +body { font-size: var(--step-0); } +h1 { font-size: var(--step-4); } +h2 { font-size: var(--step-2); } +small { font-size: var(--step--1); } + +.card { + display: grid; + gap: var(--space-s); + padding: var(--space-s-l); +} +``` + +Rules: + +- Write the inputs as plain numbers. With `20rem` and `77.5rem` the `--fluid` line divides a length by a length, and Firefox does not support that. +- Change an input in the rule that declares the steps. A step takes its value where it is declared, so a ratio set on a descendant changes nothing below it. +- Derive sizes below step 0 from step 0 and the narrow ratio, as `--step--1` does. With both ratios a small size shrinks as the screen grows. +- Check the top step against the limit in "Fluid sizes with `clamp()`". Step `n` passes when `(size-wide / size-narrow) × (ratio-wide / ratio-narrow)ⁿ` is 2.5 or less. The example gives 1.31 at step 4. +- Write a pair only where a layout uses it, and never use one for a font size. A pair is steep on purpose and breaks that limit. +- For a scale that follows the container, write `100cqi` in place of `100vw`. "Container queries with container units" (`layout.md`) has the rules. +- Components use the tokens and never repeat the math. + +Support: Chrome 120, Firefox 118, Safari 15.4, set by `pow()`. Tested in Chrome 150 and real Safari 27 on 2026-10-01, where every step came out within 0.001px of the formula. + +## Left out on purpose + +Decided on 2026-10-01. Do not add these back. + +- **The layout half of Travis Arnold's reset.** `* { grid-area: 1 / 1 / 1 / 1 }`, a 12-column grid on `body`, and `display: contents` on `div` and every sectioning element. It stacks everything in one cell and removes the box of every container, which breaks "Content grid with breakouts" (`layout.md`) and any component that styles a `div`. +- **`* { text-box: trim-both cap alphabetic }`** in the reset. It shrinks every text block. A one-line paragraph at 16px/1.5 went from 25px tall to 11px and a button from 40px to 27px. "Label centered on its letters with `text-box`" (`text-and-media.md`) is the scoped version, for labels only. +- **`text-rendering: optimizeLegibility`.** MDN recommends `auto` for body text. diff --git a/.agents/skills/good-css/references/interaction.md b/.agents/skills/good-css/references/interaction.md new file mode 100644 index 0000000..d1cc2f3 --- /dev/null +++ b/.agents/skills/good-css/references/interaction.md @@ -0,0 +1,208 @@ + + +## One focus ring with `:focus-visible` + +Use it as the focus style for every interactive element. Never write `outline: none`. + +```css +:focus-visible { + outline: max(2px, 0.08em) solid currentColor; + outline-offset: 0.25em; +} +``` + +Rules: + +- On a filled button `currentColor` can fail contrast. Set the outline to the button's background color there. +- A component that draws its ring with `box-shadow` keeps `outline-color: transparent`, never `outline: none`, so forced-colors mode can repaint it. + +Support: Chrome 86, Firefox 85, Safari 15.4. + +## Hover styles only where hover exists + +Use it on every `:hover` rule, so a tap on a touch screen does not leave the hover state stuck. + +```css +@media (hover: hover) and (pointer: fine) { + .button:hover { background: var(--primary-hover); } +} +``` + +Rules: + +- Tailwind v4's `hover:` variant wraps itself in `(hover: hover)` only. Write the full query by hand everywhere else. +- Touch users still need feedback on press. Give it with `:active`, which works for every input. + +Support: Chrome 38, Firefox 64, Safari 9. + +## Press feedback + +Use it on every button and anything else that can be pressed. + +```css +.button:active { transform: scale(0.97); } + +@media (prefers-reduced-motion: no-preference) { + .button { transition: transform 160ms var(--ease-out, ease-out); } +} +``` + +Rules: + +- Keep the scale between 0.95 and 0.98. +- "The reset" (`foundations.md`) removes the browser's tap highlight. Without this entry a tap gives no feedback at all. +- When feedback needs a script, listen for `pointerdown`, not `click`. + +Support: every browser. + +## Hit area larger than the visual + +Use it on icon buttons, close buttons and any target that looks smaller than 44px. + +```css +.icon-button { position: relative; } + +.icon-button::after { + content: ""; + position: absolute; + inset: min(0px, (100% - 44px) / 2); +} +``` + +Rules: + +- `overflow: hidden` or `clip` on the button cuts the area off. +- It does not work on ``, which has no pseudo-elements. Wrap it in a `