Affix
Pins content to the viewport or a scroll container when scrolling past it.
Installation
npx vayu-ui init #one time onlynpx vayu-ui add affixnpx vayu-ui add -t affix #with test case needsUsage
1. Top Affix (with Offset)
This bar sticks to the top of the viewport with a 20px offset. It uses zIndex: 50.
Scroll Area (300px)
2. Bottom Affix
This bar sticks to the bottom of the viewport. Useful for cookie consents or action bars.
Scroll Area (300px)
Sticks to the bottom edge.
3. Custom Target Container
The Affix can be scoped to a specific scrollable element, not just the window. Scroll inside the box below.
Start scrolling down inside this box...
Scroll Area (200px)
Scroll Area (200px)
I am stuck to the bottom of the custom container!
Scroll Area (300px)
Scroll Area (200px)
4. Accessibility & Custom Styles
This example passes a custom className and style. It also uses standard aria-label for screen readers.
Custom Styled Banner (Rounded via style prop!)
Scroll Area (300px)
<Affix offset={20} zIndex={50} role="navigation" aria-label="Sticky Top Navigation">
<div className="bg-brand text-brand-content p-4 rounded-control shadow-elevated flex justify-between items-center">
<span className="font-bold">Top Sticky Bar</span>
<span className="text-xs bg-brand/80 px-2 py-1 rounded-control">Status: Affixed</span>
</div>
</Affix>
<Affix position="bottom" offset={0} zIndex={40}>
<div className="bg-elevated text-elevated-content p-4 rounded-control shadow-elevated flex justify-between items-center">
<span className="font-bold">Bottom Action Bar</span>
<button className="bg-success text-success-content px-4 py-2 rounded-control text-sm font-medium">Save Changes</button>
</div>
</Affix>
<div ref={scrollRef} className="h-100 overflow-auto border-2 border-border rounded-surface relative">
<div className="p-4">
<Affix target={scrollRef} position="bottom" offset={10}>
<div className="bg-warning text-warning-content p-3 rounded-control shadow-elevated text-center font-medium">
Scoped to this container
</div>
</Affix>
</div>
</div>Anatomy
import { Affix } from 'vayu-ui';
<Affix offset={20} position="top" role="navigation" aria-label="Sticky navigation">
{/* Content to be affixed */}
</Affix>;- Affix — Root container. Pins its child content to the viewport or a custom scroll container when the scroll threshold is reached.
- Placeholder (internal) — Maintains layout height when the affixed element switches to
position: fixed. Markedaria-hidden="true". - Affixed element (internal) — The visible wrapper that receives
data-affixedand dynamic position styles.
Accessibility
- Keyboard Support:
- The affixed content is not focus-trapped. Users can tab through it normally as part of the document flow.
- All interactive children remain fully keyboard accessible when affixed.
- ARIA Attributes:
- Pass
roleandaria-labelto provide semantic context to screen readers. data-affixedattribute is set on the element when content is affixed, useful for CSS hooks and testing.- The placeholder element uses
aria-hidden="true"to prevent duplicate announcements.
- Pass
- Focus Behavior:
- Focus is not affected by the affix state change.
- Interactive children (buttons, links) remain focusable in both static and affixed states.
Screen reader behavior
When the affix element transitions between static and fixed positioning, no live region announcement is triggered. Screen readers perceive the content as a standard <div> with whatever role and aria-label the developer provides. The placeholder element is hidden from the accessibility tree via aria-hidden="true", so users will not encounter duplicate content. If onAffixed is used to toggle visual indicators, those changes are not announced unless wrapped in a live region (aria-live).
Component Folder Structure
Affix/
├── Affix.tsx # Root component with ref management and composition
├── hooks.ts # useAffixMeasure and useAffixScroll hooks for scroll logic
├── types.ts # AffixPosition and AffixProps type definitions
├── index.ts # Re-exports component and types
└── README.md # Component usage referenceProps
Affix
| Prop | Type | Default | Description |
|---|---|---|---|
offset | number | 0 | Distance in pixels from the viewport or container edge. |
position | "top" | "bottom" | "top" | Which edge to pin to. |
target | HTMLElement | null | null | Custom scroll container. Defaults to window. |
zIndex | number | 100 | Stack order when affixed. |
onAffixed | (affixed: boolean) => void | — | Callback fired when affixed state changes. |
children | ReactNode | — | Content to be affixed. |
className | string | — | Additional CSS classes. |
style | CSSProperties | — | Inline styles merged with affix positioning styles. |
Inherits all standard HTML <div> attributes (e.g., role, aria-label, id).