create-dstack 0.1.0 → 0.1.2

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.
@@ -1,381 +0,0 @@
1
- # Animations
2
-
3
- Interruptible animations, enter/exit transitions, and contextual icon animations.
4
-
5
- ## Interruptible Animations
6
-
7
- Users change intent mid-interaction. If animations aren't interruptible, the interface feels broken.
8
-
9
- ### CSS Transitions vs. Keyframes
10
-
11
- | | CSS Transitions | CSS Keyframe Animations |
12
- | ----------------- | ----------------------------------------------------- | ---------------------------------------------------------- |
13
- | **Behavior** | Interpolate toward latest state | Run on a fixed timeline |
14
- | **Interruptible** | Yes — retargets mid-animation | No — restarts from beginning |
15
- | **Use for** | Interactive state changes (hover, toggle, open/close) | Staged sequences that run once (enter animations, loading) |
16
- | **Duration** | Adapts to remaining distance | Fixed regardless of state |
17
-
18
- ```css
19
- /* Good — interruptible transition for a toggle */
20
- .drawer {
21
- transform: translateX(-100%);
22
- transition: transform 200ms ease-out;
23
- }
24
- .drawer.open {
25
- transform: translateX(0);
26
- }
27
-
28
- /* Clicking again mid-animation smoothly reverses — no jank */
29
- ```
30
-
31
- ```css
32
- /* Bad — keyframe animation for interactive element */
33
- .drawer.open {
34
- animation: slideIn 200ms ease-out forwards;
35
- }
36
-
37
- /* Closing mid-animation snaps or restarts — feels broken */
38
- ```
39
-
40
- **Rule:** Always prefer CSS transitions for interactive elements. Reserve keyframes for one-shot sequences.
41
-
42
- ## Enter Animations: Split and Stagger
43
-
44
- Don't animate a single large container. Break content into semantic chunks and animate each individually.
45
-
46
- ### Step by Step
47
-
48
- 1. **Split** into logical groups (title, description, buttons)
49
- 2. **Stagger** with ~100ms delay between groups
50
- 3. **For titles**, consider splitting into individual words with ~80ms stagger
51
- 4. **Combine** `opacity`, `blur`, and `translateY` for the enter effect
52
-
53
- ### Code Example
54
-
55
- ```tsx
56
- // Motion (Framer Motion) — staggered enter
57
- function PageHeader() {
58
- return (
59
- <motion.div
60
- initial="hidden"
61
- animate="visible"
62
- variants={{
63
- visible: { transition: { staggerChildren: 0.1 } }
64
- }}
65
- >
66
- <motion.h1
67
- variants={{
68
- hidden: { opacity: 0, y: 12, filter: "blur(4px)" },
69
- visible: { opacity: 1, y: 0, filter: "blur(0px)" }
70
- }}
71
- >
72
- Welcome
73
- </motion.h1>
74
-
75
- <motion.p
76
- variants={{
77
- hidden: { opacity: 0, y: 12, filter: "blur(4px)" },
78
- visible: { opacity: 1, y: 0, filter: "blur(0px)" }
79
- }}
80
- >
81
- A description of the page.
82
- </motion.p>
83
-
84
- <motion.div
85
- variants={{
86
- hidden: { opacity: 0, y: 12, filter: "blur(4px)" },
87
- visible: { opacity: 1, y: 0, filter: "blur(0px)" }
88
- }}
89
- >
90
- <Button>Get started</Button>
91
- </motion.div>
92
- </motion.div>
93
- );
94
- }
95
- ```
96
-
97
- ### CSS-Only Stagger
98
-
99
- ```css
100
- .stagger-item {
101
- opacity: 0;
102
- transform: translateY(12px);
103
- filter: blur(4px);
104
- animation: fadeInUp 400ms ease-out forwards;
105
- }
106
-
107
- .stagger-item:nth-child(1) {
108
- animation-delay: 0ms;
109
- }
110
- .stagger-item:nth-child(2) {
111
- animation-delay: 100ms;
112
- }
113
- .stagger-item:nth-child(3) {
114
- animation-delay: 200ms;
115
- }
116
-
117
- @keyframes fadeInUp {
118
- to {
119
- opacity: 1;
120
- transform: translateY(0);
121
- filter: blur(0);
122
- }
123
- }
124
- ```
125
-
126
- ## Exit Animations
127
-
128
- Exit animations should be softer and less attention-grabbing than enter animations. The user's focus is moving to the next thing — don't fight for attention.
129
-
130
- ### Subtle Exit (Recommended)
131
-
132
- ```tsx
133
- // Small fixed translateY — indicates direction without drama
134
- <motion.div
135
- exit={{
136
- opacity: 0,
137
- y: -12,
138
- filter: "blur(4px)",
139
- transition: { duration: 0.15, ease: "easeIn" }
140
- }}
141
- >
142
- {content}
143
- </motion.div>
144
- ```
145
-
146
- ### Full Exit (When Context Matters)
147
-
148
- ```tsx
149
- // Slide fully out — use when spatial context is important
150
- // (e.g., a card returning to a list, a drawer closing)
151
- <motion.div
152
- exit={{
153
- opacity: 0,
154
- x: "-100%",
155
- transition: { duration: 0.2, ease: "easeIn" }
156
- }}
157
- >
158
- {content}
159
- </motion.div>
160
- ```
161
-
162
- ### Good vs. Bad
163
-
164
- ```css
165
- /* Good — subtle exit */
166
- .item-exit {
167
- opacity: 0;
168
- transform: translateY(-12px);
169
- transition:
170
- opacity 150ms ease-in,
171
- transform 150ms ease-in;
172
- }
173
-
174
- /* Bad — dramatic exit that steals focus */
175
- .item-exit {
176
- opacity: 0;
177
- transform: translateY(-100%) scale(0.5);
178
- transition: all 400ms ease-in;
179
- }
180
-
181
- /* Bad — no exit animation at all (element just vanishes) */
182
- .item-exit {
183
- display: none;
184
- }
185
- ```
186
-
187
- **Key points:**
188
-
189
- - Use a small fixed `translateY` (e.g., `-12px`) instead of the full container height
190
- - Keep some directional movement to indicate where the element went
191
- - Exit duration should be shorter than enter duration (150ms vs 300ms)
192
- - Don't remove exit animations entirely — subtle motion preserves context
193
-
194
- ## Contextual Icon Animations
195
-
196
- When icons appear or disappear contextually (on hover, on state change), animate them with `opacity`, `scale`, and `blur` rather than just toggling visibility.
197
-
198
- ### Motion Example
199
-
200
- ```tsx
201
- import { AnimatePresence, motion } from "motion/react";
202
-
203
- function IconButton({ isActive, icon: Icon }) {
204
- return (
205
- <button>
206
- <AnimatePresence mode="popLayout">
207
- <motion.span
208
- key={isActive ? "active" : "inactive"}
209
- initial={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }}
210
- animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
211
- exit={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }}
212
- transition={{ type: "spring", duration: 0.3, bounce: 0 }}
213
- >
214
- <Icon />
215
- </motion.span>
216
- </AnimatePresence>
217
- </button>
218
- );
219
- }
220
- ```
221
-
222
- ### CSS Transition Approach (No Motion)
223
-
224
- If the project doesn't use Motion (Framer Motion), keep both icons in the DOM and cross-fade them with CSS transitions. Because neither icon unmounts, both enter and exit animate smoothly.
225
-
226
- The trick: one icon is absolutely positioned on top of the other. Toggling state cross-fades them — the entering icon scales up from `0.25` while the exiting icon scales down to `0.25`, both with opacity and blur.
227
-
228
- ```tsx
229
- function IconButton({ isActive, ActiveIcon, InactiveIcon }) {
230
- return (
231
- <button>
232
- <div className="relative">
233
- <div
234
- className={cn(
235
- "absolute inset-0 flex items-center justify-center",
236
- "transition-[opacity,filter,scale] duration-300",
237
- "cubic-bezier(0.2, 0, 0, 1)",
238
- isActive ? "scale-100 opacity-100 blur-0" : "scale-[0.25] opacity-0 blur-xs"
239
- )}
240
- >
241
- <ActiveIcon />
242
- </div>
243
- <div
244
- className={cn(
245
- "transition-[opacity,filter,scale] duration-300",
246
- "cubic-bezier(0.2, 0, 0, 1)",
247
- isActive ? "scale-[0.25] opacity-0 blur-xs" : "scale-100 opacity-100 blur-0"
248
- )}
249
- >
250
- <InactiveIcon />
251
- </div>
252
- </div>
253
- </button>
254
- );
255
- }
256
- ```
257
-
258
- The non-absolute icon (InactiveIcon) defines the layout size. The absolute icon (ActiveIcon) overlays it without affecting flow.
259
-
260
- ### Choosing Between Motion and CSS
261
-
262
- | | Motion (Framer Motion) | CSS transitions (both icons in DOM) |
263
- | ------------------- | ----------------------------------- | ------------------------------------------------------ |
264
- | **Enter animation** | Yes | Yes |
265
- | **Exit animation** | Yes (via `AnimatePresence`) | Yes (cross-fade — icon never unmounts) |
266
- | **Spring physics** | Yes | No — use `cubic-bezier(0.2, 0, 0, 1)` as approximation |
267
- | **When to use** | Project already uses `motion/react` | No motion dependency, or keeping bundle small |
268
-
269
- **Rule:** Check the project's `package.json` for `motion` or `framer-motion`. If present, use the Motion approach. If not, use the CSS cross-fade pattern — don't add a dependency just for icon transitions.
270
-
271
- ### When to Animate Icons
272
-
273
- | Animate | Don't animate |
274
- | ----------------------------------------------- | ------------------------------- |
275
- | Icons that appear on hover (action buttons) | Static navigation icons |
276
- | State change icons (play → pause, like → liked) | Decorative icons |
277
- | Icons in contextual toolbars | Icons that are always visible |
278
- | Loading/success state indicators | Icon labels (text next to icon) |
279
-
280
- **Important:** Always use exactly these values for contextual icon animations — do not deviate:
281
-
282
- - `scale`: `0.25` → `1` (never use `0.5` or `0.6`)
283
- - `opacity`: `0` → `1`
284
- - `filter`: `"blur(4px)"` → `"blur(0px)"`
285
- - `transition`: `{ type: "spring", duration: 0.3, bounce: 0 }` — **bounce must always be `0`**, never `0.1` or any other value
286
-
287
- ## Scale on Press
288
-
289
- A subtle scale-down on click gives buttons tactile feedback. Always use `scale(0.96)`. Never use a value smaller than `0.95` — anything below feels exaggerated. Use CSS transitions for interruptibility — if the user releases mid-press, it should smoothly return.
290
-
291
- Not every button needs this. Add a `static` prop to your button component that disables the scale effect when the motion would be distracting.
292
-
293
- ### CSS Example
294
-
295
- ```css
296
- .button {
297
- transition-property: scale;
298
- transition-duration: 150ms;
299
- transition-timing-function: ease-out;
300
- }
301
-
302
- .button:active {
303
- scale: 0.96;
304
- }
305
- ```
306
-
307
- ### Tailwind Example
308
-
309
- ```tsx
310
- <button className="transition-transform duration-150 ease-out active:scale-[0.96]">Click me</button>
311
- ```
312
-
313
- ### Motion Example
314
-
315
- ```tsx
316
- <motion.button whileTap={{ scale: 0.96 }}>Click me</motion.button>
317
- ```
318
-
319
- ### Static Prop Pattern
320
-
321
- Extract the scale class into a variable and conditionally apply it based on a `static` prop:
322
-
323
- ```tsx
324
- const tapScale = "active:not-disabled:scale-[0.96]";
325
-
326
- function Button({ static: isStatic, className, children, ...props }) {
327
- return (
328
- <button
329
- className={cn(
330
- "transition-transform duration-150 ease-out",
331
- !isStatic && tapScale,
332
- className,
333
- )}
334
- {...props}
335
- >
336
- {children}
337
- </button>
338
- );
339
- }
340
-
341
- // Usage
342
- <Button>Click me</Button> {/* scales on press */}
343
- <Button static>Submit</Button> {/* no scale */}
344
- ```
345
-
346
- ## Skip Animation on Page Load
347
-
348
- Use `initial={false}` on `AnimatePresence` to prevent enter animations from firing on first render. Elements that are already in their default state shouldn't animate in on page load — only on subsequent state changes.
349
-
350
- ### When It Works
351
-
352
- ```tsx
353
- // Good — icon doesn't animate in on mount, only on state change
354
- <AnimatePresence initial={false} mode="popLayout">
355
- <motion.span
356
- key={isActive ? "active" : "inactive"}
357
- initial={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }}
358
- animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
359
- exit={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }}
360
- >
361
- <Icon />
362
- </motion.span>
363
- </AnimatePresence>
364
- ```
365
-
366
- Works well for: icon swaps, toggles, tabs, segmented controls — anything that has a default state on page load.
367
-
368
- ### When It Breaks
369
-
370
- Don't use `initial={false}` when the component relies on its `initial` prop to set up a first-time enter animation, like a staggered page hero or a loading state. In those cases, removing the initial animation skips the entire entrance.
371
-
372
- ```tsx
373
- // Bad — initial={false} would skip the staggered page enter entirely
374
- <AnimatePresence initial={false}>
375
- <motion.div initial="hidden" animate="visible" variants={...}>
376
- ...
377
- </motion.div>
378
- </AnimatePresence>
379
- ```
380
-
381
- Verify the component still looks right on a full page refresh before applying this.
@@ -1,88 +0,0 @@
1
- # Performance
2
-
3
- Transition specificity and GPU compositing hints.
4
-
5
- ## Transition Only What Changes
6
-
7
- Never use `transition: all` or Tailwind's `transition` shorthand (which maps to `transition-property: all`). Always specify the exact properties that change.
8
-
9
- ### Why
10
-
11
- - `transition: all` forces the browser to watch every property for changes
12
- - Causes unexpected transitions on properties you didn't intend to animate (colors, padding, shadows)
13
- - Prevents browser optimizations
14
-
15
- ### CSS Example
16
-
17
- ```css
18
- /* Good — only transition what changes */
19
- .button {
20
- transition-property: scale, background-color;
21
- transition-duration: 150ms;
22
- transition-timing-function: ease-out;
23
- }
24
-
25
- /* Bad — transition everything */
26
- .button {
27
- transition: all 150ms ease-out;
28
- }
29
- ```
30
-
31
- ### Tailwind
32
-
33
- ```tsx
34
- // Good — explicit properties
35
- <button className="transition-[scale,background-color] duration-150 ease-out">
36
-
37
- // Bad — transition all
38
- <button className="transition duration-150 ease-out">
39
- ```
40
-
41
- ### Tailwind `transition-transform` Note
42
-
43
- `transition-transform` in Tailwind maps to `transition-property: transform, translate, scale, rotate` — it covers all transform-related properties, not just `transform`. Use this when you're only animating transforms. For multiple non-transform properties, use the bracket syntax: `transition-[scale,opacity,filter]`.
44
-
45
- ## Use `will-change` Sparingly
46
-
47
- `will-change` hints the browser to pre-promote an element to its own GPU compositing layer. Without it, the browser promotes the element only when the animation starts — that one-time layer promotion can cause a micro-stutter on the first frame.
48
-
49
- This particularly helps when an element is changing `scale`, `rotation`, or moving around with `transform`. For other properties, it doesn't help much — the browser can't composite them on the GPU anyway.
50
-
51
- ### Rules
52
-
53
- ```css
54
- /* Good — specific property that benefits from GPU compositing */
55
- .animated-card {
56
- will-change: transform;
57
- }
58
-
59
- /* Good — multiple compositor-friendly properties */
60
- .animated-card {
61
- will-change: transform, opacity;
62
- }
63
-
64
- /* Bad — never use will-change: all */
65
- .animated-card {
66
- will-change: all;
67
- }
68
-
69
- /* Bad — properties that can't be GPU-composited anyway */
70
- .animated-card {
71
- will-change: background-color, padding;
72
- }
73
- ```
74
-
75
- ### Useful Properties
76
-
77
- | Property | GPU-compositable | Worth using `will-change` |
78
- | -------------------------------- | ---------------- | ------------------------- |
79
- | `transform` | Yes | Yes |
80
- | `opacity` | Yes | Yes |
81
- | `filter` (blur, brightness) | Yes | Yes |
82
- | `clip-path` | Yes | Yes |
83
- | `top`, `left`, `width`, `height` | No | No |
84
- | `background`, `border`, `color` | No | No |
85
-
86
- ### When to Skip
87
-
88
- Modern browsers are already good at optimizing on their own. Only add `will-change` when you notice first-frame stutter — Safari in particular benefits from it. Don't add it preemptively to every animated element; each extra compositing layer costs memory.
@@ -1,245 +0,0 @@
1
- # Surfaces
2
-
3
- Border radius, optical alignment, shadows, and image outlines.
4
-
5
- ## Concentric Border Radius
6
-
7
- When nesting rounded elements, the outer radius must equal the inner radius plus the padding between them:
8
-
9
- ```
10
- outerRadius = innerRadius + padding
11
- ```
12
-
13
- This rule is most useful when nested surfaces are close together. If padding is larger than `24px`, treat the layers as separate surfaces and choose each radius independently instead of forcing strict concentric math.
14
-
15
- ### Example
16
-
17
- ```css
18
- /* Good — concentric radii */
19
- .card {
20
- border-radius: 20px; /* 12 + 8 */
21
- padding: 8px;
22
- }
23
- .card-inner {
24
- border-radius: 12px;
25
- }
26
-
27
- /* Bad — same radius on both */
28
- .card {
29
- border-radius: 12px;
30
- padding: 8px;
31
- }
32
- .card-inner {
33
- border-radius: 12px;
34
- }
35
- ```
36
-
37
- ### Tailwind Example
38
-
39
- ```tsx
40
- // Good — outer radius accounts for padding
41
- <div className="rounded-2xl p-2"> {/* 16px radius, 8px padding */}
42
- <div className="rounded-lg"> {/* 8px radius = 16 - 8 ✓ */}
43
- ...
44
- </div>
45
- </div>
46
-
47
- // Bad — same radius on both
48
- <div className="rounded-xl p-2">
49
- <div className="rounded-xl"> {/* same radius, looks off */}
50
- ...
51
- </div>
52
- </div>
53
- ```
54
-
55
- Mismatched border radii on nested elements is one of the most common things that makes interfaces feel off. Always calculate concentrically.
56
-
57
- ## Optical Alignment
58
-
59
- When geometric centering looks off, align optically instead.
60
-
61
- ### Buttons with Text + Icon
62
-
63
- Use slightly less padding on the icon side to make the button feel balanced. A reliable rule of thumb is:
64
- `icon-side padding = text-side padding - 2px`.
65
-
66
- ```css
67
- /* Good — less padding on icon side */
68
- .button-with-icon {
69
- padding-left: 16px;
70
- padding-right: 14px; /* icon side = text side - 2px */
71
- }
72
-
73
- /* Bad — equal padding looks like icon is pushed too far right */
74
- .button-with-icon {
75
- padding: 0 16px;
76
- }
77
- ```
78
-
79
- ```tsx
80
- // Tailwind
81
- <button className="pl-4 pr-3.5 flex items-center gap-2">
82
- <span>Continue</span>
83
- <ArrowRightIcon />
84
- </button>
85
- ```
86
-
87
- ### Play Button Triangles
88
-
89
- Play icons are triangular and their geometric center is not their visual center. Shift slightly right:
90
-
91
- ```css
92
- /* Good — optically centered */
93
- .play-button svg {
94
- margin-left: 2px; /* shift right to account for triangle shape */
95
- }
96
-
97
- /* Bad — geometrically centered but looks off */
98
- .play-button svg {
99
- /* no adjustment */
100
- }
101
- ```
102
-
103
- ### Asymmetric Icons (Stars, Arrows, Carets)
104
-
105
- Some icons have uneven visual weight. The best fix is adjusting the SVG directly so no extra margin/padding is needed in the component code.
106
-
107
- ```tsx
108
- // Best — fix in the SVG itself
109
- // Adjust the viewBox or path to visually center the icon
110
-
111
- // Fallback — adjust with margin
112
- <span className="ml-px">
113
- <StarIcon />
114
- </span>
115
- ```
116
-
117
- ## Shadows Instead of Borders
118
-
119
- For **buttons, cards, and containers** that use a border for depth or elevation, prefer replacing it with a subtle `box-shadow`. Shadows adapt to any background since they use transparency; solid borders don't. This also helps when using images or multiple colors as backgrounds — solid border colors don't work well on backgrounds other than the ones they were designed for.
120
-
121
- **Do not apply this to dividers** (`border-b`, `border-t`, side borders) or any border whose purpose is layout separation rather than element depth. Those should stay as borders.
122
-
123
- ### Shadow as Border (Light Mode)
124
-
125
- The shadow is comprised of three layers. The first acts as a 1px border ring, the second adds subtle lift, and the third provides ambient depth:
126
-
127
- ```css
128
- :root {
129
- --shadow-border:
130
- 0px 0px 0px 1px rgba(0, 0, 0, 0.06), 0px 1px 2px -1px rgba(0, 0, 0, 0.06),
131
- 0px 2px 4px 0px rgba(0, 0, 0, 0.04);
132
- --shadow-border-hover:
133
- 0px 0px 0px 1px rgba(0, 0, 0, 0.08), 0px 1px 2px -1px rgba(0, 0, 0, 0.08),
134
- 0px 2px 4px 0px rgba(0, 0, 0, 0.06);
135
- }
136
- ```
137
-
138
- ### Shadow as Border (Dark Mode)
139
-
140
- In dark mode, simplify to a single white ring — layered depth shadows aren't visible on dark backgrounds:
141
-
142
- ```css
143
- /* Dark mode — adapt to whatever setup the project uses
144
- (prefers-color-scheme, class, data attribute, etc.) */
145
- --shadow-border: 0 0 0 1px rgba(255, 255, 255, 0.08);
146
- --shadow-border-hover: 0 0 0 1px rgba(255, 255, 255, 0.13);
147
- ```
148
-
149
- ### Usage with Hover Transition
150
-
151
- Apply the variable and add `transition-[box-shadow]` for a smooth hover:
152
-
153
- ```css
154
- .card {
155
- box-shadow: var(--shadow-border);
156
- transition-property: box-shadow;
157
- transition-duration: 150ms;
158
- transition-timing-function: ease-out;
159
- }
160
-
161
- .card:hover {
162
- box-shadow: var(--shadow-border-hover);
163
- }
164
- ```
165
-
166
- ### When to Use Shadows vs. Borders
167
-
168
- | Use shadows | Use borders |
169
- | ------------------------------------- | --------------------------------------- |
170
- | Cards, containers with depth | Dividers between list items |
171
- | Buttons with bordered styles | Table cell boundaries |
172
- | Elevated elements (dropdowns, modals) | Form input outlines (for accessibility) |
173
- | Elements on varied backgrounds | Hairline separators in dense UI |
174
- | Hover/focus states for lift effect | |
175
-
176
- ## Image Outlines
177
-
178
- Add a subtle `1px` outline with low opacity to images. This creates consistent depth, especially in design systems where other elements use borders or shadows.
179
-
180
- ### Light Mode
181
-
182
- ```css
183
- img {
184
- outline: 1px solid rgba(0, 0, 0, 0.1);
185
- outline-offset: -1px; /* inset so it doesn't add to layout */
186
- }
187
- ```
188
-
189
- ### Dark Mode
190
-
191
- ```css
192
- img {
193
- outline: 1px solid rgba(255, 255, 255, 0.1);
194
- outline-offset: -1px;
195
- }
196
- ```
197
-
198
- ### Tailwind with Dark Mode
199
-
200
- ```tsx
201
- <img
202
- className="outline -outline-offset-1 outline-black/10 dark:outline-white/10"
203
- src={src}
204
- alt={alt}
205
- />
206
- ```
207
-
208
- **Why outline instead of border?** `outline` doesn't affect layout (no added width/height), and `outline-offset: -1px` keeps it inset so images stay their intended size.
209
-
210
- ## Minimum Hit Area
211
-
212
- Interactive elements should have a minimum hit area of 44×44px (WCAG) or at least 40×40px. If the visible element is smaller (e.g., a 20×20 checkbox), extend the hit area with a pseudo-element.
213
-
214
- ### CSS Example
215
-
216
- ```css
217
- /* Small checkbox with expanded hit area */
218
- .checkbox {
219
- position: relative;
220
- width: 20px;
221
- height: 20px;
222
- }
223
-
224
- .checkbox::after {
225
- content: "";
226
- position: absolute;
227
- top: 50%;
228
- left: 50%;
229
- transform: translate(-50%, -50%);
230
- width: 40px;
231
- height: 40px;
232
- }
233
- ```
234
-
235
- ### Tailwind Example
236
-
237
- ```tsx
238
- <button className="relative size-5 after:absolute after:top-1/2 after:left-1/2 after:size-10 after:-translate-1/2">
239
- <CheckIcon />
240
- </button>
241
- ```
242
-
243
- ### Collision Rule
244
-
245
- If the extended hit area overlaps another interactive element, shrink the pseudo-element — but make it as large as possible without colliding. Two interactive elements should never have overlapping hit areas.