@daydreamui/good-css 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -2
- package/dist/index.js +2 -0
- package/icon.svg +8 -0
- package/knowledge/INDEX.md +67 -0
- package/knowledge/compatibility.md +33 -0
- package/knowledge/examples/container-queries-with-container-units.md +150 -0
- package/knowledge/examples/content-grid-with-breakouts.md +161 -0
- package/knowledge/examples/intrinsic-grid.md +150 -0
- package/knowledge/examples/sidebar-that-wraps-on-its-own.md +156 -0
- package/knowledge/examples/subgrid-rows-shared-across-cards.md +154 -0
- package/knowledge/procedures.md +37 -0
- package/knowledge/recipes/accordion-that-animates-its-height.md +59 -0
- package/knowledge/recipes/anchor-targets-that-clear-a-sticky-header.md +45 -0
- package/knowledge/recipes/carousel-on-native-scroll.md +87 -0
- package/knowledge/recipes/concentric-nested-radius.md +45 -0
- package/knowledge/recipes/container-queries-with-container-units.md +52 -0
- package/knowledge/recipes/content-clear-of-the-notch.md +45 -0
- package/knowledge/recipes/content-grid-with-breakouts.md +69 -0
- package/knowledge/recipes/cross-document-view-transitions.md +39 -0
- package/knowledge/recipes/enter-and-exit-transitions-from-display-none.md +87 -0
- package/knowledge/recipes/fluid-sizes-with-clamp.md +46 -0
- package/knowledge/recipes/form-feedback-with-user-invalid.md +45 -0
- package/knowledge/recipes/has-for-parent-and-page-state.md +46 -0
- package/knowledge/recipes/hit-area-larger-than-the-visual.md +47 -0
- package/knowledge/recipes/hover-styles-only-where-hover-exists.md +43 -0
- package/knowledge/recipes/icon-sized-by-the-text-beside-it.md +69 -0
- package/knowledge/recipes/image-box-that-holds-any-upload.md +67 -0
- package/knowledge/recipes/indicator-that-slides-to-the-active-item.md +80 -0
- package/knowledge/recipes/intrinsic-grid.md +41 -0
- package/knowledge/recipes/label-centered-on-its-letters-with-text-box.md +59 -0
- package/knowledge/recipes/logical-properties.md +48 -0
- package/knowledge/recipes/long-text-that-wraps-truncates-or-clamps.md +62 -0
- package/knowledge/recipes/motion-tokens.md +46 -0
- package/knowledge/recipes/no-rubber-band-bounce-on-desktop.md +47 -0
- package/knowledge/recipes/oklch-color.md +41 -0
- package/knowledge/recipes/one-fluid-scale-for-type-and-space.md +85 -0
- package/knowledge/recipes/one-focus-ring-with-focus-visible.md +44 -0
- package/knowledge/recipes/one-set-of-color-tokens-for-light-and-dark.md +53 -0
- package/knowledge/recipes/opt-in-motion.md +43 -0
- package/knowledge/recipes/overflow-clip-over-hidden.md +41 -0
- package/knowledge/recipes/popover-anchored-to-its-trigger.md +57 -0
- package/knowledge/recipes/press-feedback.md +43 -0
- package/knowledge/recipes/push-one-item-away-with-an-auto-margin.md +70 -0
- package/knowledge/recipes/reveal-with-clip-path.md +61 -0
- package/knowledge/recipes/safe-alignment.md +41 -0
- package/knowledge/recipes/scroll-area-between-a-fixed-header-and-footer.md +67 -0
- package/knowledge/recipes/section-spacing-that-depends-on-its-neighbors.md +54 -0
- package/knowledge/recipes/shadow-change-that-fades-and-does-not-repaint.md +80 -0
- package/knowledge/recipes/sidebar-that-wraps-on-its-own.md +56 -0
- package/knowledge/recipes/space-between-siblings-set-by-the-parent.md +59 -0
- package/knowledge/recipes/stack-layers-with-grid.md +43 -0
- package/knowledge/recipes/styles-that-apply-only-when-a-scroller-overflows.md +99 -0
- package/knowledge/recipes/subgrid-rows-shared-across-cards.md +51 -0
- package/knowledge/recipes/tabular-numbers.md +40 -0
- package/knowledge/recipes/textarea-that-grows-with-its-content.md +73 -0
- package/knowledge/recipes/the-reset.md +96 -0
- package/knowledge/recipes/transition-a-custom-property-with-property.md +70 -0
- package/knowledge/recipes/whole-card-clickable-from-one-link.md +71 -0
- package/knowledge/upstream-guidance.md +72 -0
- package/manifest.json +12 -0
- package/package.json +15 -4
- package/playgrounds/base.css +57 -0
- package/playgrounds/container-queries-with-container-units.css +10 -0
- package/playgrounds/container-queries-with-container-units.html +13 -0
- package/playgrounds/content-grid-with-breakouts.css +21 -0
- package/playgrounds/content-grid-with-breakouts.html +13 -0
- package/playgrounds/fixtures.json +32 -0
- package/playgrounds/intrinsic-grid.css +5 -0
- package/playgrounds/intrinsic-grid.html +19 -0
- package/playgrounds/sidebar-that-wraps-on-its-own.css +16 -0
- package/playgrounds/sidebar-that-wraps-on-its-own.html +13 -0
- package/playgrounds/subgrid-rows-shared-across-cards.css +12 -0
- package/playgrounds/subgrid-rows-shared-across-cards.html +13 -0
- package/upstream/LICENSE +21 -0
- package/upstream/lock.json +7 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: "good-css-styles-that-apply-only-when-a-scroller-overflows"
|
|
3
|
+
title: "Styles that apply only when a scroller overflows"
|
|
4
|
+
tier: "reference"
|
|
5
|
+
tags:
|
|
6
|
+
- "good-css"
|
|
7
|
+
- "scroll"
|
|
8
|
+
- "viewport"
|
|
9
|
+
- "styles"
|
|
10
|
+
- "apply"
|
|
11
|
+
- "only"
|
|
12
|
+
- "when"
|
|
13
|
+
- "scroller"
|
|
14
|
+
- "overflows"
|
|
15
|
+
summary: "Use it on any row that may or may not fit, such as tabs, a toolbar or a table wrapper. It fades the edges only when there is something to scroll to. It replaces a `ResizeObserver` that compares `scrollWidth` with `clientWidth`."
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
|
|
19
|
+
# Styles that apply only when a scroller overflows
|
|
20
|
+
|
|
21
|
+
Use it on any row that may or may not fit, such as tabs, a toolbar or a table wrapper. It fades the edges only when there is something to scroll to. It replaces a `ResizeObserver` that compares `scrollWidth` with `clientWidth`.
|
|
22
|
+
|
|
23
|
+
```css
|
|
24
|
+
@keyframes overflowing {
|
|
25
|
+
from, to { --fade: 2rem; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.scroller {
|
|
29
|
+
--fade: 0px;
|
|
30
|
+
overflow-x: auto;
|
|
31
|
+
animation: overflowing linear;
|
|
32
|
+
animation-timeline: scroll(self inline);
|
|
33
|
+
mask-image: linear-gradient(
|
|
34
|
+
to right,
|
|
35
|
+
transparent,
|
|
36
|
+
#000 var(--fade),
|
|
37
|
+
#000 calc(100% - var(--fade)),
|
|
38
|
+
transparent
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
To fade only the side that still has content, register the two lengths and give the keyframes different ends:
|
|
44
|
+
|
|
45
|
+
```css
|
|
46
|
+
@property --fade-start {
|
|
47
|
+
syntax: "<length>";
|
|
48
|
+
inherits: false;
|
|
49
|
+
initial-value: 0px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@property --fade-end {
|
|
53
|
+
syntax: "<length>";
|
|
54
|
+
inherits: false;
|
|
55
|
+
initial-value: 0px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@keyframes edges {
|
|
59
|
+
from { --fade-start: 0px; --fade-end: 2rem; }
|
|
60
|
+
10%, 90% { --fade-start: 2rem; --fade-end: 2rem; }
|
|
61
|
+
to { --fade-start: 2rem; --fade-end: 0px; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.scroller-edges {
|
|
65
|
+
overflow-x: auto;
|
|
66
|
+
animation: edges linear;
|
|
67
|
+
animation-timeline: scroll(self inline);
|
|
68
|
+
mask-image: linear-gradient(
|
|
69
|
+
to right,
|
|
70
|
+
transparent,
|
|
71
|
+
#000 var(--fade-start),
|
|
72
|
+
#000 calc(100% - var(--fade-end)),
|
|
73
|
+
transparent
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Why it works:
|
|
79
|
+
|
|
80
|
+
- A scroll timeline is inactive while its scroller has nothing to scroll. An animation on an inactive timeline has no effect, so the keyframes apply only when the content overflows.
|
|
81
|
+
- In the first block `from` and `to` hold the same value. Scroll position changes nothing and the animation is an on/off switch.
|
|
82
|
+
- The keyframes set a custom property, and custom properties inherit. The scroller and everything inside it can read the result.
|
|
83
|
+
- A mask fades the content itself, so it works over any background. An overlay gradient has to match the color behind it.
|
|
84
|
+
|
|
85
|
+
Rules:
|
|
86
|
+
|
|
87
|
+
- Write `animation-timeline` after `animation`. The shorthand resets the timeline.
|
|
88
|
+
- Declare the default outside the keyframes, as `--fade: 0px` does here.
|
|
89
|
+
- Any value can go in the keyframes, including keywords. Stripe flips its navigation to `row-reverse` this way when a translation is too long to fit.
|
|
90
|
+
- Write no fallback. Firefox has no scroll timelines, so the row scrolls there without the fade.
|
|
91
|
+
|
|
92
|
+
Support: `animation-timeline` in Chrome 115 and Safari 26. Firefox has it in Nightly only.
|
|
93
|
+
|
|
94
|
+
- Borrowed from: Stripe, in the navigation on stripe.com · Bramus Van Damme, [Detect if an element can scroll or not with only CSS](https://www.bram.us/2023/09/16/solved-by-css-scroll-driven-animations-detect-if-an-element-can-scroll-or-not/)
|
|
95
|
+
- Docs: [MDN: scroll()](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/animation-timeline/scroll)
|
|
96
|
+
|
|
97
|
+
Source: [Good CSS](https://good-css.com/specimen/styles-that-apply-only-when-a-scroller-overflows), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#styles-that-apply-only-when-a-scroller-overflows).
|
|
98
|
+
|
|
99
|
+
Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: "good-css-subgrid-rows-shared-across-cards"
|
|
3
|
+
title: "Subgrid rows shared across cards"
|
|
4
|
+
tier: "reference"
|
|
5
|
+
tags:
|
|
6
|
+
- "good-css"
|
|
7
|
+
- "layout"
|
|
8
|
+
- "subgrid"
|
|
9
|
+
- "rows"
|
|
10
|
+
- "shared"
|
|
11
|
+
- "across"
|
|
12
|
+
- "cards"
|
|
13
|
+
summary: "Use it when cards in a row have parts that should line up, such as title, body and footer, whatever the length of their content."
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
|
|
17
|
+
# Subgrid rows shared across cards
|
|
18
|
+
|
|
19
|
+
Use it when cards in a row have parts that should line up, such as title, body and footer, whatever the length of their content.
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
.cards {
|
|
23
|
+
display: grid;
|
|
24
|
+
gap: 1.5rem;
|
|
25
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.card {
|
|
29
|
+
display: grid;
|
|
30
|
+
grid-row: span 3;
|
|
31
|
+
grid-template-rows: subgrid;
|
|
32
|
+
row-gap: 0.5rem;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Why it works:
|
|
37
|
+
|
|
38
|
+
- Each card spans three rows of the parent grid and adopts those tracks. The tallest title in a row sets the title height for every card in that row.
|
|
39
|
+
|
|
40
|
+
Rules:
|
|
41
|
+
|
|
42
|
+
- The span must equal the number of parts in the card. Without `grid-row: span N` every part piles into one row.
|
|
43
|
+
- Set `row-gap` on the card, or it inherits the parent's gap between its parts.
|
|
44
|
+
|
|
45
|
+
Support: Chrome 117, Firefox 71, Safari 16.
|
|
46
|
+
|
|
47
|
+
- Borrowed from: Josh W. Comeau, [Brand New Layouts with CSS Subgrid](https://www.joshwcomeau.com/css/subgrid/), 2025-11-25 · Kevin Powell, [Subgrid & Container Queries change how we can create layouts](https://www.youtube.com/watch?v=Zddz_R1RnfM), 2023-10-26
|
|
48
|
+
|
|
49
|
+
Source: [Good CSS](https://good-css.com/specimen/subgrid-rows-shared-across-cards), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#subgrid-rows-shared-across-cards).
|
|
50
|
+
|
|
51
|
+
Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: "good-css-tabular-numbers"
|
|
3
|
+
title: "Tabular numbers"
|
|
4
|
+
tier: "reference"
|
|
5
|
+
tags:
|
|
6
|
+
- "good-css"
|
|
7
|
+
- "text"
|
|
8
|
+
- "media"
|
|
9
|
+
- "tabular"
|
|
10
|
+
- "numbers"
|
|
11
|
+
summary: "Use it on any number that changes or sits in a column, such as prices, tables, timers and counters."
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
|
|
15
|
+
# Tabular numbers
|
|
16
|
+
|
|
17
|
+
Use it on any number that changes or sits in a column, such as prices, tables, timers and counters.
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
.price,
|
|
21
|
+
td,
|
|
22
|
+
time { font-variant-numeric: tabular-nums; }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Why it works:
|
|
26
|
+
|
|
27
|
+
- Every digit gets the same width, so columns line up and a ticking value does not shift its neighbors.
|
|
28
|
+
|
|
29
|
+
Rules:
|
|
30
|
+
|
|
31
|
+
- Do not set it globally. Proportional digits read better in prose.
|
|
32
|
+
- The font must ship tabular figures, or nothing changes.
|
|
33
|
+
|
|
34
|
+
Support: Chrome 52, Firefox 34, Safari 9.1.
|
|
35
|
+
|
|
36
|
+
- Borrowed from: Rauno Freiberg, [Web Interface Guidelines](https://interfaces.rauno.me/) · [Vercel Web Interface Guidelines](https://vercel.com/design/guidelines)
|
|
37
|
+
|
|
38
|
+
Source: [Good CSS](https://good-css.com/specimen/tabular-numbers), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#tabular-numbers).
|
|
39
|
+
|
|
40
|
+
Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: "good-css-textarea-that-grows-with-its-content"
|
|
3
|
+
title: "Textarea that grows with its content"
|
|
4
|
+
tier: "reference"
|
|
5
|
+
tags:
|
|
6
|
+
- "good-css"
|
|
7
|
+
- "interaction"
|
|
8
|
+
- "textarea"
|
|
9
|
+
- "grows"
|
|
10
|
+
- "content"
|
|
11
|
+
summary: "Use it for any multi-line input, such as a chat composer or a comment box. `field-sizing: content` sizes the textarea to its text, so there is no auto-grow script and no hidden mirror element."
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
|
|
15
|
+
# Textarea that grows with its content
|
|
16
|
+
|
|
17
|
+
Use it for any multi-line input, such as a chat composer or a comment box. `field-sizing: content` sizes the textarea to its text, so there is no auto-grow script and no hidden mirror element.
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
textarea {
|
|
21
|
+
field-sizing: content;
|
|
22
|
+
min-height: 3lh;
|
|
23
|
+
max-height: 12lh;
|
|
24
|
+
resize: none;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The height snaps to each new line. To animate it, wrap the textarea and let a `ResizeObserver` copy its height onto the wrapper. The wrapper transitions.
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<div class="field"><textarea></textarea></div>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```css
|
|
35
|
+
.field {
|
|
36
|
+
box-sizing: content-box;
|
|
37
|
+
overflow: clip;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
41
|
+
.field { transition: height 0.2s var(--ease-out, ease-out); }
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
const textarea = document.querySelector(".field textarea");
|
|
47
|
+
|
|
48
|
+
new ResizeObserver(([entry]) => {
|
|
49
|
+
textarea.parentElement.style.height = `${entry.borderBoxSize[0].blockSize}px`;
|
|
50
|
+
}).observe(textarea);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Why it works:
|
|
54
|
+
|
|
55
|
+
- The browser measures the text itself, so the size is right on first paint, on paste and on window resize.
|
|
56
|
+
- The textarea reaches its final height at once. Only the wrapper animates, and it clips the textarea until it catches up.
|
|
57
|
+
- `lh` is one line at the element's own line-height. `min-height: 3lh` means three lines at any font size.
|
|
58
|
+
|
|
59
|
+
Rules:
|
|
60
|
+
|
|
61
|
+
- Bound it with `min-height` and `max-height`. A fixed `height` brings the fixed size back. Past `max-height` the textarea scrolls.
|
|
62
|
+
- Put the border and background on the wrapper and leave the textarea bare. The box the user sees must be the one that animates.
|
|
63
|
+
- Keep the wrapper `content-box`. The observer writes the textarea's full height, and a `border-box` wrapper would subtract its own border from it.
|
|
64
|
+
- Write no fallback. A browser without `field-sizing` shows a fixed textarea that scrolls.
|
|
65
|
+
|
|
66
|
+
Support: Chrome 123, Safari 26.2, Firefox 152. Baseline newly available since June 2026.
|
|
67
|
+
|
|
68
|
+
- Borrowed from: Jhey Tompkins, @jh3yy on X, 2026-10-01
|
|
69
|
+
- Docs: [MDN: field-sizing](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/field-sizing) · Adam Argyle, [CSS field-sizing](https://developer.chrome.com/docs/css-ui/css-field-sizing) · [MDN: ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
|
|
70
|
+
|
|
71
|
+
Source: [Good CSS](https://good-css.com/specimen/textarea-that-grows-with-its-content), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#textarea-that-grows-with-its-content).
|
|
72
|
+
|
|
73
|
+
Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: "good-css-the-reset"
|
|
3
|
+
title: "The reset"
|
|
4
|
+
tier: "reference"
|
|
5
|
+
tags:
|
|
6
|
+
- "good-css"
|
|
7
|
+
- "foundations"
|
|
8
|
+
- "reset"
|
|
9
|
+
summary: "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."
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
|
|
13
|
+
# The reset
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
*,
|
|
19
|
+
*::before,
|
|
20
|
+
*::after {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
min-width: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
:root {
|
|
26
|
+
interpolate-size: allow-keywords;
|
|
27
|
+
scrollbar-gutter: stable;
|
|
28
|
+
text-wrap: pretty;
|
|
29
|
+
overflow-wrap: break-word;
|
|
30
|
+
font-synthesis: none;
|
|
31
|
+
-webkit-font-smoothing: antialiased;
|
|
32
|
+
-moz-osx-font-smoothing: grayscale;
|
|
33
|
+
-webkit-text-size-adjust: 100%;
|
|
34
|
+
-webkit-tap-highlight-color: transparent;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
body {
|
|
38
|
+
margin: 0;
|
|
39
|
+
min-height: 100svh;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
h1, h2, h3, h4 { text-wrap: balance; }
|
|
43
|
+
|
|
44
|
+
img, svg, video {
|
|
45
|
+
display: block;
|
|
46
|
+
max-width: 100%;
|
|
47
|
+
height: auto;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
input, textarea, select { font-size: max(16px, 1rem); }
|
|
51
|
+
|
|
52
|
+
button, a, [role="button"] { touch-action: manipulation; }
|
|
53
|
+
|
|
54
|
+
button, [role="button"] {
|
|
55
|
+
user-select: none;
|
|
56
|
+
-webkit-user-select: none;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Why it works:
|
|
61
|
+
|
|
62
|
+
- `min-width: 0` lets flex and grid items shrink below their content. Their default is `min-width: auto`, which is why a long word or a wide child overflows a grid.
|
|
63
|
+
- `interpolate-size: allow-keywords` turns on transitions to and from `auto` for the whole page. [Accordion that animates its height](https://good-css.com/#accordion-that-animates-its-height) depends on it.
|
|
64
|
+
- `text-wrap: pretty` tells the browser to spend more effort on line breaks. It avoids a last line that holds one short word, and Safari also evens out the ragged edge. The property inherits, so `:root` reaches every element.
|
|
65
|
+
- `text-wrap: balance` gives headings lines of similar length.
|
|
66
|
+
- `font-synthesis: none` stops the browser from faking a bold or italic that the font file lacks.
|
|
67
|
+
- The two smoothing rules switch macOS from subpixel to grayscale antialiasing. Text renders thinner and matches the weight shown in Figma. Other systems ignore them.
|
|
68
|
+
- `100svh` is the viewport height with the mobile browser toolbars showing, so the page never sits behind them. `100dvh` changes as the toolbar moves and re-lays out the page each time.
|
|
69
|
+
- `scrollbar-gutter: stable` reserves the scrollbar's space. The page does not shift sideways when a modal locks scrolling or when content grows past one screen.
|
|
70
|
+
- `overflow-wrap: break-word` breaks a word that cannot fit on a line, such as a URL, where it would otherwise run out of its box. [Long text that wraps, truncates or clamps](https://good-css.com/#long-text-that-wraps-truncates-or-clamps) has the rest.
|
|
71
|
+
- `-webkit-text-size-adjust: 100%` stops iOS from inflating text in landscape.
|
|
72
|
+
- `-webkit-tap-highlight-color: transparent` removes the gray flash iOS and Android paint over a tapped element.
|
|
73
|
+
- Inputs at 16px or more stop iOS Safari from zooming the page on focus. It does not zoom back out.
|
|
74
|
+
- `touch-action: manipulation` tells the browser the element never double-tap zooms, so `click` fires without the wait.
|
|
75
|
+
- `user-select: none` on controls stops a long press from selecting a button's label.
|
|
76
|
+
|
|
77
|
+
Rules:
|
|
78
|
+
|
|
79
|
+
- Load every weight and style the design uses. With `font-synthesis: none` a missing bold renders as regular.
|
|
80
|
+
- 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`.
|
|
81
|
+
- The reset removes the tap highlight, so every pressable element needs its own `:active` state. [Press feedback](https://good-css.com/#press-feedback) has it.
|
|
82
|
+
- Never fix input zoom with `user-scalable=no` or `maximum-scale=1`. That takes zoom away from people who need it.
|
|
83
|
+
- Keep `user-select: none` to controls. Never set it on `body` or on links, because people copy text.
|
|
84
|
+
- Text the user types inherits `pretty`, in `textarea` and `contenteditable`. If lines shift while typing, set `text-wrap: stable` on that element.
|
|
85
|
+
- The reserved gutter puts centered content slightly off-center on a page too short to scroll. Chris Coyier documents this. Accept it.
|
|
86
|
+
- `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.
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
- Borrowed from: Travis Arnold, @souporserious on X, 2025-04-02, "2025 CSS Reset" · Ayomidé Daniel, @aydahnizzy on X, 2025-10-10, for the font rules · `text-wrap: pretty` on everything is a house rule · Kevin Powell, [3 modern CSS properties to add to your reset](https://www.youtube.com/watch?v=qI5rXLJnxco), 2026-04-02, for `svh` and the gutter · Emil Kowalski, [mobile-native skill](https://github.com/emilkowalski/skills/blob/main/skills/mobile-native/SKILL.md), for the touch rules · Chris Coyier, [The Downsides of scrollbar-gutter: stable](https://blog.master.dev/the-downsides-of-scrollbar-gutter-stable-and-one-weird-trick/), 2025-12-03 · Ahmad Shadeed, [Defensive CSS](https://defensivecss.dev/), for `overflow-wrap` and the `flex: none` rule
|
|
91
|
+
- Background: Jen Simmons, [Better typography with text-wrap pretty](https://webkit.org/blog/16547/better-typography-with-text-wrap-pretty/) · Adam Argyle, [CSS text-wrap: pretty](https://developer.chrome.com/blog/css-text-wrap-pretty)
|
|
92
|
+
- Docs: [MDN: text-wrap](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/text-wrap) · [MDN: font-synthesis](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-synthesis) · [MDN: font-smooth](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-smooth)
|
|
93
|
+
|
|
94
|
+
Source: [Good CSS](https://good-css.com/specimen/the-reset), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#the-reset).
|
|
95
|
+
|
|
96
|
+
Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: "good-css-transition-a-custom-property-with-property"
|
|
3
|
+
title: "Transition a custom property with @property"
|
|
4
|
+
tier: "reference"
|
|
5
|
+
tags:
|
|
6
|
+
- "good-css"
|
|
7
|
+
- "motion"
|
|
8
|
+
- "transition"
|
|
9
|
+
- "custom"
|
|
10
|
+
- "property"
|
|
11
|
+
summary: "Use it when one state change should drive several values together, or when a script feeds a number such as pointer position or progress into CSS and the result should ease."
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
|
|
15
|
+
# Transition a custom property with `@property`
|
|
16
|
+
|
|
17
|
+
Use it when one state change should drive several values together, or when a script feeds a number such as pointer position or progress into CSS and the result should ease.
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
@property --progress {
|
|
21
|
+
syntax: "<number>";
|
|
22
|
+
inherits: true;
|
|
23
|
+
initial-value: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.card:focus-within { --progress: 1; }
|
|
27
|
+
|
|
28
|
+
@media (hover: hover) and (pointer: fine) {
|
|
29
|
+
.card:hover { --progress: 1; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.card .icon {
|
|
33
|
+
display: inline-block;
|
|
34
|
+
rotate: calc(var(--progress) * 90deg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.card .bar {
|
|
38
|
+
transform-origin: left;
|
|
39
|
+
scale: calc(0.25 + var(--progress) * 0.75) 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.card .more { opacity: var(--progress); }
|
|
43
|
+
|
|
44
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
45
|
+
.card { transition: --progress 0.3s var(--ease-out, ease-out); }
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Why it works:
|
|
50
|
+
|
|
51
|
+
- An unregistered custom property is a string, and a string cannot be interpolated. Registering it with a `syntax` gives it a type, and the browser then transitions it like any number.
|
|
52
|
+
- One transition with one easing drives every dependent value. The children derive theirs with `calc()`, so the parts stay in step.
|
|
53
|
+
- `inherits: true` passes the number down to the children that use it.
|
|
54
|
+
|
|
55
|
+
Rules:
|
|
56
|
+
|
|
57
|
+
- `initial-value` is required for every syntax except `"*"`.
|
|
58
|
+
- `rotate`, `scale` and `translate` do nothing on an inline element. Give the element `display: inline-block` or another box type first.
|
|
59
|
+
- A registration is global. Register each name once and keep the names specific.
|
|
60
|
+
- A script only writes the variable, as in `el.style.setProperty("--x", "40%")`. The easing and duration stay in CSS.
|
|
61
|
+
- Other useful types are `<length-percentage>` for a gradient position and `<angle>` for a conic gradient.
|
|
62
|
+
|
|
63
|
+
Support: Chrome 85, Firefox 128, Safari 16.4.
|
|
64
|
+
|
|
65
|
+
- Borrowed from: Stripe, on the stripe.com home page and its sales contact page
|
|
66
|
+
- Docs: [MDN: @property](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@property)
|
|
67
|
+
|
|
68
|
+
Source: [Good CSS](https://good-css.com/specimen/transition-a-custom-property-with-property), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#transition-a-custom-property-with-property).
|
|
69
|
+
|
|
70
|
+
Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: "good-css-whole-card-clickable-from-one-link"
|
|
3
|
+
title: "Whole card clickable from one link"
|
|
4
|
+
tier: "reference"
|
|
5
|
+
tags:
|
|
6
|
+
- "good-css"
|
|
7
|
+
- "interaction"
|
|
8
|
+
- "whole"
|
|
9
|
+
- "card"
|
|
10
|
+
- "clickable"
|
|
11
|
+
- "one"
|
|
12
|
+
- "link"
|
|
13
|
+
summary: "Use it for cards, list rows and tiles where the whole area should navigate. It replaces an `<a>` wrapped around the card, which makes a screen reader read every word as the link text, and it replaces a click handler on a `div`."
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
|
|
17
|
+
# Whole card clickable from one link
|
|
18
|
+
|
|
19
|
+
Use it for cards, list rows and tiles where the whole area should navigate. It replaces an `<a>` wrapped around the card, which makes a screen reader read every word as the link text, and it replaces a click handler on a `div`.
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<article class="card">
|
|
23
|
+
<h3><a class="card-link" href="/reports/q3">Quarterly report</a></h3>
|
|
24
|
+
<p>Revenue grew in every region.</p>
|
|
25
|
+
<button type="button">Save</button>
|
|
26
|
+
</article>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```css
|
|
30
|
+
.card { position: relative; }
|
|
31
|
+
|
|
32
|
+
.card-link::after {
|
|
33
|
+
content: "";
|
|
34
|
+
position: absolute;
|
|
35
|
+
inset: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.card-link:focus-visible { outline: 2px solid transparent; }
|
|
39
|
+
|
|
40
|
+
.card:has(.card-link:focus-visible) {
|
|
41
|
+
outline: 2px solid currentColor;
|
|
42
|
+
outline-offset: 2px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.card :is(button, a:not(.card-link)) {
|
|
46
|
+
position: relative;
|
|
47
|
+
z-index: 1;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Why it works:
|
|
52
|
+
|
|
53
|
+
- The pseudo-element belongs to the link and covers the card, so a click anywhere activates the link. It is a real link, so open in new tab and the status bar URL still work.
|
|
54
|
+
- The link text stays short. A screen reader announces "Quarterly report" and not the whole card.
|
|
55
|
+
- `:has()` moves the focus ring from the small link to the card it activates.
|
|
56
|
+
- Other controls sit above the overlay and keep their own clicks.
|
|
57
|
+
|
|
58
|
+
Rules:
|
|
59
|
+
|
|
60
|
+
- No element between the card and the link may be positioned. The overlay would size itself to that element.
|
|
61
|
+
- The link's own ring is made transparent, never removed, as [One focus ring with `:focus-visible`](https://good-css.com/#one-focus-ring-with-focus-visible) requires. Write the full `2px solid transparent`. Safari ignores `outline-color` on its default ring.
|
|
62
|
+
- Text under the overlay cannot be selected. Accept it.
|
|
63
|
+
- Put hover styles on `.card:hover`, inside the query from [Hover styles only where hover exists](https://good-css.com/#hover-styles-only-where-hover-exists).
|
|
64
|
+
|
|
65
|
+
Support: Chrome 105, Firefox 121, Safari 15.4.
|
|
66
|
+
|
|
67
|
+
- Borrowed from: Heydon Pickering, [Cards](https://inclusive-components.design/cards/), for the stretched `::after` · Stripe, for moving the focus ring to the card with `:has()`
|
|
68
|
+
|
|
69
|
+
Source: [Good CSS](https://good-css.com/specimen/whole-card-clickable-from-one-link), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#whole-card-clickable-from-one-link).
|
|
70
|
+
|
|
71
|
+
Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: "good-css-upstream-guidance"
|
|
3
|
+
title: "Good CSS scope and exclusions"
|
|
4
|
+
tier: "reference"
|
|
5
|
+
tags:
|
|
6
|
+
- "good-css"
|
|
7
|
+
- "scope"
|
|
8
|
+
- "exclusions"
|
|
9
|
+
summary: "Upstream principles, intentionally excluded techniques, deferred ideas, and references."
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# good-css
|
|
13
|
+
|
|
14
|
+
Strong opinions about modern CSS technique, borrowed from experts, for agents to know and reach for.
|
|
15
|
+
|
|
16
|
+
Each one is described in plain CSS because that is the common language. None of them depends on a class name, a file layout or a framework: the properties and values are the technique. Write them in whatever the project already uses, whether that is a stylesheet, Tailwind utilities or StyleX objects. Names in the examples are illustrative.
|
|
17
|
+
|
|
18
|
+
The bias throughout: one declaration that adapts on its own beats a set of breakpoints, and a CSS feature beats a script. JavaScript appears only where it makes the result nicer, and the CSS works without it.
|
|
19
|
+
|
|
20
|
+
Every entry has the same shape: when to use it, the CSS, why it works, the rules, and who it is borrowed from.
|
|
21
|
+
|
|
22
|
+
## Foundations
|
|
23
|
+
|
|
24
|
+
## Layout
|
|
25
|
+
|
|
26
|
+
## Spacing and shape
|
|
27
|
+
|
|
28
|
+
## Text and media
|
|
29
|
+
|
|
30
|
+
## Interaction
|
|
31
|
+
|
|
32
|
+
## Motion
|
|
33
|
+
|
|
34
|
+
## Show and hide
|
|
35
|
+
|
|
36
|
+
## Scroll and viewport
|
|
37
|
+
|
|
38
|
+
## Left out on purpose
|
|
39
|
+
|
|
40
|
+
Decided on 2026-10-01. Do not add these back.
|
|
41
|
+
|
|
42
|
+
- **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](https://good-css.com/#content-grid-with-breakouts) and any component that styles a `div`.
|
|
43
|
+
- **`* { 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`](https://good-css.com/#label-centered-on-its-letters-with-text-box) is the scoped version, for labels only.
|
|
44
|
+
- **`text-rendering: optimizeLegibility`.** MDN recommends `auto` for body text.
|
|
45
|
+
|
|
46
|
+
## Next
|
|
47
|
+
|
|
48
|
+
Held for later:
|
|
49
|
+
|
|
50
|
+
- Scroll-driven reveal with `animation-timeline: view()`. Chrome and Safari 26 have it. Firefox does not.
|
|
51
|
+
- Customizable `<select>` with `appearance: base-select`. Safari 27 shipped it in September 2026 and Firefox has it behind a flag.
|
|
52
|
+
- One `box-shadow` built from independent layers, from Stripe's docs site. Marked maybe on 2026-10-01.
|
|
53
|
+
|
|
54
|
+
## References
|
|
55
|
+
|
|
56
|
+
Cloned into `refs/`, which git ignores. They drive decisions here and are not copied wholesale.
|
|
57
|
+
|
|
58
|
+
How to write a skill, for when this list becomes one:
|
|
59
|
+
|
|
60
|
+
- Matt Pocock, [mattpocock/skills](https://github.com/mattpocock/skills) at `refs/mattpocock-skills`
|
|
61
|
+
- poteto, [cursor/plugins, pstack/skills](https://github.com/cursor/plugins/tree/main/pstack/skills) at `refs/cursor-plugins/pstack/skills`
|
|
62
|
+
- Emil Kowalski, [emilkowalski/skills](https://github.com/emilkowalski/skills) at `refs/emilkowalski-skills`
|
|
63
|
+
|
|
64
|
+
What comes from Emil Kowalski, and what stays with him:
|
|
65
|
+
|
|
66
|
+
- This list borrows the declarations every project should ship: [Motion tokens](https://good-css.com/#motion-tokens), [Press feedback](https://good-css.com/#press-feedback), the touch rules in [The reset](https://good-css.com/#the-reset), [Content clear of the notch](https://good-css.com/#content-clear-of-the-notch) and [Hover styles only where hover exists](https://good-css.com/#hover-styles-only-where-hover-exists).
|
|
67
|
+
- His skills keep the judgment. Use them directly for deciding whether something should animate, designing or reviewing motion, springs, gestures and drag, and checking a web app on a real phone. The ones to reach for are `animate`, `review-animations`, `improve-animations`, `find-animation-opportunities` and `mobile-native`.
|
|
68
|
+
- When the two disagree on a value, his skill wins and this list gets corrected.
|
|
69
|
+
|
|
70
|
+
Source: [Good CSS](https://good-css.com), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md).
|
|
71
|
+
|
|
72
|
+
Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
|
package/manifest.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "daydream.good-css",
|
|
3
|
+
"name": "Good CSS",
|
|
4
|
+
"description": "Pinned CSS techniques and playgrounds that agents read before they author or review CSS.",
|
|
5
|
+
"version": "0.3.0",
|
|
6
|
+
"api": "3.1",
|
|
7
|
+
"contributes": {
|
|
8
|
+
"knowledge": "knowledge",
|
|
9
|
+
"instructions": "Good CSS supplies locally pinned CSS techniques and editable playgrounds. Before authoring or reviewing CSS, read daydream.good-css/procedures.md with knowledge_read, then search for the task's techniques with knowledge_search and read the matching recipes. The procedures link Daydream preview constraints. This plugin registers no lint gates."
|
|
10
|
+
},
|
|
11
|
+
"built": "dist/index.js"
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daydreamui/good-css",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Pinned CSS techniques and playgrounds that agents read before they author or review CSS.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"daydream-plugin"
|
|
8
|
+
],
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/martinbavio/daydream-plugins.git",
|
|
15
|
+
"directory": "plugins/daydream.good-css"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Teaching fixture only; recipe declarations live in the other sheet. */
|
|
2
|
+
* {
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
}
|
|
5
|
+
html {
|
|
6
|
+
background: #f5f4ef;
|
|
7
|
+
color: #26352d;
|
|
8
|
+
font:
|
|
9
|
+
16px/1.5 system-ui,
|
|
10
|
+
sans-serif;
|
|
11
|
+
}
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
padding: 1.5rem;
|
|
15
|
+
}
|
|
16
|
+
h1,
|
|
17
|
+
h2,
|
|
18
|
+
p {
|
|
19
|
+
margin: 0;
|
|
20
|
+
}
|
|
21
|
+
h1 {
|
|
22
|
+
font-size: 1.5rem;
|
|
23
|
+
margin-block-end: 1.5rem;
|
|
24
|
+
}
|
|
25
|
+
h2 {
|
|
26
|
+
font-size: 1.2rem;
|
|
27
|
+
}
|
|
28
|
+
article,
|
|
29
|
+
aside,
|
|
30
|
+
.main,
|
|
31
|
+
.panel {
|
|
32
|
+
padding: 1rem;
|
|
33
|
+
background: #e0e8df;
|
|
34
|
+
}
|
|
35
|
+
section > h2 {
|
|
36
|
+
padding: 1rem;
|
|
37
|
+
background: #d2decd;
|
|
38
|
+
}
|
|
39
|
+
footer {
|
|
40
|
+
padding: 0.5rem;
|
|
41
|
+
background: #c9d8c5;
|
|
42
|
+
}
|
|
43
|
+
.slot {
|
|
44
|
+
margin-block: 1rem;
|
|
45
|
+
}
|
|
46
|
+
.card {
|
|
47
|
+
overflow-wrap: anywhere;
|
|
48
|
+
}
|
|
49
|
+
.card > * {
|
|
50
|
+
margin: 0;
|
|
51
|
+
}
|
|
52
|
+
.cards h2 {
|
|
53
|
+
background: #c9d8c5;
|
|
54
|
+
}
|
|
55
|
+
.cards p {
|
|
56
|
+
background: #eaf0e7;
|
|
57
|
+
}
|