Skip to content

Commit cfe9c7a

Browse files
authored
Merge pull request #50 from fluttersdk/feat/css-positioning-utilities
feat: CSS positioning utilities — relative/absolute + inset offsets
2 parents e88b18f + aa8b74e commit cfe9c7a

12 files changed

Lines changed: 1519 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.
88

99
## [Unreleased]
1010

11+
### Added
12+
- **CSS Positioning**: `relative` and `absolute` position types with Stack/Positioned rendering
13+
- **Offset Utilities**: `top-*`, `right-*`, `bottom-*`, `left-*` offset tokens using spacing scale
14+
- **Inset Shortcuts**: `inset-*`, `inset-x-*`, `inset-y-*` for multi-side offsets
15+
- **Negative Offsets**: `-top-*`, `-inset-*` for negative positioning
16+
- **Arbitrary Position Values**: `top-[24px]`, `left-[12px]` bracket syntax (px only)
17+
1118
---
1219

1320
## [1.0.0-alpha.5] - 2026-03-31

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ WDynamic(
247247
<details>
248248
<summary><strong>Layout</strong> — flex, grid, positioning, overflow</summary>
249249

250-
`flex` `flex-row` `flex-col` `flex-wrap` `flex-1` `grid` `grid-cols-{n}` `gap-{n}` `justify-center` `justify-between` `items-center` `items-start` `self-center` `wrap` `hidden` `overflow-hidden` `overflow-y-auto`
250+
`flex` `flex-row` `flex-col` `flex-wrap` `flex-1` `grid` `grid-cols-{n}` `gap-{n}` `justify-center` `justify-between` `items-center` `items-start` `self-center` `wrap` `hidden` `overflow-hidden` `overflow-y-auto` `relative` `absolute` `top-{n}` `right-{n}` `bottom-{n}` `left-{n}` `inset-{n}` `inset-x-{n}` `inset-y-{n}` `top-[24px]` `left-[24px]` `-top-{n}` `-inset-{n}`
251251

252252
</details>
253253

doc/layout/positioning.md

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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

Comments
 (0)