@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.
Files changed (75) hide show
  1. package/README.md +77 -2
  2. package/dist/index.js +2 -0
  3. package/icon.svg +8 -0
  4. package/knowledge/INDEX.md +67 -0
  5. package/knowledge/compatibility.md +33 -0
  6. package/knowledge/examples/container-queries-with-container-units.md +150 -0
  7. package/knowledge/examples/content-grid-with-breakouts.md +161 -0
  8. package/knowledge/examples/intrinsic-grid.md +150 -0
  9. package/knowledge/examples/sidebar-that-wraps-on-its-own.md +156 -0
  10. package/knowledge/examples/subgrid-rows-shared-across-cards.md +154 -0
  11. package/knowledge/procedures.md +37 -0
  12. package/knowledge/recipes/accordion-that-animates-its-height.md +59 -0
  13. package/knowledge/recipes/anchor-targets-that-clear-a-sticky-header.md +45 -0
  14. package/knowledge/recipes/carousel-on-native-scroll.md +87 -0
  15. package/knowledge/recipes/concentric-nested-radius.md +45 -0
  16. package/knowledge/recipes/container-queries-with-container-units.md +52 -0
  17. package/knowledge/recipes/content-clear-of-the-notch.md +45 -0
  18. package/knowledge/recipes/content-grid-with-breakouts.md +69 -0
  19. package/knowledge/recipes/cross-document-view-transitions.md +39 -0
  20. package/knowledge/recipes/enter-and-exit-transitions-from-display-none.md +87 -0
  21. package/knowledge/recipes/fluid-sizes-with-clamp.md +46 -0
  22. package/knowledge/recipes/form-feedback-with-user-invalid.md +45 -0
  23. package/knowledge/recipes/has-for-parent-and-page-state.md +46 -0
  24. package/knowledge/recipes/hit-area-larger-than-the-visual.md +47 -0
  25. package/knowledge/recipes/hover-styles-only-where-hover-exists.md +43 -0
  26. package/knowledge/recipes/icon-sized-by-the-text-beside-it.md +69 -0
  27. package/knowledge/recipes/image-box-that-holds-any-upload.md +67 -0
  28. package/knowledge/recipes/indicator-that-slides-to-the-active-item.md +80 -0
  29. package/knowledge/recipes/intrinsic-grid.md +41 -0
  30. package/knowledge/recipes/label-centered-on-its-letters-with-text-box.md +59 -0
  31. package/knowledge/recipes/logical-properties.md +48 -0
  32. package/knowledge/recipes/long-text-that-wraps-truncates-or-clamps.md +62 -0
  33. package/knowledge/recipes/motion-tokens.md +46 -0
  34. package/knowledge/recipes/no-rubber-band-bounce-on-desktop.md +47 -0
  35. package/knowledge/recipes/oklch-color.md +41 -0
  36. package/knowledge/recipes/one-fluid-scale-for-type-and-space.md +85 -0
  37. package/knowledge/recipes/one-focus-ring-with-focus-visible.md +44 -0
  38. package/knowledge/recipes/one-set-of-color-tokens-for-light-and-dark.md +53 -0
  39. package/knowledge/recipes/opt-in-motion.md +43 -0
  40. package/knowledge/recipes/overflow-clip-over-hidden.md +41 -0
  41. package/knowledge/recipes/popover-anchored-to-its-trigger.md +57 -0
  42. package/knowledge/recipes/press-feedback.md +43 -0
  43. package/knowledge/recipes/push-one-item-away-with-an-auto-margin.md +70 -0
  44. package/knowledge/recipes/reveal-with-clip-path.md +61 -0
  45. package/knowledge/recipes/safe-alignment.md +41 -0
  46. package/knowledge/recipes/scroll-area-between-a-fixed-header-and-footer.md +67 -0
  47. package/knowledge/recipes/section-spacing-that-depends-on-its-neighbors.md +54 -0
  48. package/knowledge/recipes/shadow-change-that-fades-and-does-not-repaint.md +80 -0
  49. package/knowledge/recipes/sidebar-that-wraps-on-its-own.md +56 -0
  50. package/knowledge/recipes/space-between-siblings-set-by-the-parent.md +59 -0
  51. package/knowledge/recipes/stack-layers-with-grid.md +43 -0
  52. package/knowledge/recipes/styles-that-apply-only-when-a-scroller-overflows.md +99 -0
  53. package/knowledge/recipes/subgrid-rows-shared-across-cards.md +51 -0
  54. package/knowledge/recipes/tabular-numbers.md +40 -0
  55. package/knowledge/recipes/textarea-that-grows-with-its-content.md +73 -0
  56. package/knowledge/recipes/the-reset.md +96 -0
  57. package/knowledge/recipes/transition-a-custom-property-with-property.md +70 -0
  58. package/knowledge/recipes/whole-card-clickable-from-one-link.md +71 -0
  59. package/knowledge/upstream-guidance.md +72 -0
  60. package/manifest.json +12 -0
  61. package/package.json +15 -4
  62. package/playgrounds/base.css +57 -0
  63. package/playgrounds/container-queries-with-container-units.css +10 -0
  64. package/playgrounds/container-queries-with-container-units.html +13 -0
  65. package/playgrounds/content-grid-with-breakouts.css +21 -0
  66. package/playgrounds/content-grid-with-breakouts.html +13 -0
  67. package/playgrounds/fixtures.json +32 -0
  68. package/playgrounds/intrinsic-grid.css +5 -0
  69. package/playgrounds/intrinsic-grid.html +19 -0
  70. package/playgrounds/sidebar-that-wraps-on-its-own.css +16 -0
  71. package/playgrounds/sidebar-that-wraps-on-its-own.html +13 -0
  72. package/playgrounds/subgrid-rows-shared-across-cards.css +12 -0
  73. package/playgrounds/subgrid-rows-shared-across-cards.html +13 -0
  74. package/upstream/LICENSE +21 -0
  75. package/upstream/lock.json +7 -0
