Skip to content

Position

Position utilities set the position type and the offsets that place an element: top, right, bottom, left, the logical inset-inline-start and inset-inline-end, and the inset shorthand. Offsets read the spacing scale, so a badge placed with top-2 lines up with a p-2 gutter.

  • Type. static, relative, absolute, fixed and sticky set position. absolute-center places and centres in one class.
  • Offsets. top-4, start-4 and inset-4 use spacing keys and output var(--nu-space-4). Keys with a p suffix are percentages: top-50p is top: 50%.
  • Logical offsets. start-* and end-* set inset-inline-start and inset-inline-end, which swap in right-to-left layouts.
Part Property Examples
top, right, bottom, left Physical offsets top-0, left-4
start, end inset-inline-start, inset-inline-end (logical) start-2, end-0
inset All four sides inset-0
inset-x, inset-y left and right, top and bottom (physical) inset-x-4, inset-y-0

inset-x sets the physical left and right, unlike px and mx, which are logical. With one value on both sides the result is the same in both directions.

Class Declaration Use
static position: static Back to normal flow, for example to undo a positioned state at a breakpoint
relative position: relative A positioning context for absolute children, or a small nudge with offsets
absolute position: absolute Taken out of flow, placed against the nearest positioned ancestor
fixed position: fixed Placed against the viewport and stays there while the page scrolls
sticky position: sticky, plus default top: 0 and z-index: 999 Scrolls with the page until it reaches its offset, then sticks
Absolute children inside a relative parent
top-2 start-2
bottom-2 end-2
<div class="relative h-32 bg-alternate rounded">
<div class="absolute top-2 start-2 p-2 bg-surface border rounded">top-2 start-2</div>
<div class="absolute bottom-2 end-2 p-2 bg-surface border rounded">bottom-2 end-2</div>
</div>

fixed places an element against the viewport, so it is not shown live here:

<header class="fixed top-0 inset-x-0 z-fixed p-4 bg-surface">Site header</header>

sticky keeps an element in view once it reaches the top of its scrolling container. The class sets position: sticky at normal specificity and puts its defaults, top: 0 and z-index: 999, inside :where(), which has zero specificity:

.sticky {
position: sticky;
}
:where(.sticky) {
top: 0;
z-index: 999;
}

So any offset or z-index class on the same element wins, whatever its order in the stylesheet: sticky top-4 sticks 1rem from the top, and sticky z-sticky uses the --nu-z-sticky level instead of 999.

sticky: scroll this box
Row 1
Row 2
Row 3
Row 4
Row 5
Row 6
Row 7
Row 8
<div class="relative z-0 h-40 overflow-y-auto border rounded">
<div class="sticky p-2 bg-brand text-on-brand">sticky: scroll this box</div>
<div class="p-2">Row 1</div>
<div class="p-2">Row 2</div>
<div class="p-2">Row 3</div>
<div class="p-2">Row 4</div>
<div class="p-2">Row 5</div>
<div class="p-2">Row 6</div>
<div class="p-2">Row 7</div>
<div class="p-2">Row 8</div>
</div>

The sticky mixin emits the same three declarations at normal specificity (position: sticky; z-index: 999; top: 0). Add top() or z-index() after it in the same rule to change them.

A sticky element sticks only inside its scrolling ancestor, and stops sticking when its parent scrolls out of view. An ancestor with overflow: hidden creates a scrolling container and can stop it sticking to the page.

Offset classes use every spacing key and the percentage keys 25p, 33p, 50p, 66p, 75p and 100p. Here 33p is 33.333% and 66p is 66.667%.

KeyTokenValuePixels
0--nu-space-000px
1--nu-space-10.25rem4px
2--nu-space-20.5rem8px
3--nu-space-30.75rem12px
4--nu-space-41rem16px
5--nu-space-51.25rem20px
6--nu-space-61.5rem24px
8--nu-space-82rem32px
10--nu-space-102.5rem40px
12--nu-space-123rem48px
16--nu-space-164rem64px
20--nu-space-205rem80px
24--nu-space-246rem96px
32--nu-space-328rem128px
40--nu-space-4010rem160px
48--nu-space-4812rem192px
56--nu-space-5614rem224px
64--nu-space-6416rem256px
top-0 left-0
top-50p left-50p
bottom-0 right-0
<div class="relative h-40 bg-alternate rounded">
<div class="absolute top-0 left-0 p-2 bg-surface border rounded">top-0 left-0</div>
<div class="absolute top-50p left-50p p-2 bg-surface border rounded">top-50p left-50p</div>
<div class="absolute bottom-0 right-0 p-2 bg-surface border rounded">bottom-0 right-0</div>
</div>

