Premium sticky positioning with visual state indicators, accessibility features, and responsive breakpoints
<link rel="stylesheet" href="styles/sticky-elements.css">
<script src="javascript/sticky-elements.js"></script>
<!-- Basic sticky header -->
<header data-rsl-sticky data-sticky-variant="header">
<nav>...</nav>
</header>
<!-- Sticky sidebar with offset -->
<aside data-rsl-sticky data-sticky-offset="lg" data-sticky-variant="sidebar">
<nav>Table of Contents</nav>
</aside>
<!-- Responsive sticky (only on desktop) -->
<div data-rsl-sticky data-sticky-breakpoint="lg">
Only sticky on screens 1024px and wider
</div>
V2 is fully backward compatible with V1 CSS classes (.sticky-element, .sticky-top, etc.). Existing implementations continue to work unchanged.
| Feature | V1 (CSS Classes) | V2 (Data Attributes) |
|---|---|---|
| Basic Sticky | .sticky-element |
data-rsl-sticky |
| Position Variants | .sticky-top, .sticky-top-md |
data-sticky-offset="md" |
| Responsive | .sticky-md-up, .sticky-lg-up |
data-sticky-breakpoint="md" |
| Z-Index | .sticky-z-50 |
data-sticky-z="high" |
| Stuck Detection | Not available | .rsl-sticky-stuck (auto-applied) |
| Shadow on Stuck | Not available | data-sticky-shadow="lg" |
| Presets | Not available | data-sticky-variant="header" |
| Stacking Groups | Not available | data-sticky-group="nav" |
Configure sticky elements declaratively using data attributes:
| Attribute | Values | Default | Description |
|---|---|---|---|
data-rsl-sticky |
- | - | Initialize V2 sticky behavior on element |
data-sticky-offset |
0, none, xs, sm, md, lg, xl |
0 |
Distance from top when stuck |
data-sticky-variant |
header, sidebar, tabs, toc, action, table-header |
- | Preset configuration for common use cases |
data-sticky-breakpoint |
xs, sm, md, lg, xl, xxl |
- | Only sticky at this breakpoint and above |
data-sticky-z |
low, base, high, overlay |
base |
Z-index layer |
| Attribute | Values | Default | Description |
|---|---|---|---|
data-sticky-shadow |
none, sm, default, lg |
default |
Shadow intensity when stuck |
data-sticky-bg-stuck |
true, false |
false |
Apply frosted glass background when stuck |
data-sticky-border |
true, false |
false |
Show bottom border when stuck |
data-sticky-scale |
true, false |
false |
Slight scale-down effect when stuck |
| Attribute | Values | Description |
|---|---|---|
data-sticky-direction |
up |
Only show when scrolling up (hide on scroll down) |
data-sticky-group |
String | Group name for stacking multiple sticky elements |
data-sticky-stack |
Number | Order within a sticky group (lower = higher) |
data-sticky-skip-link |
true, false |
Add accessibility skip link (default: true for header variant) |
data-sticky-label |
String | Label for screen reader announcements |
data-filterbus-publish |
String | FilterBus key to publish stuck state changes |
| Method | Parameters | Description |
|---|---|---|
RSL.Sticky.init(options) |
Config object | Initialize all [data-rsl-sticky] elements |
RSL.Sticky.create(el, options) |
Element, config object | Create sticky instance on element |
RSL.Sticky.destroy(el) |
Element | Remove sticky behavior from element |
RSL.Sticky.isStuck(el) |
Element | Returns boolean if element is currently stuck |
RSL.Sticky.getState(el) |
Element | Returns full state object |
RSL.Sticky.enable(el) |
Element | Enable sticky behavior |
RSL.Sticky.disable(el) |
Element | Disable sticky behavior |
RSL.Sticky.refresh(el) |
Element (optional) | Recalculate positions (all if no element) |
| Event | Detail Properties | Description |
|---|---|---|
rsl-sticky-change |
element, isStuck, state |
Fired when stuck state changes |
// Initialize all sticky elements
RSL.Sticky.init();
// Create sticky programmatically
const sidebar = document.querySelector('.sidebar');
RSL.Sticky.create(sidebar, {
offset: 'lg',
variant: 'sidebar',
shadow: 'sm'
});
// Check if stuck
if (RSL.Sticky.isStuck(sidebar)) {
console.log('Sidebar is stuck');
}
// Listen for stuck state changes
sidebar.addEventListener('rsl-sticky-change', function(e) {
console.log('Stuck:', e.detail.isStuck);
});
// Temporarily disable
RSL.Sticky.disable(sidebar);
// Re-enable
RSL.Sticky.enable(sidebar);
// Cleanup
RSL.Sticky.destroy(sidebar);
| Class | Description |
|---|---|
.sticky-element |
Basic sticky with 1rem top offset |
.sticky-top |
Sticky at top: 0 |
.sticky-top-sm |
Sticky with 0.5rem offset |
.sticky-top-md |
Sticky with 1rem offset |
.sticky-top-lg |
Sticky with 2rem offset |
.sticky-md-up |
Only sticky at 768px and above |
.sticky-lg-up |
Only sticky at 1024px and above |
.sticky-z-10/20/50/100 |
Z-index values |
| Class | Description |
|---|---|
.rsl-sticky-stuck |
Applied when element is stuck to viewport |
.rsl-sticky-hidden |
Applied when element is hidden (scroll direction) |
.rsl-sticky-show-up |
Applied when visible on scroll up |
.rsl-sticky-boundary |
Applied when element hits boundary |
| Class | Description |
|---|---|
.rsl-sticky-disabled |
Force disable sticky behavior |
.rsl-sticky-forced |
Force enable sticky behavior |
.rsl-sticky-container |
Boundary container for sticky elements |
.rsl-sticky-full-width |
Break out of container for full-width sticky |
Customize the sticky elements system by overriding these CSS variables:
:root {
/* Offsets */
--rsl-sticky-offset: 0;
--rsl-sticky-offset-xs: 0;
--rsl-sticky-offset-sm: 0.5rem;
--rsl-sticky-offset-md: 1rem;
--rsl-sticky-offset-lg: 1.5rem;
--rsl-sticky-offset-xl: 2rem;
/* Z-index layers */
--rsl-sticky-z-base: 100;
--rsl-sticky-z-header: 1000;
--rsl-sticky-z-modal: 1050;
/* Visual effects */
--rsl-sticky-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
--rsl-sticky-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
--rsl-sticky-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.2);
/* Transitions */
--rsl-sticky-transition: all 0.2s ease;
/* Stuck state colors */
--rsl-sticky-bg: rgba(255, 255, 255, 0.98);
--rsl-sticky-border: rgba(0, 0, 0, 0.1);
}
The Sticky Elements component is designed with accessibility as a priority:
aria-live regionsprefers-reduced-motion: reduce is setforced-colors:focus-visibleUse data-sticky-label to provide meaningful context for screen readers. For example: data-sticky-label="Main navigation" will announce "Main navigation is now fixed to the top of the page" when stuck.
Sticky Elements integrates with RSL's FilterBus for both publishing stuck state changes and subscribing to filter controls for visibility filtering.
When a sticky element becomes stuck or unstuck, it can publish this state change to FilterBus, allowing other components to react:
data-filterbus-publish="key" to broadcast stuck state changes{ element, isStuck, variant, timestamp }<!-- Sticky header publishes when stuck -->
<header data-rsl-sticky data-sticky-variant="header" data-filterbus-publish="header-state">
<nav>My App</nav>
</header>
<!-- JavaScript: Subscribe to react to stuck state -->
<script>
RSL.FilterBus.subscribe('header-listener', function(state) {
var headerState = state['header-state'];
if (headerState && headerState.isStuck) {
document.getElementById('back-to-top').classList.add('visible');
} else {
document.getElementById('back-to-top').classList.remove('visible');
}
}, { keys: ['header-state'] });
</script>
Sticky elements can also subscribe to FilterBus to show/hide based on filter values:
data-filter-subscribe="key" to listen to a FilterBus channeldata-filter-value="value" to specify which filter value this element matchesdata-filter-value always show<!-- Filter publishes to FilterBus -->
<div data-rsl-filter data-filter-key="category">
<button data-filter-value="All">All</button>
<button data-filter-value="Sales">Sales</button>
<button data-filter-value="Marketing">Marketing</button>
</div>
<!-- Sticky element subscribes to FilterBus -->
<aside data-rsl-sticky
data-sticky-variant="sidebar"
data-filter-subscribe="category"
data-filter-value="Sales">
Sales Dashboard Sidebar
</aside>
| Attribute | Type | Description |
|---|---|---|
data-filterbus-publish |
String | FilterBus key to publish stuck state changes to |
data-filter-subscribe |
String | FilterBus key to subscribe to (e.g., "region", "status") |
data-filter-value |
String | Filter value(s) this element matches. Supports comma-separated values for multi-match (e.g., "North,South"). Elements without this attribute always show. |
| Event | Detail | Description |
|---|---|---|
rsl-sticky:filterbus-update |
{ state, meta, visible } |
Fired when element visibility changes due to FilterBus update |
View examples.html for comprehensive demos
Test features in playground.html
Browse all RSL components