@hegemonart/get-design-done 1.16.0 → 1.19.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/.claude-plugin/marketplace.json +12 -4
- package/.claude-plugin/plugin.json +22 -4
- package/CHANGELOG.md +111 -0
- package/README.md +27 -2
- package/agents/design-auditor.md +65 -1
- package/agents/design-context-builder.md +6 -1
- package/agents/design-doc-writer.md +21 -0
- package/agents/design-executor.md +22 -4
- package/agents/design-pattern-mapper.md +62 -0
- package/agents/design-phase-researcher.md +1 -1
- package/agents/motion-mapper.md +74 -9
- package/agents/token-mapper.md +8 -0
- package/package.json +16 -2
- package/reference/components/README.md +27 -23
- package/reference/components/alert.md +198 -0
- package/reference/components/badge.md +202 -0
- package/reference/components/breadcrumbs.md +198 -0
- package/reference/components/chip.md +209 -0
- package/reference/components/command-palette.md +228 -0
- package/reference/components/date-picker.md +227 -0
- package/reference/components/file-upload.md +219 -0
- package/reference/components/list.md +217 -0
- package/reference/components/menu.md +212 -0
- package/reference/components/navbar.md +211 -0
- package/reference/components/pagination.md +205 -0
- package/reference/components/progress.md +210 -0
- package/reference/components/rich-text-editor.md +226 -0
- package/reference/components/sidebar.md +211 -0
- package/reference/components/skeleton.md +197 -0
- package/reference/components/slider.md +208 -0
- package/reference/components/stepper.md +220 -0
- package/reference/components/table.md +229 -0
- package/reference/components/toast.md +200 -0
- package/reference/components/tree.md +225 -0
- package/reference/css-grid-layout.md +835 -0
- package/reference/data-visualization.md +333 -0
- package/reference/external/NOTICE.hyperframes +28 -0
- package/reference/form-patterns.md +245 -0
- package/reference/image-optimization.md +582 -0
- package/reference/information-architecture.md +255 -0
- package/reference/motion-advanced.md +754 -0
- package/reference/motion-easings.md +381 -0
- package/reference/motion-interpolate.md +282 -0
- package/reference/motion-spring.md +234 -0
- package/reference/motion-transition-taxonomy.md +155 -0
- package/reference/motion.md +20 -0
- package/reference/onboarding-progressive-disclosure.md +250 -0
- package/reference/output-contracts/motion-map.schema.json +135 -0
- package/reference/platforms.md +346 -0
- package/reference/registry.json +445 -220
- package/reference/registry.schema.json +4 -0
- package/reference/rtl-cjk-cultural.md +353 -0
- package/reference/user-research.md +360 -0
- package/reference/variable-fonts-loading.md +532 -0
- package/scripts/lib/easings.cjs +280 -0
- package/scripts/lib/parse-contract.cjs +220 -0
- package/scripts/lib/spring.cjs +160 -0
- package/scripts/tests/test-motion-provenance.sh +64 -0
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
<!-- Source: Phase 18 — get-design-done -->
|
|
2
|
+
|
|
3
|
+
# CSS Grid Layout — Advanced Craft Reference
|
|
4
|
+
|
|
5
|
+
## 1. CSS Grid Template Patterns
|
|
6
|
+
|
|
7
|
+
### Holy Grail Layout
|
|
8
|
+
|
|
9
|
+
Classic five-area layout: header, nav, main, aside, footer.
|
|
10
|
+
|
|
11
|
+
```css
|
|
12
|
+
.holy-grail {
|
|
13
|
+
display: grid;
|
|
14
|
+
grid-template-areas:
|
|
15
|
+
"header header header"
|
|
16
|
+
"nav main aside"
|
|
17
|
+
"footer footer footer";
|
|
18
|
+
grid-template-columns: 200px 1fr 200px;
|
|
19
|
+
grid-template-rows: auto 1fr auto;
|
|
20
|
+
min-height: 100dvh;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.header { grid-area: header; }
|
|
24
|
+
.nav { grid-area: nav; }
|
|
25
|
+
.main { grid-area: main; }
|
|
26
|
+
.aside { grid-area: aside; }
|
|
27
|
+
.footer { grid-area: footer; }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Responsive collapse — stack columns below 768px:
|
|
31
|
+
|
|
32
|
+
```css
|
|
33
|
+
@media (max-width: 768px) {
|
|
34
|
+
.holy-grail {
|
|
35
|
+
grid-template-areas:
|
|
36
|
+
"header"
|
|
37
|
+
"nav"
|
|
38
|
+
"main"
|
|
39
|
+
"aside"
|
|
40
|
+
"footer";
|
|
41
|
+
grid-template-columns: 1fr;
|
|
42
|
+
grid-template-rows: auto;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Bento Grid Layout
|
|
48
|
+
|
|
49
|
+
Dashboard card mosaic where cards span varied tracks.
|
|
50
|
+
|
|
51
|
+
```css
|
|
52
|
+
.bento {
|
|
53
|
+
display: grid;
|
|
54
|
+
grid-template-columns: repeat(4, 1fr);
|
|
55
|
+
grid-template-rows: repeat(3, 200px);
|
|
56
|
+
gap: 16px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Feature card — spans 2 cols × 2 rows */
|
|
60
|
+
.bento__card--featured {
|
|
61
|
+
grid-column: span 2;
|
|
62
|
+
grid-row: span 2;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Wide card — full width */
|
|
66
|
+
.bento__card--wide {
|
|
67
|
+
grid-column: 1 / -1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Tall card */
|
|
71
|
+
.bento__card--tall {
|
|
72
|
+
grid-row: span 2;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Auto-fit bento with minimum card size:
|
|
77
|
+
|
|
78
|
+
```css
|
|
79
|
+
.bento-auto {
|
|
80
|
+
display: grid;
|
|
81
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
82
|
+
grid-auto-rows: minmax(180px, auto);
|
|
83
|
+
gap: 16px;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Masonry via `grid-template-rows: masonry`
|
|
88
|
+
|
|
89
|
+
Native CSS masonry — items pack into shortest column, no JavaScript.
|
|
90
|
+
|
|
91
|
+
```css
|
|
92
|
+
.masonry {
|
|
93
|
+
display: grid;
|
|
94
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
95
|
+
grid-template-rows: masonry; /* native masonry axis */
|
|
96
|
+
gap: 16px;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Browser support (2025):** Firefox (behind flag), Safari 18+ (partial). Use `@supports` for progressive enhancement:
|
|
101
|
+
|
|
102
|
+
```css
|
|
103
|
+
@supports (grid-template-rows: masonry) {
|
|
104
|
+
.masonry {
|
|
105
|
+
grid-template-rows: masonry;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Fallback: multi-column layout */
|
|
110
|
+
@supports not (grid-template-rows: masonry) {
|
|
111
|
+
.masonry {
|
|
112
|
+
column-count: 3;
|
|
113
|
+
column-gap: 16px;
|
|
114
|
+
}
|
|
115
|
+
.masonry > * {
|
|
116
|
+
break-inside: avoid;
|
|
117
|
+
margin-bottom: 16px;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 2. Subgrid
|
|
125
|
+
|
|
126
|
+
### What It Is
|
|
127
|
+
|
|
128
|
+
`subgrid` lets a nested grid inherit track definitions from its parent grid. Without subgrid, inner grids define their own tracks independently — card headers and footers cannot align across sibling cards.
|
|
129
|
+
|
|
130
|
+
```css
|
|
131
|
+
/* Parent defines the tracks */
|
|
132
|
+
.card-grid {
|
|
133
|
+
display: grid;
|
|
134
|
+
grid-template-columns: repeat(3, 1fr);
|
|
135
|
+
grid-template-rows: auto 1fr auto; /* header, body, footer */
|
|
136
|
+
gap: 24px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Each card participates in parent row tracks */
|
|
140
|
+
.card {
|
|
141
|
+
display: grid;
|
|
142
|
+
grid-row: span 3;
|
|
143
|
+
grid-template-rows: subgrid; /* inherit parent's 3 row tracks */
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.card__header { grid-row: 1; }
|
|
147
|
+
.card__body { grid-row: 2; }
|
|
148
|
+
.card__footer { grid-row: 3; }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### When to Use Subgrid
|
|
152
|
+
|
|
153
|
+
- Card grids where headers, body areas, and CTAs must align across columns
|
|
154
|
+
- Form layouts with labels and inputs spanning parent tracks
|
|
155
|
+
- Table-like layouts built from components
|
|
156
|
+
|
|
157
|
+
### Browser Support
|
|
158
|
+
|
|
159
|
+
Chrome 117+, Firefox 71+, Safari 16+. Baseline widely available as of 2024.
|
|
160
|
+
|
|
161
|
+
```css
|
|
162
|
+
/* Safe to use without @supports for modern browsers */
|
|
163
|
+
/* Fallback pattern for older targets */
|
|
164
|
+
@supports not (grid-template-rows: subgrid) {
|
|
165
|
+
.card {
|
|
166
|
+
display: flex;
|
|
167
|
+
flex-direction: column;
|
|
168
|
+
}
|
|
169
|
+
.card__body {
|
|
170
|
+
flex: 1; /* push footer down */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 3. Container Queries
|
|
178
|
+
|
|
179
|
+
### Core Syntax
|
|
180
|
+
|
|
181
|
+
Query an element's own container rather than the viewport.
|
|
182
|
+
|
|
183
|
+
```css
|
|
184
|
+
/* 1. Define the container */
|
|
185
|
+
.card-wrapper {
|
|
186
|
+
container-type: inline-size;
|
|
187
|
+
container-name: card;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* 2. Query inside */
|
|
191
|
+
@container card (min-width: 400px) {
|
|
192
|
+
.card {
|
|
193
|
+
display: grid;
|
|
194
|
+
grid-template-columns: 120px 1fr;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@container card (min-width: 600px) {
|
|
199
|
+
.card__title {
|
|
200
|
+
font-size: 1.5rem;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### `container-type` Values
|
|
206
|
+
|
|
207
|
+
| Value | Queries |
|
|
208
|
+
|---|---|
|
|
209
|
+
| `inline-size` | Width queries only (most common) |
|
|
210
|
+
| `size` | Width and height queries |
|
|
211
|
+
| `normal` | No containment (default) |
|
|
212
|
+
|
|
213
|
+
### `container-name`
|
|
214
|
+
|
|
215
|
+
Named containers allow nested or specific targeting:
|
|
216
|
+
|
|
217
|
+
```css
|
|
218
|
+
.sidebar {
|
|
219
|
+
container-type: inline-size;
|
|
220
|
+
container-name: sidebar;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.main-content {
|
|
224
|
+
container-type: inline-size;
|
|
225
|
+
container-name: main;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@container sidebar (max-width: 300px) {
|
|
229
|
+
.widget { font-size: 0.875rem; }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@container main (min-width: 700px) {
|
|
233
|
+
.widget { font-size: 1rem; }
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Practical Reusable Component Pattern
|
|
238
|
+
|
|
239
|
+
```css
|
|
240
|
+
/* Component owns its responsiveness */
|
|
241
|
+
.media-card {
|
|
242
|
+
container-type: inline-size;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* Stacked by default */
|
|
246
|
+
.media-card__inner {
|
|
247
|
+
display: flex;
|
|
248
|
+
flex-direction: column;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* Side-by-side when container is wide enough */
|
|
252
|
+
@container (min-width: 480px) {
|
|
253
|
+
.media-card__inner {
|
|
254
|
+
flex-direction: row;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.media-card__image {
|
|
258
|
+
width: 40%;
|
|
259
|
+
flex-shrink: 0;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Browser support:** Chrome 105+, Firefox 110+, Safari 16+. Baseline widely available.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 4. Fluid Typography with `clamp()`
|
|
269
|
+
|
|
270
|
+
### The Utopia.fyi Formula
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
font-size: clamp(min, preferred, max)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Where preferred is a linear interpolation between viewport sizes:
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
preferred = V × 1vw + R × 1rem
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
- `V` = viewport coefficient (controls how fast size scales)
|
|
283
|
+
- `R` = root-relative offset (base size contribution)
|
|
284
|
+
|
|
285
|
+
### Deriving V and R
|
|
286
|
+
|
|
287
|
+
Given: min font size `f_min` at viewport `v_min`, max font size `f_max` at viewport `v_max` (all in px):
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
V = (f_max - f_min) / (v_max - v_min) × 100
|
|
291
|
+
R = f_min - V × (v_min / 100) (convert back to rem ÷ 16)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Real Examples
|
|
295
|
+
|
|
296
|
+
**Body text — 16px at 320px viewport → 20px at 1440px:**
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
V = (20 - 16) / (1440 - 320) × 100 = 0.357
|
|
300
|
+
R = 16 - 0.357 × (320/100) = 16 - 1.143 = 14.857 → ÷16 = 0.929rem
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```css
|
|
304
|
+
body {
|
|
305
|
+
font-size: clamp(1rem, 0.929rem + 0.357vw, 1.25rem);
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Display heading — 32px at 320px → 72px at 1440px:**
|
|
310
|
+
|
|
311
|
+
```css
|
|
312
|
+
h1 {
|
|
313
|
+
font-size: clamp(2rem, 0.286rem + 5.357vw, 4.5rem);
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Practical scale using clamp:**
|
|
318
|
+
|
|
319
|
+
```css
|
|
320
|
+
:root {
|
|
321
|
+
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
|
|
322
|
+
--text-sm: clamp(0.875rem, 0.825rem + 0.25vw, 1rem);
|
|
323
|
+
--text-base: clamp(1rem, 0.929rem + 0.357vw, 1.25rem);
|
|
324
|
+
--text-lg: clamp(1.125rem, 1rem + 0.625vw, 1.5rem);
|
|
325
|
+
--text-xl: clamp(1.25rem, 1rem + 1.25vw, 2rem);
|
|
326
|
+
--text-2xl: clamp(1.5rem, 1rem + 2.5vw, 2.5rem);
|
|
327
|
+
--text-3xl: clamp(1.875rem, 1rem + 4.375vw, 3.5rem);
|
|
328
|
+
--text-4xl: clamp(2.25rem, 0.75rem + 7.5vw, 5rem);
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Fluid spacing with clamp:**
|
|
333
|
+
|
|
334
|
+
```css
|
|
335
|
+
:root {
|
|
336
|
+
--space-s: clamp(0.75rem, 0.25vw + 0.68rem, 1rem);
|
|
337
|
+
--space-m: clamp(1rem, 0.5vw + 0.87rem, 1.5rem);
|
|
338
|
+
--space-l: clamp(1.5rem, 1vw + 1.25rem, 2.5rem);
|
|
339
|
+
--space-xl: clamp(2rem, 2vw + 1.5rem, 4rem);
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 5. Intrinsic Sizing in Grid Track Definitions
|
|
346
|
+
|
|
347
|
+
### Keywords
|
|
348
|
+
|
|
349
|
+
| Keyword | Behavior |
|
|
350
|
+
|---|---|
|
|
351
|
+
| `min-content` | Shrinks to smallest content size (longest unbreakable word) |
|
|
352
|
+
| `max-content` | Grows to fit all content on one line |
|
|
353
|
+
| `fit-content(N)` | Like `max-content` but clamps at `N` |
|
|
354
|
+
| `auto` | Distributes remaining space, respects min/max |
|
|
355
|
+
| `fr` | Fractional remaining space after fixed tracks |
|
|
356
|
+
|
|
357
|
+
### Usage in Grid
|
|
358
|
+
|
|
359
|
+
```css
|
|
360
|
+
.layout {
|
|
361
|
+
display: grid;
|
|
362
|
+
|
|
363
|
+
/* Label column: as wide as needed, content column: takes rest */
|
|
364
|
+
grid-template-columns: max-content 1fr;
|
|
365
|
+
|
|
366
|
+
/* Sidebar: shrinks to content, main: fills */
|
|
367
|
+
grid-template-columns: min-content 1fr;
|
|
368
|
+
|
|
369
|
+
/* Tag list: constrained but wraps if narrow */
|
|
370
|
+
grid-template-columns: fit-content(200px) 1fr;
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Auto-sizing Rows
|
|
375
|
+
|
|
376
|
+
```css
|
|
377
|
+
.card-grid {
|
|
378
|
+
grid-auto-rows: minmax(min-content, auto);
|
|
379
|
+
/* Each row is at least as tall as its shortest content,
|
|
380
|
+
and grows to fit the tallest cell */
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### `minmax()` Patterns
|
|
385
|
+
|
|
386
|
+
```css
|
|
387
|
+
/* Never narrower than 250px, fills available space */
|
|
388
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
389
|
+
|
|
390
|
+
/* Text column: at least 45 characters wide */
|
|
391
|
+
grid-template-columns: minmax(45ch, 1fr) 300px;
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## 6. Logical Properties
|
|
397
|
+
|
|
398
|
+
### Why They Matter
|
|
399
|
+
|
|
400
|
+
Physical properties (`margin-left`, `padding-top`) break when:
|
|
401
|
+
- Language direction is RTL (Arabic, Hebrew, Farsi)
|
|
402
|
+
- Writing mode is vertical (Japanese, Chinese traditional)
|
|
403
|
+
|
|
404
|
+
Logical properties map to flow-relative directions.
|
|
405
|
+
|
|
406
|
+
### Property Mapping
|
|
407
|
+
|
|
408
|
+
| Physical | Logical |
|
|
409
|
+
|---|---|
|
|
410
|
+
| `margin-left` | `margin-inline-start` |
|
|
411
|
+
| `margin-right` | `margin-inline-end` |
|
|
412
|
+
| `margin-top` | `margin-block-start` |
|
|
413
|
+
| `margin-bottom` | `margin-block-end` |
|
|
414
|
+
| `padding-left/right` | `padding-inline` |
|
|
415
|
+
| `padding-top/bottom` | `padding-block` |
|
|
416
|
+
| `width` | `inline-size` |
|
|
417
|
+
| `height` | `block-size` |
|
|
418
|
+
| `top` | `inset-block-start` |
|
|
419
|
+
| `left` | `inset-inline-start` |
|
|
420
|
+
| `border-left` | `border-inline-start` |
|
|
421
|
+
| `text-align: left` | `text-align: start` |
|
|
422
|
+
|
|
423
|
+
### Real Usage
|
|
424
|
+
|
|
425
|
+
```css
|
|
426
|
+
/* Navigation link — respects RTL padding */
|
|
427
|
+
.nav__link {
|
|
428
|
+
padding-inline: 1rem;
|
|
429
|
+
padding-block: 0.5rem;
|
|
430
|
+
border-inline-start: 3px solid transparent;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.nav__link:hover {
|
|
434
|
+
border-inline-start-color: var(--color-accent);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* Card — flow-safe spacing */
|
|
438
|
+
.card {
|
|
439
|
+
margin-block-end: 1.5rem;
|
|
440
|
+
padding-inline: 1.5rem;
|
|
441
|
+
padding-block: 1rem;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* Positioned element — flow-relative */
|
|
445
|
+
.tooltip {
|
|
446
|
+
position: absolute;
|
|
447
|
+
inset-block-start: 100%;
|
|
448
|
+
inset-inline-start: 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/* Shorthand: all four insets */
|
|
452
|
+
.overlay {
|
|
453
|
+
inset: 0; /* shorthand for top/right/bottom/left: 0 */
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**Browser support:** All modern browsers. IE11 does not support logical properties.
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## 7. Safe Area Insets
|
|
462
|
+
|
|
463
|
+
### Purpose
|
|
464
|
+
|
|
465
|
+
On devices with notches, home bars, rounded corners, or camera cutouts, content can be hidden behind the system UI. CSS environment variables expose the safe zone.
|
|
466
|
+
|
|
467
|
+
### Variables
|
|
468
|
+
|
|
469
|
+
| Variable | Maps to |
|
|
470
|
+
|---|---|
|
|
471
|
+
| `env(safe-area-inset-top)` | Top notch / status bar |
|
|
472
|
+
| `env(safe-area-inset-right)` | Right edge |
|
|
473
|
+
| `env(safe-area-inset-bottom)` | Home indicator / bottom bar |
|
|
474
|
+
| `env(safe-area-inset-left)` | Left edge |
|
|
475
|
+
|
|
476
|
+
### Requirements
|
|
477
|
+
|
|
478
|
+
The viewport meta tag must include `viewport-fit=cover`:
|
|
479
|
+
|
|
480
|
+
```html
|
|
481
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Usage Patterns
|
|
485
|
+
|
|
486
|
+
```css
|
|
487
|
+
/* Fixed header — clear the notch */
|
|
488
|
+
.app-header {
|
|
489
|
+
position: fixed;
|
|
490
|
+
top: 0;
|
|
491
|
+
inset-inline: 0;
|
|
492
|
+
padding-block-start: env(safe-area-inset-top);
|
|
493
|
+
height: calc(60px + env(safe-area-inset-top));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* Fixed bottom nav — clear home indicator */
|
|
497
|
+
.bottom-nav {
|
|
498
|
+
position: fixed;
|
|
499
|
+
bottom: 0;
|
|
500
|
+
inset-inline: 0;
|
|
501
|
+
padding-block-end: env(safe-area-inset-bottom);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/* Scrollable content — offset for fixed header/footer */
|
|
505
|
+
.scroll-content {
|
|
506
|
+
padding-block-start: calc(60px + env(safe-area-inset-top));
|
|
507
|
+
padding-block-end: calc(56px + env(safe-area-inset-bottom));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/* Fallback with max() for older browsers */
|
|
511
|
+
.app-header {
|
|
512
|
+
padding-top: max(env(safe-area-inset-top), 16px);
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**Browser support:** Safari 11.1+, Chrome 69+, Firefox 110+.
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## 8. `aspect-ratio` with `object-fit` and `object-position`
|
|
521
|
+
|
|
522
|
+
### `aspect-ratio`
|
|
523
|
+
|
|
524
|
+
Forces an element to maintain a width-to-height ratio:
|
|
525
|
+
|
|
526
|
+
```css
|
|
527
|
+
.card__image {
|
|
528
|
+
aspect-ratio: 16 / 9;
|
|
529
|
+
width: 100%;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.avatar {
|
|
533
|
+
aspect-ratio: 1; /* square */
|
|
534
|
+
width: 48px;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.portrait {
|
|
538
|
+
aspect-ratio: 3 / 4;
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### `object-fit`
|
|
543
|
+
|
|
544
|
+
Controls how replaced content (img, video) fills its box:
|
|
545
|
+
|
|
546
|
+
| Value | Behavior |
|
|
547
|
+
|---|---|
|
|
548
|
+
| `cover` | Scales to fill, may crop. Maintains aspect ratio. |
|
|
549
|
+
| `contain` | Scales to fit inside. Letterboxed if needed. |
|
|
550
|
+
| `fill` | Stretches to fill exactly. Distorts if different ratio. |
|
|
551
|
+
| `none` | No resizing. May overflow or underflow. |
|
|
552
|
+
| `scale-down` | Picks smaller of `none` or `contain`. |
|
|
553
|
+
|
|
554
|
+
```css
|
|
555
|
+
/* Hero image — always fills, crops if needed */
|
|
556
|
+
.hero__img {
|
|
557
|
+
width: 100%;
|
|
558
|
+
aspect-ratio: 21 / 9;
|
|
559
|
+
object-fit: cover;
|
|
560
|
+
object-position: center 30%; /* focus on upper portion */
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* Thumbnail — fits inside, no crop */
|
|
564
|
+
.thumb {
|
|
565
|
+
width: 80px;
|
|
566
|
+
height: 80px;
|
|
567
|
+
object-fit: contain;
|
|
568
|
+
background: var(--color-surface-muted); /* fills letterbox area */
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* Product shot — keep top of image in frame */
|
|
572
|
+
.product__img {
|
|
573
|
+
aspect-ratio: 1;
|
|
574
|
+
object-fit: cover;
|
|
575
|
+
object-position: top center;
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### `object-position`
|
|
580
|
+
|
|
581
|
+
Same syntax as `background-position`. Positions the content within the box:
|
|
582
|
+
|
|
583
|
+
```css
|
|
584
|
+
/* Keywords */
|
|
585
|
+
object-position: center center; /* default */
|
|
586
|
+
object-position: top right;
|
|
587
|
+
object-position: bottom left;
|
|
588
|
+
|
|
589
|
+
/* Length / percentage */
|
|
590
|
+
object-position: 50% 20%; /* horizontal vertical */
|
|
591
|
+
object-position: 0 0; /* top-left corner */
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## 9. Alignment Shorthands: `place-items`, `place-content`, `place-self`
|
|
597
|
+
|
|
598
|
+
These are shorthands combining `align-*` and `justify-*`.
|
|
599
|
+
|
|
600
|
+
### `place-items` (on container)
|
|
601
|
+
|
|
602
|
+
Sets both `align-items` and `justify-items`:
|
|
603
|
+
|
|
604
|
+
```css
|
|
605
|
+
/* Center all items in their grid cell */
|
|
606
|
+
.grid {
|
|
607
|
+
display: grid;
|
|
608
|
+
place-items: center; /* align-items: center; justify-items: center */
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/* Different values: align-items / justify-items */
|
|
612
|
+
.grid {
|
|
613
|
+
place-items: start end;
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### `place-content` (on container)
|
|
618
|
+
|
|
619
|
+
Sets both `align-content` and `justify-content`:
|
|
620
|
+
|
|
621
|
+
```css
|
|
622
|
+
/* Full centering of the grid tracks within the container */
|
|
623
|
+
.grid {
|
|
624
|
+
display: grid;
|
|
625
|
+
place-content: center;
|
|
626
|
+
|
|
627
|
+
/* Or with different values */
|
|
628
|
+
place-content: space-between center;
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### `place-self` (on grid/flex child)
|
|
633
|
+
|
|
634
|
+
Sets both `align-self` and `justify-self` for one item:
|
|
635
|
+
|
|
636
|
+
```css
|
|
637
|
+
.modal {
|
|
638
|
+
display: grid;
|
|
639
|
+
place-items: center; /* centers all children */
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.modal__close-btn {
|
|
643
|
+
place-self: start end; /* override: top-right corner */
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Centering Cheat Sheet
|
|
648
|
+
|
|
649
|
+
```css
|
|
650
|
+
/* Center a single element, any size */
|
|
651
|
+
.parent {
|
|
652
|
+
display: grid;
|
|
653
|
+
place-items: center;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/* Center flex children */
|
|
657
|
+
.flex-parent {
|
|
658
|
+
display: flex;
|
|
659
|
+
place-content: center;
|
|
660
|
+
gap: 1rem;
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## 10. `grid-auto-flow: dense`
|
|
667
|
+
|
|
668
|
+
### What It Does
|
|
669
|
+
|
|
670
|
+
When grid items have different sizes (some spanning multiple columns), gaps appear when larger items cannot fit in the current row. `dense` backfills those gaps with smaller items, ignoring source order.
|
|
671
|
+
|
|
672
|
+
```css
|
|
673
|
+
.gallery {
|
|
674
|
+
display: grid;
|
|
675
|
+
grid-template-columns: repeat(4, 1fr);
|
|
676
|
+
grid-auto-rows: 200px;
|
|
677
|
+
gap: 16px;
|
|
678
|
+
grid-auto-flow: dense; /* fills gaps with smaller items */
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.gallery__item--wide { grid-column: span 2; }
|
|
682
|
+
.gallery__item--tall { grid-row: span 2; }
|
|
683
|
+
.gallery__item--large { grid-column: span 2; grid-row: span 2; }
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### Trade-off
|
|
687
|
+
|
|
688
|
+
`dense` reorders items visually away from DOM order. This breaks keyboard navigation and screen reader traversal for interactive content. Use only for purely visual galleries where tab order is not meaningful.
|
|
689
|
+
|
|
690
|
+
```css
|
|
691
|
+
/* Safe: purely decorative image mosaic */
|
|
692
|
+
.photo-mosaic {
|
|
693
|
+
grid-auto-flow: dense;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* Unsafe: card grid with links or buttons */
|
|
697
|
+
/* Do NOT use dense here — tab order will seem random */
|
|
698
|
+
.card-grid {
|
|
699
|
+
grid-auto-flow: row; /* default — preserve order */
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## 11. Anchor Positioning
|
|
706
|
+
|
|
707
|
+
### What It Is
|
|
708
|
+
|
|
709
|
+
CSS Anchor Positioning (`anchor()`, `position-anchor`) allows an absolutely positioned element to tether its position to a named anchor element — without JavaScript. Replaces Popper.js / Floating UI for tooltips, dropdowns, popovers.
|
|
710
|
+
|
|
711
|
+
### Core Syntax
|
|
712
|
+
|
|
713
|
+
```css
|
|
714
|
+
/* 1. Define the anchor */
|
|
715
|
+
.trigger {
|
|
716
|
+
anchor-name: --my-tooltip;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/* 2. Tether positioned element to anchor */
|
|
720
|
+
.tooltip {
|
|
721
|
+
position: absolute;
|
|
722
|
+
position-anchor: --my-tooltip;
|
|
723
|
+
|
|
724
|
+
/* Position relative to anchor edges */
|
|
725
|
+
top: anchor(bottom); /* align tooltip top to anchor bottom */
|
|
726
|
+
left: anchor(left); /* align tooltip left to anchor left */
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### `anchor()` Values
|
|
731
|
+
|
|
732
|
+
```css
|
|
733
|
+
.popover {
|
|
734
|
+
position: absolute;
|
|
735
|
+
position-anchor: --trigger;
|
|
736
|
+
|
|
737
|
+
/* Anchor edge references */
|
|
738
|
+
top: anchor(bottom); /* below anchor */
|
|
739
|
+
bottom: anchor(top); /* above anchor */
|
|
740
|
+
left: anchor(right); /* right of anchor */
|
|
741
|
+
right: anchor(left); /* left of anchor */
|
|
742
|
+
top: anchor(center); /* anchor vertical center */
|
|
743
|
+
left: anchor(center); /* anchor horizontal center */
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### `@position-try` — Automatic Flip
|
|
748
|
+
|
|
749
|
+
Define fallback positions if primary placement goes off-screen:
|
|
750
|
+
|
|
751
|
+
```css
|
|
752
|
+
@position-try --flip-above {
|
|
753
|
+
top: auto;
|
|
754
|
+
bottom: anchor(top);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.tooltip {
|
|
758
|
+
position: absolute;
|
|
759
|
+
position-anchor: --trigger;
|
|
760
|
+
top: anchor(bottom);
|
|
761
|
+
position-try-fallbacks: --flip-above;
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
### Progressive Enhancement Pattern
|
|
766
|
+
|
|
767
|
+
```css
|
|
768
|
+
/* Base: JavaScript-positioned fallback */
|
|
769
|
+
.tooltip {
|
|
770
|
+
position: fixed; /* JS will set top/left */
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/* Enhancement: native anchor positioning */
|
|
774
|
+
@supports (anchor-name: --x) {
|
|
775
|
+
.tooltip {
|
|
776
|
+
position: absolute;
|
|
777
|
+
position-anchor: --trigger;
|
|
778
|
+
top: anchor(bottom);
|
|
779
|
+
left: anchor(left);
|
|
780
|
+
/* Remove JS positioning when supported */
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.trigger {
|
|
784
|
+
anchor-name: --trigger;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
**Browser support (2025):** Chrome 125+, Edge 125+. Firefox and Safari: not yet shipped. Use `@supports (anchor-name: --x)` gate for all production use.
|
|
790
|
+
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
## Quick Reference: Grid Track Sizing
|
|
794
|
+
|
|
795
|
+
```css
|
|
796
|
+
/* Fixed */
|
|
797
|
+
grid-template-columns: 200px 1fr;
|
|
798
|
+
|
|
799
|
+
/* Repeat patterns */
|
|
800
|
+
grid-template-columns: repeat(3, 1fr);
|
|
801
|
+
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
802
|
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
803
|
+
|
|
804
|
+
/* Mixed */
|
|
805
|
+
grid-template-columns: 250px repeat(3, 1fr) min-content;
|
|
806
|
+
|
|
807
|
+
/* Named lines */
|
|
808
|
+
grid-template-columns: [sidebar-start] 250px [sidebar-end main-start] 1fr [main-end];
|
|
809
|
+
|
|
810
|
+
/* Content item span */
|
|
811
|
+
.item {
|
|
812
|
+
grid-column: main-start / main-end;
|
|
813
|
+
grid-column: 1 / -1; /* full width */
|
|
814
|
+
grid-column: span 2;
|
|
815
|
+
}
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
## Quick Reference: Alignment
|
|
819
|
+
|
|
820
|
+
```css
|
|
821
|
+
/* Container — distribute tracks */
|
|
822
|
+
justify-content: start | end | center | stretch | space-between | space-around | space-evenly;
|
|
823
|
+
align-content: start | end | center | stretch | space-between | space-around | space-evenly;
|
|
824
|
+
place-content: <align-content> <justify-content>;
|
|
825
|
+
|
|
826
|
+
/* Container — align items within cells */
|
|
827
|
+
justify-items: start | end | center | stretch;
|
|
828
|
+
align-items: start | end | center | stretch | baseline;
|
|
829
|
+
place-items: <align-items> <justify-items>;
|
|
830
|
+
|
|
831
|
+
/* Item — override for one child */
|
|
832
|
+
justify-self: start | end | center | stretch;
|
|
833
|
+
align-self: start | end | center | stretch | baseline;
|
|
834
|
+
place-self: <align-self> <justify-self>;
|
|
835
|
+
```
|