top-50p left-50p places the element’s top left corner at the centre. To centre the element itself, use absolute-center.

The same markup is rendered left to right and right to left. end-2 moves to the other side; right-2 stays on the right.

LTR
end-2
right-2
RTL
end-2
right-2
<div class="space-y-2">
<div class="relative h-12 bg-alternate rounded">
<span class="absolute top-2 end-2 px-2 bg-brand text-on-brand rounded">end-2</span>
</div>
<div class="relative h-12 bg-alternate rounded">
<span class="absolute top-2 right-2 px-2 bg-surface border rounded">right-2</span>
</div>
</div>

There are no negative offset classes. Use a negative key in a mixin: offset and inset mixins accept it and output calc(-1 * var(--nu-space-{key})).

.badge {
@include apply(absolute, top(-2), inset-end(-2));
}
.badge {
position: absolute;
top: calc(-1 * var(--nu-space-2));
inset-inline-end: calc(-1 * var(--nu-space-2));
}

inset-* sets all four sides at once; inset-x-* sets left and right, inset-y-* top and bottom. inset-0 on an absolute child covers its positioned parent. inset-auto, inset-x-auto and inset-y-auto reset the sides to auto.

absolute inset-4
<div class="relative h-32 bg-alternate rounded">
<div class="absolute inset-4 p-2 bg-brand-subtle border border-brand rounded">absolute inset-4</div>
</div>

Inset classes come before the side classes in the stylesheet, so a side class adjusts an inset: inset-0 top-4 covers the parent except for 1rem at the top. Inset classes take spacing keys and auto; there are no percentage inset classes.

absolute-center sets position: absolute, top: 50%, left: 50% and a -50% translate on both axes, so the element’s centre sits at the centre of its positioned parent.

absolute-center
<div class="relative h-32 bg-alternate rounded">
<div class="absolute-center p-2 bg-surface border rounded">absolute-center</div>
</div>

The translate goes through the transform custom properties (--nu-translate-x, --nu-translate-y), so rotate, scale and skew utilities on the same element combine with it instead of replacing it. Because it uses physical left, the centring is the same in both directions.

Every position class has a responsive variant with an @{breakpoint} suffix: type (relative@md, sticky@lg), absolute-center@md, offsets (top-4@md, start-50p@lg) and insets (inset-0@md, inset-x-auto@lg). A variant applies from its breakpoint upwards.

static, absolute from md
<div class="relative h-32 bg-alternate rounded">
<div class="static absolute@md top-2@md end-2@md p-2 bg-surface border rounded">static, absolute from md</div>
</div>

Responsive inset classes come after every non-responsive position class, so inset-0@md overrides top-4. sticky@{breakpoint} keeps its defaults inside :where() too.

SuffixMedia query
@xs@media (min-width: 480px)
@sm@media (min-width: 640px)
@md@media (min-width: 768px)
@lg@media (min-width: 1024px)
@xl@media (min-width: 1280px)
@2xl@media (min-width: 1536px)

Position has no state variants.

@use "@nuvoui/core" as *;
.toast {
@include apply(fixed, bottom(4), inset-end(4), z-index(toast));
}
.toast {
position: fixed;
bottom: var(--nu-space-4);
inset-inline-end: var(--nu-space-4);
z-index: var(--nu-z-toast);
}
Mixin Class
static, relative, absolute, fixed, sticky Same name
top(), right(), bottom(), left() top-*, right-*, bottom-*, left-*
inset-start(), inset-end() start-*, end-*
inset(), inset-x(), inset-y() inset-*, inset-x-*, inset-y-*
inset-auto, inset-x-auto, inset-y-auto Same name
absolute-center Same name