@@ -0,0 +1,57 @@
1
+ ---
2
+ topic: "good-css-popover-anchored-to-its-trigger"
3
+ title: "Popover anchored to its trigger"
4
+ tier: "reference"
5
+ tags:
6
+ - "good-css"
7
+ - "show"
8
+ - "hide"
9
+ - "popover"
10
+ - "anchored"
11
+ - "trigger"
12
+ summary: "Use it for dropdown menus and other popovers opened by a click. It replaces a positioning library or a script that reads `getBoundingClientRect()`."
13
+ ---
14
+
15
+ <!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
16
+ # Popover anchored to its trigger
17
+
18
+ Use it for dropdown menus and other popovers opened by a click. It replaces a positioning library or a script that reads `getBoundingClientRect()`.
19
+
20
+ ```html
21
+ <button popovertarget="menu">Options</button>
22
+ <div id="menu" popover>
23
+ <button>Rename</button>
24
+ <button>Delete</button>
25
+ </div>
26
+ ```
27
+
28
+ ```css
29
+ @supports (position-area: block-end) {
30
+ [popover] {
31
+ margin: 0;
32
+ margin-block-start: 0.5rem;
33
+ position-area: block-end span-inline-end;
34
+ position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
35
+ }
36
+ }
37
+ ```
38
+
39
+ Why it works:
40
+
41
+ - `popovertarget` makes the button the popover's anchor, so no anchor names are needed.
42
+ - `position-area` places the popover on a 3×3 grid around the anchor. The fallbacks flip it when it would overflow the viewport.
43
+
44
+ Rules:
45
+
46
+ - Use `span-*` values, not `center`. Adam Argyle found that `center` stops the flips when the popover is wider than its grid cell.
47
+ - `margin-block-start` is the gap between trigger and popover. The flip mirrors it, so the gap stays on the trigger's side.
48
+ - A popover opened by a script has no anchor and lands in the top-left corner. Pass the trigger, as in `menu.showPopover({ source: button })`.
49
+ - Write no fallback. Without support the `@supports` block is skipped and the popover opens centered in the viewport.
50
+
51
+ Support: Chrome 133, Firefox 147, Safari 26.
52
+
53
+ - Borrowed from: Una Kravets, [Introducing the CSS anchor positioning API](https://developer.chrome.com/blog/anchor-positioning-api), 2024-05-10 · Saron Yitbarek, [A gentle introduction to anchor positioning](https://webkit.org/blog/17240/a-gentle-introduction-to-anchor-positioning/), 2025-08-12 · Adam Argyle, [Why isn't my position-try-fallback working in small spaces?](https://nerdy.dev/why-isnt-my-position-try-fallback-working-in-small-spaces), 2024-10-12
54
+
55
+ Source: [Good CSS](https://good-css.com/specimen/popover-anchored-to-its-trigger), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#popover-anchored-to-its-trigger).
56
+
57
+ Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
@@ -0,0 +1,43 @@
1
+ ---
2
+ topic: "good-css-press-feedback"
3
+ title: "Press feedback"
4
+ tier: "reference"
5
+ tags:
6
+ - "good-css"
7
+ - "interaction"
8
+ - "press"
9
+ - "feedback"
10
+ summary: "Use it on every button and anything else that can be pressed."
11
+ ---
12
+
13
+ <!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
14
+ # Press feedback
15
+
16
+ Use it on every button and anything else that can be pressed.
17
+
18
+ ```css
19
+ .button:active { transform: scale(0.97); }
20
+
21
+ @media (prefers-reduced-motion: no-preference) {
22
+ .button { transition: transform 160ms var(--ease-out, ease-out); }
23
+ }
24
+ ```
25
+
26
+ Why it works:
27
+
28
+ - A native button responds the instant a finger lands. `:active` does the same on every kind of input. Feedback that waits for `click` arrives when the finger leaves, and that reads as lag.
29
+ - `scale()` shrinks the label and icon along with the button, so the whole control reads as pressed.
30
+
31
+ Rules:
32
+
33
+ - Keep the scale between 0.95 and 0.98.
34
+ - [The reset](https://good-css.com/#the-reset) removes the browser's tap highlight. Without this entry a tap gives no feedback at all.
35
+ - When feedback needs a script, listen for `pointerdown`, not `click`.
36
+
37
+ Support: every browser.
38
+
39
+ - Borrowed from: Emil Kowalski, [emil-design-eng skill](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) and [mobile-native skill](https://github.com/emilkowalski/skills/blob/main/skills/mobile-native/SKILL.md)
40
+
41
+ Source: [Good CSS](https://good-css.com/specimen/press-feedback), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#press-feedback).
42
+
43
+ Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
@@ -0,0 +1,70 @@
1
+ ---
2
+ topic: "good-css-push-one-item-away-with-an-auto-margin"
3
+ title: "Push one item away with an auto margin"
4
+ tier: "reference"
5
+ tags:
6
+ - "good-css"
7
+ - "spacing"
8
+ - "shape"
9
+ - "push"
10
+ - "one"
11
+ - "item"
12
+ - "away"
13
+ - "auto"
14
+ - "margin"
15
+ summary: "Use it when one item in a flex row or column sits apart from the rest, such as the actions at the bottom of a card, the account link at the end of a toolbar, or a title centered in a full-height section between a header and a footer."
16
+ ---
17
+
18
+ <!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
19
+ # Push one item away with an auto margin
20
+
21
+ Use it when one item in a flex row or column sits apart from the rest, such as the actions at the bottom of a card, the account link at the end of a toolbar, or a title centered in a full-height section between a header and a footer.
22
+
23
+ ```css
24
+ .card {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: 0.5rem;
28
+ }
29
+
30
+ .card > .actions { margin-block-start: auto; }
31
+
32
+ .toolbar {
33
+ display: flex;
34
+ gap: 1rem;
35
+ }
36
+
37
+ .toolbar > .account { margin-inline-start: auto; }
38
+
39
+ .hero {
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: 1rem;
43
+ min-block-size: 100svh;
44
+ }
45
+
46
+ .hero > h1 { margin-block: auto; }
47
+ ```
48
+
49
+ Why it works:
50
+
51
+ - In a flex container an auto margin takes all the free space on its side. One auto margin pushes the item, and everything after it, to the far end.
52
+ - Two opposite auto margins split the free space, so the item sits centered between its neighbors. The header stays at the top and the footer at the bottom, and either can be missing.
53
+ - `gap` still applies. It is the least space the pushed item keeps when there is no free space left.
54
+
55
+ Rules:
56
+
57
+ - The container needs free space. A card has it when a grid row stretches it to match a taller neighbor. A column that is the only child of a taller box needs `block-size: 100%`.
58
+ - The centered item is centered in the space left over, not in the section. A footer with no header pulls the title up by half the footer's height.
59
+ - Write `min-block-size`, never `block-size`. A fixed height cuts off long content.
60
+ - This is a flex technique. In a grid column the spare height goes to the rows first and every child grows.
61
+ - It replaces a spacer element, `space-between` on a container with more than two children, and a wrapper around the group that should stay together.
62
+
63
+ Support: auto margins work wherever flexbox does. `margin-block` in Chrome 87, Firefox 66, Safari 14.1. `svh` in Chrome 108, Firefox 101, Safari 15.4.
64
+
65
+ - Borrowed from: Heydon Pickering and Andy Bell, [The Stack](https://every-layout.dev/layouts/stack/), section "Splitting the stack", and [The Cover](https://every-layout.dev/layouts/cover/), Every Layout. The Cover gives every child a margin and removes it at both ends. `gap` does that in one declaration.
66
+ - Docs: [MDN: Aligning items in a flex container](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Flexible_box_layout/Aligning_items)
67
+
68
+ Source: [Good CSS](https://good-css.com/specimen/push-one-item-away-with-an-auto-margin), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#push-one-item-away-with-an-auto-margin).
69
+
70
+ Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
@@ -0,0 +1,61 @@
1
+ ---
2
+ topic: "good-css-reveal-with-clip-path"
3
+ title: "Reveal with clip-path"
4
+ tier: "reference"
5
+ tags:
6
+ - "good-css"
7
+ - "show"
8
+ - "hide"
9
+ - "reveal"
10
+ - "clip"
11
+ - "path"
12
+ summary: "Use it for dropdowns, menus and panels that open over the page and whose height is not known. It unrolls the panel without animating `height`, and it works in every browser."
13
+ ---
14
+
15
+ <!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
16
+ # Reveal with `clip-path`
17
+
18
+ Use it for dropdowns, menus and panels that open over the page and whose height is not known. It unrolls the panel without animating `height`, and it works in every browser.
19
+
20
+ ```css
21
+ .menu {
22
+ clip-path: inset(0 -3rem 100%);
23
+ visibility: hidden;
24
+ }
25
+
26
+ .menu.is-open {
27
+ clip-path: inset(0 -3rem -3rem);
28
+ visibility: visible;
29
+ }
30
+
31
+ @media (prefers-reduced-motion: no-preference) {
32
+ .menu {
33
+ transition:
34
+ clip-path 0.25s var(--ease-out, ease-out),
35
+ visibility 0.25s;
36
+ }
37
+ }
38
+ ```
39
+
40
+ Why it works:
41
+
42
+ - `inset()` clips from each edge, in the order top, sides, bottom. A bottom value of `100%` hides the whole box. The percentage refers to the element's own height, so nothing has to measure it.
43
+ - A negative inset widens the clip beyond the box. That leaves room for the shadow at the sides and the bottom. The top stays at `0`, so no shadow shows where the menu meets its trigger.
44
+ - Layout never changes. The panel has its full size the whole time and nothing around it moves.
45
+ - The clipped part takes no pointer events, so a closed menu cannot be clicked.
46
+ - `visibility` in the transition switches to `hidden` only when the close has finished. That takes the links out of the tab order.
47
+
48
+ Rules:
49
+
50
+ - The negative inset must be larger than the shadow's blur plus its offset.
51
+ - Use it for panels that overlap the page. A closed panel in the normal flow leaves a gap, so an accordion uses [Accordion that animates its height](https://good-css.com/#accordion-that-animates-its-height).
52
+ - Add `round` with a radius inside `inset()` only when the clip itself should have round corners. The element's own `border-radius` still applies.
53
+
54
+ Support: every browser.
55
+
56
+ - Borrowed from: Stripe, in the navigation menu on stripe.com. The `visibility` part was added for this list.
57
+ - Docs: [MDN: inset()](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/basic-shape/inset)
58
+
59
+ Source: [Good CSS](https://good-css.com/specimen/reveal-with-clip-path), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#reveal-with-clip-path).
60
+
61
+ Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
@@ -0,0 +1,41 @@
1
+ ---
2
+ topic: "good-css-safe-alignment"
3
+ title: "Safe alignment"
4
+ tier: "reference"
5
+ tags:
6
+ - "good-css"
7
+ - "layout"
8
+ - "safe"
9
+ - "alignment"
10
+ summary: "Use it wherever content is centered or end-aligned in a container that can become too small, such as a tab row, a toolbar or a vertically centered modal."
11
+ ---
12
+
13
+ <!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
14
+ # Safe alignment
15
+
16
+ Use it wherever content is centered or end-aligned in a container that can become too small, such as a tab row, a toolbar or a vertically centered modal.
17
+
18
+ ```css
19
+ .tabs {
20
+ display: flex;
21
+ overflow-x: auto;
22
+ justify-content: safe center;
23
+ }
24
+ ```
25
+
26
+ Why it works:
27
+
28
+ - Plain `center` pushes overflow off the start edge, where scrolling cannot reach it. `safe` falls back to `start` once the content overflows.
29
+
30
+ Rules:
31
+
32
+ - It works on every `align-*`, `justify-*` and `place-*` property.
33
+ - Auto margins on the item center it the same way and are safe in every browser that has flexbox. [Push one item away with an auto margin](https://good-css.com/#push-one-item-away-with-an-auto-margin) has them.
34
+
35
+ Support: Chrome 115, Firefox 63, Safari 17.6.
36
+
37
+ - Borrowed from: Temani Afif, [Safe align your content](https://css-tip.com/safe-align/), 2025-06-10
38
+
39
+ Source: [Good CSS](https://good-css.com/specimen/safe-alignment), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#safe-alignment).
40
+
41
+ Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
@@ -0,0 +1,67 @@
1
+ ---
2
+ topic: "good-css-scroll-area-between-a-fixed-header-and-footer"
3
+ title: "Scroll area between a fixed header and footer"
4
+ tier: "reference"
5
+ tags:
6
+ - "good-css"
7
+ - "scroll"
8
+ - "viewport"
9
+ - "area"
10
+ - "between"
11
+ - "fixed"
12
+ - "header"
13
+ - "footer"
14
+ summary: "Use it for the body of a modal, a chat list, a drawer or a sidebar. The panel grows with its content up to a limit, then the middle scrolls and the header and footer stay put."
15
+ ---
16
+
17
+ <!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
18
+ # Scroll area between a fixed header and footer
19
+
20
+ Use it for the body of a modal, a chat list, a drawer or a sidebar. The panel grows with its content up to a limit, then the middle scrolls and the header and footer stay put.
21
+
22
+ ```css
23
+ .panel {
24
+ display: flex;
25
+ flex-direction: column;
26
+ max-block-size: 80dvh;
27
+ }
28
+
29
+ .panel-body {
30
+ flex: 1;
31
+ overflow-y: auto;
32
+ overscroll-behavior: contain;
33
+ scrollbar-gutter: stable;
34
+ }
35
+
36
+ .panel-wrap {
37
+ flex: 1;
38
+ min-block-size: 0;
39
+ display: flex;
40
+ flex-direction: column;
41
+ }
42
+ ```
43
+
44
+ Why it works:
45
+
46
+ - A flex item will not shrink below its content. A scroll container is the exception, with a minimum of zero, so the body shrinks to the space left and scrolls.
47
+ - An element between the panel and the body, such as a form, is not a scroll container. It keeps the height of its content and pushes the footer out of the panel. `min-block-size: 0` gives it the same zero minimum.
48
+ - `auto` shows a scrollbar only when there is overflow.
49
+ - `scrollbar-gutter` does not inherit. The reset's declaration on `:root` covers the page and no inner scroller.
50
+ - `overscroll-behavior: contain` stops the scroll from passing to the page at the end of the list.
51
+
52
+ Rules:
53
+
54
+ - The panel needs a limit, either `max-block-size` or a height. With no limit nothing overflows.
55
+ - `.panel-wrap` is only for an element that sits between the panel and the body. Leave it out when the body is a direct child.
56
+ - A header or footer with a set height shrinks to its content when the body overflows. Give it `flex: none`, or size it with padding.
57
+ - Keep `min-height: 0` out of the reset. On every element it lets each item of a fixed-height column shrink under its own text.
58
+ - The gutter takes the scrollbar's width on a short list as well. That is the price of no shift.
59
+
60
+ Support: `scrollbar-gutter` in Chrome 94, Firefox 97, Safari 18.2. `overscroll-behavior` in Chrome 63, Firefox 59, Safari 16. `dvh` in Chrome 108, Firefox 101, Safari 15.4.
61
+
62
+ - Borrowed from: Ahmad Shadeed, [Defensive CSS](https://defensivecss.dev/), the tips [Scrollbars on demand](https://defensivecss.dev/tip/scrollbar/), [Scrollbar gutter](https://defensivecss.dev/tip/scrollbar-gutter/), [Scroll chaining](https://defensivecss.dev/tip/scroll-chain/) and [Minimum content size in flexbox](https://defensivecss.dev/tip/flexbox-min-content-size/). He gives the four declarations one by one, and the panel around them is added here.
63
+ - Docs: [MDN: scrollbar-gutter](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/scrollbar-gutter) · [MDN: overscroll-behavior](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/overscroll-behavior) · [MDN: min-height](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/min-height)
64
+
65
+ Source: [Good CSS](https://good-css.com/specimen/scroll-area-between-a-fixed-header-and-footer), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#scroll-area-between-a-fixed-header-and-footer).
66
+
67
+ Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
@@ -0,0 +1,54 @@
1
+ ---
2
+ topic: "good-css-section-spacing-that-depends-on-its-neighbors"
3
+ title: "Section spacing that depends on its neighbors"
4
+ tier: "reference"
5
+ tags:
6
+ - "good-css"
7
+ - "spacing"
8
+ - "shape"
9
+ - "section"
10
+ - "depends"
11
+ - "neighbors"
12
+ summary: "Use it on any page built from reorderable sections, above all in a CMS where an editor decides the order. When two particular sections meet, the spacing between them changes on its own."
13
+ ---
14
+
15
+ <!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
16
+ # Section spacing that depends on its neighbors
17
+
18
+ Use it on any page built from reorderable sections, above all in a CMS where an editor decides the order. When two particular sections meet, the spacing between them changes on its own.
19
+
20
+ ```css
21
+ section { padding-block: 6rem; }
22
+
23
+ .logos:has(+ .features) { padding-block-end: 2rem; }
24
+ .features:has(+ .cta) { padding-block-end: 0; }
25
+
26
+ .hero + .logos { padding-block-start: 2rem; }
27
+ .features + .faq { padding-block-start: 3rem; }
28
+
29
+ .logos:where(.hero + *) { border-block-start: 1px solid; }
30
+ ```
31
+
32
+ Why it works:
33
+
34
+ - `A:has(+ B)` looks ahead. It matches A when B follows it, so A adjusts its own bottom edge.
35
+ - `A + B` looks back. It matches B when A precedes it, so B adjusts its own top edge.
36
+ - The rules describe pairs, not pages. Whatever order an editor picks, each pair that meets gets its spacing and every other section keeps its default.
37
+ - `:where()` adds no specificity, so a default that depends on the neighbor stays easy to override.
38
+
39
+ Rules:
40
+
41
+ - A section owns its default padding. Write a pair rule only for a meeting that looks wrong with the defaults.
42
+ - Never fix spacing with a spacer element, a per-page override, or a modifier class the template has to work out.
43
+ - Put the rule on the section whose edge changes. Look ahead to change a bottom edge and look back to change a top edge.
44
+ - Name sections by what they are, such as `.hero` and `.faq`, never by the page they sit on. The pair rules depend on those names.
45
+ - Keep the list short. When many pairs share a reason, such as two sections with the same background, write one rule for the reason.
46
+ - Where the authoring system has no sibling selectors, keep these few rules in a plain stylesheet.
47
+
48
+ Support: `:has()` in Chrome 105, Firefox 121, Safari 15.4. The `+` combinator works everywhere.
49
+
50
+ - Borrowed from: house pattern, used on production CMS sites · Kevin Powell, [The adjacent sibling combinator](https://www.kevinpowell.co/article/the-adjacent-sibling-combinator/) · Jen Simmons, [Using :has() as a CSS Parent Selector and much more](https://webkit.org/blog/13096/css-has-pseudo-class/), 2022-08-18
51
+
52
+ Source: [Good CSS](https://good-css.com/specimen/section-spacing-that-depends-on-its-neighbors), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#section-spacing-that-depends-on-its-neighbors).
53
+
54
+ Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
@@ -0,0 +1,80 @@
1
+ ---
2
+ topic: "good-css-shadow-change-that-fades-and-does-not-repaint"
3
+ title: "Shadow change that fades and does not repaint"
4
+ tier: "reference"
5
+ tags:
6
+ - "good-css"
7
+ - "motion"
8
+ - "shadow"
9
+ - "change"
10
+ - "fades"
11
+ - "does"
12
+ - "not"
13
+ - "repaint"
14
+ summary: "Use it on cards that lift on hover, most of all in a grid of many cards or with a large blur."
15
+ ---
16
+
17
+ <!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
18
+ # Shadow change that fades and does not repaint
19
+
20
+ Use it on cards that lift on hover, most of all in a grid of many cards or with a large blur.
21
+
22
+ ```css
23
+ .card {
24
+ position: relative;
25
+ isolation: isolate;
26
+ }
27
+
28
+ .card::before,
29
+ .card::after {
30
+ content: "";
31
+ position: absolute;
32
+ inset: 0;
33
+ z-index: -1;
34
+ border-radius: inherit;
35
+ pointer-events: none;
36
+ }
37
+
38
+ .card::before {
39
+ box-shadow:
40
+ 0 1px 4px oklch(0.3 0.05 250 / 0.1),
41
+ 0 2px 10px oklch(0.3 0.05 250 / 0.08);
42
+ }
43
+
44
+ .card::after {
45
+ box-shadow:
46
+ 0 4px 8px oklch(0.3 0.05 250 / 0.06),
47
+ 0 12px 32px oklch(0.3 0.05 250 / 0.16);
48
+ opacity: 0;
49
+ }
50
+
51
+ @media (hover: hover) and (pointer: fine) {
52
+ .card:hover::before { opacity: 0; }
53
+ .card:hover::after { opacity: 1; }
54
+ }
55
+
56
+ @media (prefers-reduced-motion: no-preference) {
57
+ .card::before,
58
+ .card::after { transition: opacity 0.3s var(--ease-out, ease-out); }
59
+ }
60
+ ```
61
+
62
+ Why it works:
63
+
64
+ - A `box-shadow` transition repaints the shadow on every frame. Each shadow here is painted once, and the transition changes only `opacity`, which the compositor handles. In a Chrome trace of one card it took 12 paint events, against about 78 for a plain `box-shadow` transition.
65
+ - The two shadows cross-fade, so the end state is exactly the raised shadow and not the sum of both.
66
+ - `isolation: isolate` gives the card its own stacking context. `z-index: -1` then puts the pseudo-elements above the card's background and below its content.
67
+
68
+ Rules:
69
+
70
+ - The card needs its own background. The pseudo-elements have none.
71
+ - It uses both pseudo-elements of the card. [Hit area larger than the visual](https://good-css.com/#hit-area-larger-than-the-visual) uses `::after` for the hit area and [Whole card clickable from one link](https://good-css.com/#whole-card-clickable-from-one-link) uses it for the link, so do not combine them on one element.
72
+ - For a single small element, transition `box-shadow` directly. The saving is real but small per card.
73
+
74
+ Support: every browser.
75
+
76
+ - Borrowed from: Stripe, on its resource cards · Tobias Ahlin, [How to animate box-shadow](https://tobiasahlin.com/blog/how-to-animate-box-shadow/), for the older form of the idea
77
+
78
+ Source: [Good CSS](https://good-css.com/specimen/shadow-change-that-fades-and-does-not-repaint), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#shadow-change-that-fades-and-does-not-repaint).
79
+
80
+ Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
@@ -0,0 +1,56 @@
1
+ ---
2
+ topic: "good-css-sidebar-that-wraps-on-its-own"
3
+ title: "Sidebar that wraps on its own"
4
+ tier: "reference"
5
+ tags:
6
+ - "good-css"
7
+ - "layout"
8
+ - "sidebar"
9
+ - "wraps"
10
+ - "own"
11
+ summary: "Use it for any pair where one side has an ideal width and the other takes the rest, such as a media object, an input with a button, or a page with an aside."
12
+ ---
13
+
14
+ <!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
15
+ # Sidebar that wraps on its own
16
+
17
+ Use it for any pair where one side has an ideal width and the other takes the rest, such as a media object, an input with a button, or a page with an aside.
18
+
19
+ ```css
20
+ .with-sidebar {
21
+ display: flex;
22
+ flex-wrap: wrap;
23
+ gap: 1rem;
24
+ }
25
+
26
+ .sidebar {
27
+ flex-basis: 20rem;
28
+ flex-grow: 1;
29
+ }
30
+
31
+ .main {
32
+ flex-basis: 0;
33
+ flex-grow: 999;
34
+ min-inline-size: 50%;
35
+ }
36
+ ```
37
+
38
+ Why it works:
39
+
40
+ - The main side's huge `flex-grow` takes all the free space, which holds the sidebar at its basis.
41
+ - Once the main side would get less than half the container it wraps below, and both sides grow to full width.
42
+
43
+ Rules:
44
+
45
+ - The wrap point is a share of the container, not a length. Change `min-inline-size` to move it.
46
+ - Drop the sidebar's `flex-basis` for a sidebar sized by its content.
47
+ - For a sticky sidebar, add `align-self: start` beside `position: sticky` and its inset. The two sides stretch to the same height, and a stretched sidebar has no room to stick.
48
+ - A bare `img` or `video` as one of the sides stretches to the height of the row and distorts. Set `align-items: start` on the container.
49
+
50
+ Support: Chrome 84, Firefox 63, Safari 14.1.
51
+
52
+ - Borrowed from: Heydon Pickering and Andy Bell, [The Sidebar](https://every-layout.dev/layouts/sidebar/), Every Layout
53
+
54
+ Source: [Good CSS](https://good-css.com/specimen/sidebar-that-wraps-on-its-own), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#sidebar-that-wraps-on-its-own).
55
+
56
+ Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
@@ -0,0 +1,59 @@
1
+ ---
2
+ topic: "good-css-space-between-siblings-set-by-the-parent"
3
+ title: "Space between siblings set by the parent"
4
+ tier: "reference"
5
+ tags:
6
+ - "good-css"
7
+ - "spacing"
8
+ - "shape"
9
+ - "space"
10
+ - "between"
11
+ - "siblings"
12
+ - "set"
13
+ - "parent"
14
+ summary: "Use it wherever elements sit one above another, such as form fields, the parts of a card or the blocks of an article. The parent sets one space between its children, and no child carries a block margin of its own."
15
+ ---
16
+
17
+ <!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
18
+ # Space between siblings set by the parent
19
+
20
+ Use it wherever elements sit one above another, such as form fields, the parts of a card or the blocks of an article. The parent sets one space between its children, and no child carries a block margin of its own.
21
+
22
+ ```css
23
+ .fields {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: 1rem;
27
+ }
28
+
29
+ .prose > * { margin-block: 0; }
30
+ .prose > * + * { margin-block-start: var(--flow-space, 1em); }
31
+
32
+ .prose > :is(h2, h3) { --flow-space: 2em; }
33
+ .prose > :is(h2, h3) + * { --flow-space: 0.5em; }
34
+ ```
35
+
36
+ Why it works:
37
+
38
+ - A margin describes the relation between two neighbors, so the element itself is the wrong place for it. A paragraph with `margin-block-end` leaves that margin behind when it is the last child of a padded box.
39
+ - `gap` puts space between flex items only. Nothing is left above the first child or below the last.
40
+ - `* + *` matches every child that has a sibling before it, so the first child gets no margin. Heydon Pickering named the selector the lobotomized owl.
41
+ - The fallback in `var(--flow-space, 1em)` is in `em`, so the space above each element follows that element's font size.
42
+ - A custom property on a child changes the space above that child alone. A heading asks for more room above itself, and whatever follows a heading asks for less.
43
+
44
+ Rules:
45
+
46
+ - Use the flex form in a component whose children you know. Use the margin form for content you do not control, such as CMS or Markdown output, because flex turns every child into a flex item.
47
+ - A flex column stretches its children, so a button or link that is a direct child becomes full width. Set `align-self: start` on it.
48
+ - In the margin form, zero the children's block margins first. Without that line the browser's default margins stay above the first child and below the last.
49
+ - Keep the `>`. Without it the rule reaches every nested element, list items included.
50
+ - The margin form counts a hidden child, so a hidden first child leaves a space at the top. `gap` ignores hidden children.
51
+
52
+ Support: `gap` in flex layout in Chrome 84, Firefox 63, Safari 14.1. `:is()` in Chrome 88, Firefox 78, Safari 14.
53
+
54
+ - Borrowed from: Heydon Pickering and Andy Bell, [The Stack](https://every-layout.dev/layouts/stack/), Every Layout · Heydon Pickering, [Axiomatic CSS and Lobotomized Owls](https://alistapart.com/article/axiomatic-css-and-lobotomized-owls/), 2014-10-21 · Andy Bell, [My favourite 3 lines of CSS](https://piccalil.li/blog/my-favourite-3-lines-of-css/), 2023-02-06. Both authors keep the margin form everywhere. This version uses `gap` in components.
55
+ - Docs: [MDN: gap](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/gap) · [MDN: next-sibling combinator](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Next-sibling_combinator)
56
+
57
+ Source: [Good CSS](https://good-css.com/specimen/space-between-siblings-set-by-the-parent), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#space-between-siblings-set-by-the-parent).
58
+
59
+ Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.
@@ -0,0 +1,43 @@
1
+ ---
2
+ topic: "good-css-stack-layers-with-grid"
3
+ title: "Stack layers with grid"
4
+ tier: "reference"
5
+ tags:
6
+ - "good-css"
7
+ - "layout"
8
+ - "stack"
9
+ - "layers"
10
+ - "grid"
11
+ summary: "Use it whenever things sit on top of each other, such as text over an image, a badge on a card, or two icons that swap. It replaces `position: absolute` with its offsets, sizes and transforms."
12
+ ---
13
+
14
+ <!-- Generated by pnpm good-css:sync. Local notes belong in compatibility.md. -->
15
+ # Stack layers with grid
16
+
17
+ Use it whenever things sit on top of each other, such as text over an image, a badge on a card, or two icons that swap. It replaces `position: absolute` with its offsets, sizes and transforms.
18
+
19
+ ```css
20
+ .stack { display: grid; }
21
+ .stack > * { grid-area: 1 / 1; }
22
+
23
+ .stack > .title { place-self: center; }
24
+ .stack > .badge { place-self: start end; }
25
+ ```
26
+
27
+ Why it works:
28
+
29
+ - Every child goes into the same grid cell, so they overlap.
30
+ - The layers stay in flow. The container takes the size of its largest layer, where an absolutely positioned child adds nothing to it.
31
+ - `place-self` aligns one layer inside the cell. Without it the layer stretches to fill.
32
+
33
+ Rules:
34
+
35
+ - A layer later in the DOM paints on top. Use `z-index` only to change that order.
36
+ - Keep `position: absolute` for a layer that must not affect the container's size.
37
+
38
+ - Borrowed from: Ana Tudor, @anatudor on X, 2025-03-29
39
+ - Docs: [MDN: grid-area](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/grid-area) · [MDN: place-self](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/place-self)
40
+
41
+ Source: [Good CSS](https://good-css.com/specimen/stack-layers-with-grid), [pinned source](https://github.com/vojtaholik/good-css/blob/6d16d2fd27f4892e2aea4b5c5c2b016f45be7eef/PRACTICES.md#stack-layers-with-grid).
42
+
43
+ Copyright (c) 2026 Vojta Holik. License: upstream/LICENSE in this plugin.