|
| 1 | +# Positioning |
| 2 | + |
| 3 | +Utilities for controlling how elements are positioned in the layout using `relative` and `absolute` placement. |
| 4 | + |
| 5 | +- [Position Types](#position-types) |
| 6 | +- [Offset Utilities](#offset-utilities) |
| 7 | +- [Inset Shortcuts](#inset-shortcuts) |
| 8 | +- [Negative Offsets](#negative-offsets) |
| 9 | +- [Arbitrary Values](#arbitrary-values) |
| 10 | +- [Common Patterns](#common-patterns) |
| 11 | +- [Combining with Flex](#combining-with-flex) |
| 12 | +- [Future Work](#future-work) |
| 13 | +- [Related Documentation](#related-documentation) |
| 14 | + |
| 15 | +<x-preview path="layout/positioning" size="lg" source="example/lib/pages/layout/positioning.dart"></x-preview> |
| 16 | + |
| 17 | +```dart |
| 18 | +// Badge overlay — red dot on an avatar |
| 19 | +WDiv( |
| 20 | + className: 'relative', |
| 21 | + children: [ |
| 22 | + WDiv(className: 'w-12 h-12 rounded-full bg-gray-300'), |
| 23 | + WDiv(className: 'absolute top-0 right-0 w-3 h-3 rounded-full bg-red-500'), |
| 24 | + ], |
| 25 | +) |
| 26 | +``` |
| 27 | + |
| 28 | +<a name="position-types"></a> |
| 29 | +## Position Types |
| 30 | + |
| 31 | +Wind maps CSS `position` values to Flutter's `Stack` widget. A `relative` parent establishes a stacking context; `absolute` children are placed inside it using offset utilities. |
| 32 | + |
| 33 | +| Wind className | CSS Equivalent | Flutter Widget | |
| 34 | +|:---------------|:---------------|:---------------| |
| 35 | +| `relative` | `position: relative` | `Stack` (parent) | |
| 36 | +| `absolute` | `position: absolute` | `Positioned` (child inside `Stack`) | |
| 37 | + |
| 38 | +> [!NOTE] |
| 39 | +> An `absolute` child must live inside a `relative` parent. Wind will wrap the `relative` container in a `Stack` and each `absolute` child in a `Positioned` widget automatically. |
| 40 | +
|
| 41 | +```dart |
| 42 | +WDiv( |
| 43 | + className: 'relative w-32 h-32 bg-gray-100', |
| 44 | + children: [ |
| 45 | + WDiv(className: 'absolute top-2 left-2 w-8 h-8 bg-blue-500'), |
| 46 | + ], |
| 47 | +) |
| 48 | +``` |
| 49 | + |
| 50 | +<a name="offset-utilities"></a> |
| 51 | +## Offset Utilities |
| 52 | + |
| 53 | +Control the position of `absolute` children using directional offset classes. Values follow the spacing scale (`spacing * n`). |
| 54 | + |
| 55 | +| Class | CSS Equivalent | Description | |
| 56 | +|:------|:---------------|:------------| |
| 57 | +| `top-{n}` | `top: {n}` | Distance from the top edge | |
| 58 | +| `right-{n}` | `right: {n}` | Distance from the right edge | |
| 59 | +| `bottom-{n}` | `bottom: {n}` | Distance from the bottom edge | |
| 60 | +| `left-{n}` | `left: {n}` | Distance from the left edge | |
| 61 | + |
| 62 | +Default spacing scale (base unit = 4px): |
| 63 | + |
| 64 | +| Class | Value | |
| 65 | +|:------|:------| |
| 66 | +| `top-0` | 0px | |
| 67 | +| `top-1` | 4px | |
| 68 | +| `top-2` | 8px | |
| 69 | +| `top-4` | 16px | |
| 70 | +| `top-6` | 24px | |
| 71 | +| `top-8` | 32px | |
| 72 | + |
| 73 | +The same scale applies to `right-*`, `bottom-*`, and `left-*`. |
| 74 | + |
| 75 | +```dart |
| 76 | +WDiv( |
| 77 | + className: 'relative h-48 bg-white border border-gray-200 rounded-lg', |
| 78 | + children: [ |
| 79 | + WDiv( |
| 80 | + className: 'absolute bottom-4 right-4 px-3 py-2 bg-blue-600 rounded', |
| 81 | + child: WText('Action', className: 'text-sm text-white'), |
| 82 | + ), |
| 83 | + ], |
| 84 | +) |
| 85 | +``` |
| 86 | + |
| 87 | +<a name="inset-shortcuts"></a> |
| 88 | +## Inset Shortcuts |
| 89 | + |
| 90 | +Apply offsets to multiple sides at once with `inset-*` shorthand classes. |
| 91 | + |
| 92 | +| Class | Sides Affected | Description | |
| 93 | +|:------|:---------------|:------------| |
| 94 | +| `inset-{n}` | top, right, bottom, left | All four sides | |
| 95 | +| `inset-x-{n}` | left, right | Horizontal sides only | |
| 96 | +| `inset-y-{n}` | top, bottom | Vertical sides only | |
| 97 | +| `inset-0` | top, right, bottom, left | Full stretch (fills parent) | |
| 98 | + |
| 99 | +```dart |
| 100 | +// Full overlay — covers the entire relative parent |
| 101 | +WDiv( |
| 102 | + className: 'relative w-full h-48', |
| 103 | + children: [ |
| 104 | + WImage(src: 'assets/photo.jpg', className: 'w-full h-full'), |
| 105 | + // Semi-transparent overlay that fills the image |
| 106 | + WDiv( |
| 107 | + className: 'absolute inset-0 bg-black opacity-40', |
| 108 | + ), |
| 109 | + WText( |
| 110 | + 'Caption', |
| 111 | + className: 'absolute bottom-4 left-4 text-white font-semibold', |
| 112 | + ), |
| 113 | + ], |
| 114 | +) |
| 115 | +``` |
| 116 | + |
| 117 | +```dart |
| 118 | +// Horizontal inset — leaves top and bottom unconstrained |
| 119 | +WDiv( |
| 120 | + className: 'absolute inset-x-4 bottom-4 bg-white rounded p-3', |
| 121 | + child: WText('Bottom bar'), |
| 122 | +) |
| 123 | +``` |
| 124 | + |
| 125 | +<a name="negative-offsets"></a> |
| 126 | +## Negative Offsets |
| 127 | + |
| 128 | +Prefix any offset class with `-` to pull an element outside its parent's boundary. |
| 129 | + |
| 130 | +| Class | Value | Description | |
| 131 | +|:------|:------|:------------| |
| 132 | +| `-top-{n}` | negative top | Pulls element above the parent | |
| 133 | +| `-right-{n}` | negative right | Pulls element to the right of the parent | |
| 134 | +| `-bottom-{n}` | negative bottom | Pulls element below the parent | |
| 135 | +| `-left-{n}` | negative left | Pulls element to the left of the parent | |
| 136 | +| `-inset-{n}` | negative all sides | Expands element beyond all four parent edges | |
| 137 | + |
| 138 | +```dart |
| 139 | +// Notification badge — overlaps the top-right corner of an icon button |
| 140 | +WDiv( |
| 141 | + className: 'relative', |
| 142 | + children: [ |
| 143 | + WDiv(className: 'w-10 h-10 rounded bg-gray-200 flex items-center justify-center'), |
| 144 | + WDiv( |
| 145 | + className: 'absolute -top-1 -right-1 w-4 h-4 rounded-full bg-red-500 flex items-center justify-center', |
| 146 | + child: WText('3', className: 'text-[10px] text-white font-bold'), |
| 147 | + ), |
| 148 | + ], |
| 149 | +) |
| 150 | +``` |
| 151 | + |
| 152 | +<a name="arbitrary-values"></a> |
| 153 | +## Arbitrary Values |
| 154 | + |
| 155 | +Use bracket notation when you need an exact pixel value not in the theme scale. Only `px` values are supported — percentage (`%`) offsets are not supported because Flutter's `Positioned` widget uses logical pixels, not relative units. |
| 156 | + |
| 157 | +```dart |
| 158 | +// Exact pixel value |
| 159 | +WDiv(className: 'absolute top-[24px] left-[12px]') |
| 160 | +
|
| 161 | +// Mixed — precise multi-side offset |
| 162 | +WDiv(className: 'absolute top-[12px] right-[8px] bottom-[12px] left-[8px]') |
| 163 | +``` |
| 164 | + |
| 165 | +<a name="common-patterns"></a> |
| 166 | +## Common Patterns |
| 167 | + |
| 168 | +### Badge Overlay |
| 169 | + |
| 170 | +A notification dot positioned on top of an avatar or icon. |
| 171 | + |
| 172 | +```dart |
| 173 | +WDiv( |
| 174 | + className: 'relative w-12 h-12', |
| 175 | + children: [ |
| 176 | + WDiv(className: 'w-12 h-12 rounded-full bg-indigo-500'), |
| 177 | + WDiv( |
| 178 | + className: 'absolute -top-1 -right-1 w-5 h-5 rounded-full bg-red-500 border-2 border-white flex items-center justify-center', |
| 179 | + child: WText('2', className: 'text-[9px] text-white font-bold'), |
| 180 | + ), |
| 181 | + ], |
| 182 | +) |
| 183 | +``` |
| 184 | + |
| 185 | +### Floating Action Button (FAB) |
| 186 | + |
| 187 | +An action button pinned to the bottom-right corner of a scrollable view. |
| 188 | + |
| 189 | +```dart |
| 190 | +WDiv( |
| 191 | + className: 'relative flex-1', |
| 192 | + children: [ |
| 193 | + WDiv( |
| 194 | + className: 'w-full h-full overflow-y-auto p-4', |
| 195 | + child: WText('Scrollable content...'), |
| 196 | + ), |
| 197 | + WDiv( |
| 198 | + className: 'absolute bottom-6 right-6 w-14 h-14 rounded-full bg-blue-600 flex items-center justify-center shadow-lg', |
| 199 | + child: WIcon(Icons.add_outlined, className: 'text-white'), |
| 200 | + ), |
| 201 | + ], |
| 202 | +) |
| 203 | +``` |
| 204 | + |
| 205 | +### Full Overlay |
| 206 | + |
| 207 | +A semi-transparent scrim that covers an entire card or image. |
| 208 | + |
| 209 | +```dart |
| 210 | +WDiv( |
| 211 | + className: 'relative rounded-xl overflow-hidden', |
| 212 | + children: [ |
| 213 | + WImage(src: 'assets/hero.jpg', className: 'w-full h-48 object-cover'), |
| 214 | + WDiv(className: 'absolute inset-0 bg-gradient-to-t from-black/60 to-transparent'), |
| 215 | + WDiv( |
| 216 | + className: 'absolute bottom-0 left-0 right-0 p-4', |
| 217 | + child: WText('Hero Title', className: 'text-white text-lg font-bold'), |
| 218 | + ), |
| 219 | + ], |
| 220 | +) |
| 221 | +``` |
| 222 | + |
| 223 | +<a name="combining-with-flex"></a> |
| 224 | +## Combining with Flex |
| 225 | + |
| 226 | +`relative` and `absolute` compose naturally with flex layouts. The `relative` container itself can be a flex row or column — the `Stack` wraps around the flex widget, and `absolute` children are layered on top. |
| 227 | + |
| 228 | +```dart |
| 229 | +// Navigation bar with an absolute badge on the icon |
| 230 | +WDiv( |
| 231 | + className: 'flex flex-row items-center justify-between px-4 py-3 bg-white border-b border-gray-200', |
| 232 | + children: [ |
| 233 | + WText('Inbox', className: 'text-base font-semibold text-gray-900'), |
| 234 | + WDiv( |
| 235 | + className: 'relative', |
| 236 | + children: [ |
| 237 | + WIcon(Icons.notifications_outlined, className: 'text-gray-700'), |
| 238 | + WDiv( |
| 239 | + className: 'absolute -top-1 -right-1 w-4 h-4 rounded-full bg-red-500', |
| 240 | + ), |
| 241 | + ], |
| 242 | + ), |
| 243 | + ], |
| 244 | +) |
| 245 | +``` |
| 246 | + |
| 247 | +> [!NOTE] |
| 248 | +> When you add `absolute` children to a flex item, Wind promotes that item to a `Stack`. The flex layout of the parent is preserved. |
| 249 | +
|
| 250 | +<a name="future-work"></a> |
| 251 | +## Future Work |
| 252 | + |
| 253 | +The following position types are tracked but not yet implemented in v1: |
| 254 | + |
| 255 | +| Class | Status | |
| 256 | +|:------|:-------| |
| 257 | +| `fixed` | Deferred — maps to Flutter's `Overlay`/`Stack` at the root level | |
| 258 | +| `sticky` | Deferred — requires custom `SliverPersistentHeader` integration | |
| 259 | + |
| 260 | +Until these land, use `Overlay` directly or Flutter's `Stack` at the `Scaffold` body level for fixed positioning. |
| 261 | + |
| 262 | +<a name="related-documentation"></a> |
| 263 | +## Related Documentation |
| 264 | + |
| 265 | +- [Flexbox & Layout](./flexbox.md) |
| 266 | +- [Grid Layout](./grid.md) |
| 267 | +- [Sizing](../sizing/width.md) |
| 268 | +- [Spacing](../spacing/padding.md) |
0 commit comments