The logical offset mixins are named inset-start() and inset-end() because start and end are flex alignment mixins.

A unitless number is a spacing key and may be negative. A value with a unit is used as written, and auto, calc(), var() and the CSS-wide keywords pass through. For a percentage, write the %: the 25p style keys are class names only.

.tooltip {
@include apply(absolute, top(100%), left(50%), mt(2));
}
.drawer {
position: fixed;
inset-block: 0;
inset-inline-end: token(space-0);
width: token(space-64);
}

Offset and inset classes follow $spacings: adding a key adds top-{key}, inset-{key} and the rest. The percentage keys are fixed. See Spacing configuration.

@use "@nuvoui/core" with (
$generate-utility-classes: true,
$spacings: (0: 0, 1: 0.25rem, 2: 0.5rem, 3: 0.75rem, 4: 1rem, 6: 1.5rem, 8: 2rem, 12: 3rem, 16: 4rem)
);
Input Error
top(7) top: 7 is not a configured key. Valid keys: 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64. Write a unit for a custom value (7px, 7rem).
top(foo) top: unknown value foo. Use a key (0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64), a value with a CSS unit, calc(), var() or a keyword (auto, inherit, initial, unset, revert, revert-layer).

Negative keys and negative lengths are accepted by every offset and inset mixin.

Variable Description
--nu-space-{key} Used by offset and inset classes and mixins.

absolute-center sets the internal properties --nu-translate-x and --nu-translate-y. They are not tokens.

Generated from the compiled core build. Responsive variants are not listed. The :where(.sticky) rule with the sticky defaults is shown in Sticky.

Position type (6 rules)
SelectorDeclarationsDefault value
.staticposition: staticstatic
.relativeposition: relativerelative
.absoluteposition: absoluteabsolute
.fixedposition: fixedfixed
.stickyposition: stickysticky
.absolute-centerposition: absolute
left: 50%
top: 50%
transform: translateX(var(--nu-translate-x)) translateY(var(--nu-translate-y)) translateZ(var(--nu-translate-z)) rotate(var(--nu-rotate)) skewX(var(--nu-skew-x)) skewY(var(--nu-skew-y)) scaleX(var(--nu-scale-x)) scaleY(var(--nu-scale-y))
absolute
50%
50%
translateX(var(--nu-translate-x)) translateY(var(--nu-translate-y)) translateZ(var(--nu-translate-z)) rotate(var(--nu-rotate)) skewX(var(--nu-skew-x)) skewY(var(--nu-skew-y)) scaleX(var(--nu-scale-x)) scaleY(var(--nu-scale-y))
Inset (57 rules)
SelectorDeclarationsDefault value
.inset-autoinset: autoauto
.inset-x-autoleft: auto
right: auto
auto
auto
.inset-y-autotop: auto
bottom: auto
auto
auto
.inset-0inset: var(--nu-space-0)0
.inset-x-0left: var(--nu-space-0)
right: var(--nu-space-0)
0
0
.inset-y-0top: var(--nu-space-0)
bottom: var(--nu-space-0)
0
0
.inset-1inset: var(--nu-space-1)0.25rem
.inset-x-1left: var(--nu-space-1)
right: var(--nu-space-1)
0.25rem
0.25rem
.inset-y-1top: var(--nu-space-1)
bottom: var(--nu-space-1)
0.25rem
0.25rem
.inset-2inset: var(--nu-space-2)0.5rem
.inset-x-2left: var(--nu-space-2)
right: var(--nu-space-2)
0.5rem
0.5rem
.inset-y-2top: var(--nu-space-2)
bottom: var(--nu-space-2)
0.5rem
0.5rem
.inset-3inset: var(--nu-space-3)0.75rem
.inset-x-3left: var(--nu-space-3)
right: var(--nu-space-3)
0.75rem
0.75rem
.inset-y-3top: var(--nu-space-3)
bottom: var(--nu-space-3)
0.75rem
0.75rem
.inset-4inset: var(--nu-space-4)1rem
.inset-x-4left: var(--nu-space-4)
right: var(--nu-space-4)
1rem
1rem
.inset-y-4top: var(--nu-space-4)
bottom: var(--nu-space-4)
1rem
1rem
.inset-5inset: var(--nu-space-5)1.25rem
.inset-x-5left: var(--nu-space-5)
right: var(--nu-space-5)
1.25rem
1.25rem
.inset-y-5top: var(--nu-space-5)
bottom: var(--nu-space-5)
1.25rem
1.25rem
.inset-6inset: var(--nu-space-6)1.5rem
.inset-x-6left: var(--nu-space-6)
right: var(--nu-space-6)
1.5rem
1.5rem
.inset-y-6top: var(--nu-space-6)
bottom: var(--nu-space-6)
1.5rem
1.5rem
.inset-8inset: var(--nu-space-8)2rem
.inset-x-8left: var(--nu-space-8)
right: var(--nu-space-8)
2rem
2rem
.inset-y-8top: var(--nu-space-8)
bottom: var(--nu-space-8)
2rem
2rem
.inset-10inset: var(--nu-space-10)2.5rem
.inset-x-10left: var(--nu-space-10)
right: var(--nu-space-10)
2.5rem
2.5rem
.inset-y-10top: var(--nu-space-10)
bottom: var(--nu-space-10)
2.5rem
2.5rem
.inset-12inset: var(--nu-space-12)3rem
.inset-x-12left: var(--nu-space-12)
right: var(--nu-space-12)
3rem
3rem
.inset-y-12top: var(--nu-space-12)
bottom: var(--nu-space-12)
3rem
3rem
.inset-16inset: var(--nu-space-16)4rem
.inset-x-16left: var(--nu-space-16)
right: var(--nu-space-16)
4rem
4rem
.inset-y-16top: var(--nu-space-16)
bottom: var(--nu-space-16)
4rem
4rem
.inset-20inset: var(--nu-space-20)5rem
.inset-x-20left: var(--nu-space-20)
right: var(--nu-space-20)
5rem
5rem
.inset-y-20top: var(--nu-space-20)
bottom: var(--nu-space-20)
5rem
5rem
.inset-24inset: var(--nu-space-24)6rem
.inset-x-24left: var(--nu-space-24)
right: var(--nu-space-24)
6rem
6rem
.inset-y-24top: var(--nu-space-24)
bottom: var(--nu-space-24)
6rem
6rem
.inset-32inset: var(--nu-space-32)8rem
.inset-x-32left: var(--nu-space-32)
right: var(--nu-space-32)
8rem
8rem
.inset-y-32top: var(--nu-space-32)
bottom: var(--nu-space-32)
8rem
8rem
.inset-40inset: var(--nu-space-40)10rem
.inset-x-40left: var(--nu-space-40)
right: var(--nu-space-40)
10rem
10rem
.inset-y-40top: var(--nu-space-40)
bottom: var(--nu-space-40)
10rem
10rem
.inset-48inset: var(--nu-space-48)12rem
.inset-x-48left: var(--nu-space-48)
right: var(--nu-space-48)
12rem
12rem
.inset-y-48top: var(--nu-space-48)
bottom: var(--nu-space-48)
12rem
12rem
.inset-56inset: var(--nu-space-56)14rem
.inset-x-56left: var(--nu-space-56)
right: var(--nu-space-56)
14rem
14rem
.inset-y-56top: var(--nu-space-56)
bottom: var(--nu-space-56)
14rem
14rem
.inset-64inset: var(--nu-space-64)16rem
.inset-x-64left: var(--nu-space-64)
right: var(--nu-space-64)
16rem
16rem
.inset-y-64top: var(--nu-space-64)
bottom: var(--nu-space-64)
16rem
16rem
Physical offsets (96 rules)
SelectorDeclarationsDefault value
.top-25ptop: 25%25%
.right-25pright: 25%25%
.bottom-25pbottom: 25%25%
.left-25pleft: 25%25%
.top-33ptop: 33.333%33.333%
.right-33pright: 33.333%33.333%
.bottom-33pbottom: 33.333%33.333%
.left-33pleft: 33.333%33.333%
.top-50ptop: 50%50%
.right-50pright: 50%50%
.bottom-50pbottom: 50%50%
.left-50pleft: 50%50%
.top-66ptop: 66.667%66.667%
.right-66pright: 66.667%66.667%
.bottom-66pbottom: 66.667%66.667%
.left-66pleft: 66.667%66.667%
.top-75ptop: 75%75%
.right-75pright: 75%75%
.bottom-75pbottom: 75%75%
.left-75pleft: 75%75%
.top-100ptop: 100%100%
.right-100pright: 100%100%
.bottom-100pbottom: 100%100%
.left-100pleft: 100%100%
.top-0top: var(--nu-space-0)0
.right-0right: var(--nu-space-0)0
.bottom-0bottom: var(--nu-space-0)0
.left-0left: var(--nu-space-0)0
.top-1top: var(--nu-space-1)0.25rem
.right-1right: var(--nu-space-1)0.25rem
.bottom-1bottom: var(--nu-space-1)0.25rem
.left-1left: var(--nu-space-1)0.25rem
.top-2top: var(--nu-space-2)0.5rem
.right-2right: var(--nu-space-2)0.5rem
.bottom-2bottom: var(--nu-space-2)0.5rem
.left-2left: var(--nu-space-2)0.5rem
.top-3top: var(--nu-space-3)0.75rem
.right-3right: var(--nu-space-3)0.75rem
.bottom-3bottom: var(--nu-space-3)0.75rem
.left-3left: var(--nu-space-3)0.75rem
.top-4top: var(--nu-space-4)1rem
.right-4right: var(--nu-space-4)1rem
.bottom-4bottom: var(--nu-space-4)1rem
.left-4left: var(--nu-space-4)1rem
.top-5top: var(--nu-space-5)1.25rem
.right-5right: var(--nu-space-5)1.25rem
.bottom-5bottom: var(--nu-space-5)1.25rem
.left-5left: var(--nu-space-5)1.25rem
.top-6top: var(--nu-space-6)1.5rem
.right-6right: var(--nu-space-6)1.5rem
.bottom-6bottom: var(--nu-space-6)1.5rem
.left-6left: var(--nu-space-6)1.5rem
.top-8top: var(--nu-space-8)2rem
.right-8right: var(--nu-space-8)2rem
.bottom-8bottom: var(--nu-space-8)2rem
.left-8left: var(--nu-space-8)2rem
.top-10top: var(--nu-space-10)2.5rem
.right-10right: var(--nu-space-10)2.5rem
.bottom-10bottom: var(--nu-space-10)2.5rem
.left-10left: var(--nu-space-10)2.5rem
.top-12top: var(--nu-space-12)3rem
.right-12right: var(--nu-space-12)3rem
.bottom-12bottom: var(--nu-space-12)3rem
.left-12left: var(--nu-space-12)3rem
.top-16top: var(--nu-space-16)4rem
.right-16right: var(--nu-space-16)4rem
.bottom-16bottom: var(--nu-space-16)4rem
.left-16left: var(--nu-space-16)4rem
.top-20top: var(--nu-space-20)5rem
.right-20right: var(--nu-space-20)5rem
.bottom-20bottom: var(--nu-space-20)5rem
.left-20left: var(--nu-space-20)5rem
.top-24top: var(--nu-space-24)6rem
.right-24right: var(--nu-space-24)6rem
.bottom-24bottom: var(--nu-space-24)6rem
.left-24left: var(--nu-space-24)6rem
.top-32top: var(--nu-space-32)8rem
.right-32right: var(--nu-space-32)8rem
.bottom-32bottom: var(--nu-space-32)8rem
.left-32left: var(--nu-space-32)8rem
.top-40top: var(--nu-space-40)10rem
.right-40right: var(--nu-space-40)10rem
.bottom-40bottom: var(--nu-space-40)10rem
.left-40left: var(--nu-space-40)10rem
.top-48top: var(--nu-space-48)12rem
.right-48right: var(--nu-space-48)12rem
.bottom-48bottom: var(--nu-space-48)12rem
.left-48left: var(--nu-space-48)12rem
.top-56top: var(--nu-space-56)14rem
.right-56right: var(--nu-space-56)14rem
.bottom-56bottom: var(--nu-space-56)14rem
.left-56left: var(--nu-space-56)14rem
.top-64top: var(--nu-space-64)16rem
.right-64right: var(--nu-space-64)16rem
.bottom-64bottom: var(--nu-space-64)16rem
.left-64left: var(--nu-space-64)16rem
Logical offsets (48 rules)
SelectorDeclarationsDefault value
.start-25pinset-inline-start: 25%25%
.end-25pinset-inline-end: 25%25%
.start-33pinset-inline-start: 33.333%33.333%
.end-33pinset-inline-end: 33.333%33.333%
.start-50pinset-inline-start: 50%50%
.end-50pinset-inline-end: 50%50%
.start-66pinset-inline-start: 66.667%66.667%
.end-66pinset-inline-end: 66.667%66.667%
.start-75pinset-inline-start: 75%75%
.end-75pinset-inline-end: 75%75%
.start-100pinset-inline-start: 100%100%
.end-100pinset-inline-end: 100%100%
.start-0inset-inline-start: var(--nu-space-0)0
.end-0inset-inline-end: var(--nu-space-0)0
.start-1inset-inline-start: var(--nu-space-1)0.25rem
.end-1inset-inline-end: var(--nu-space-1)0.25rem
.start-2inset-inline-start: var(--nu-space-2)0.5rem
.end-2inset-inline-end: var(--nu-space-2)0.5rem
.start-3inset-inline-start: var(--nu-space-3)0.75rem
.end-3inset-inline-end: var(--nu-space-3)0.75rem
.start-4inset-inline-start: var(--nu-space-4)1rem
.end-4inset-inline-end: var(--nu-space-4)1rem
.start-5inset-inline-start: var(--nu-space-5)1.25rem
.end-5inset-inline-end: var(--nu-space-5)1.25rem
.start-6inset-inline-start: var(--nu-space-6)1.5rem
.end-6inset-inline-end: var(--nu-space-6)1.5rem
.start-8inset-inline-start: var(--nu-space-8)2rem
.end-8inset-inline-end: var(--nu-space-8)2rem
.start-10inset-inline-start: var(--nu-space-10)2.5rem
.end-10inset-inline-end: var(--nu-space-10)2.5rem
.start-12inset-inline-start: var(--nu-space-12)3rem
.end-12inset-inline-end: var(--nu-space-12)3rem
.start-16inset-inline-start: var(--nu-space-16)4rem
.end-16inset-inline-end: var(--nu-space-16)4rem
.start-20inset-inline-start: var(--nu-space-20)5rem
.end-20inset-inline-end: var(--nu-space-20)5rem
.start-24inset-inline-start: var(--nu-space-24)6rem
.end-24inset-inline-end: var(--nu-space-24)6rem
.start-32inset-inline-start: var(--nu-space-32)8rem
.end-32inset-inline-end: var(--nu-space-32)8rem
.start-40inset-inline-start: var(--nu-space-40)10rem
.end-40inset-inline-end: var(--nu-space-40)10rem
.start-48inset-inline-start: var(--nu-space-48)12rem
.end-48inset-inline-end: var(--nu-space-48)12rem
.start-56inset-inline-start: var(--nu-space-56)14rem
.end-56inset-inline-end: var(--nu-space-56)14rem
.start-64inset-inline-start: var(--nu-space-64)16rem
.end-64inset-inline-end: var(--nu-space-64)16rem
  • Mirror with logical offsets. Use start-* and end-* for close buttons, badges and anything placed on a side that should follow the reading direction.
  • Keep visual and source order aligned. Positioning moves elements visually but not in the reading and focus order. Place elements in the source where they make sense to keyboard and screen reader users (WCAG 1.3.2 Meaningful Sequence, 2.4.3 Focus Order).
  • Do not hide focused content. A sticky or fixed header can cover the element that has keyboard focus. Leave room with scroll-padding-top on the scrolling container (WCAG 2.2, 2.4.11 Focus Not Obscured).
  • Fixed content and zoom. Large fixed or sticky bars take up much of the screen at 400% zoom or on small landscape phones. Keep them short, or make them static at small breakpoints (static sticky@md) (WCAG 1.4.10 Reflow).