This guide documents the current implementation of floating search and filter boxes in the Divemap application. The solution ensures that search boxes and filter controls float together as a cohesive unit below the navbar during scrolling, working consistently across desktop and mobile devices.
✅ Fully Implemented - This pattern is actively used in:
- DiveSites page (
/frontend/src/pages/DiveSites.js) - DivingCenters page (
/frontend/src/pages/DivingCenters.js) - DiveTrips page (
/frontend/src/pages/DiveTrips.js)
- Desktop view: Search box and filters must float together RIGHT BELOW the desktop navbar
- Mobile view: Search box and filters must float together RIGHT BELOW the mobile navbar
- No space between navbar and search box
- No space between search box and filters box
- Navbar must always be visible during scrolling
- Responsive behavior must work on both device types
The implementation uses a custom CSS class .sticky-below-navbar defined in /frontend/src/index.css:
/* Sticky Positioning for Search and Filters */
.sticky-below-navbar {
position: sticky;
top: var(--sticky-offset-desktop);
z-index: 40;
}
@media (max-width: 768px) {
.sticky-below-navbar {
top: var(--sticky-offset-mobile);
}
}CSS Variables:
:root {
--navbar-height-desktop: 4rem; /* 64px - matches h-16 */
--navbar-height-mobile: 4rem; /* 64px - matches h-16 */
--sticky-offset-desktop: calc(var(--navbar-height-desktop) + 0px); /* No gap */
--sticky-offset-mobile: calc(var(--navbar-height-mobile) + 0px); /* No gap */
}The current implementation uses this structure across all pages:
{/* Sticky Filter Bar - Mobile-First Responsive Design */}
<div className='sticky-below-navbar bg-white shadow-sm border-b border-gray-200 rounded-t-lg py-3 sm:py-4'>
{/* Desktop Search Bar - Only visible on desktop/tablet */}
{!isMobile && (
<DesktopSearchBar
searchValue={filters.search_query}
onSearchChange={value => handleFilterChange('search_query', value)}
onSearchSelect={selectedItem => {
handleFilterChange('search_query', selectedItem.name);
}}
data={diveSites?.results || []}
configType='diveSites'
placeholder='Search dive sites by name, country, region, or description...'
/>
)}
<ResponsiveFilterBar
showFilters={showAdvancedFilters}
onToggleFilters={() => setShowAdvancedFilters(!showAdvancedFilters)}
onClearFilters={clearFilters}
activeFiltersCount={getActiveFiltersCount()}
filters={{ ...filters, availableTags, user }}
onFilterChange={handleFilterChange}
onQuickFilter={handleQuickFilter}
quickFilter={quickFilter}
variant='inline'
showQuickFilters={true}
showAdvancedToggle={true}
searchQuery={filters.search_query}
onSearchChange={value => handleFilterChange('search_query', value)}
onSearchSubmit={() => {}}
// Additional props for sorting and view modes
sortBy={sortBy}
sortOrder={sortOrder}
sortOptions={getSortOptions('dive-sites')}
onSortChange={handleSortChange}
onReset={resetSorting}
viewMode={viewMode}
onViewModeChange={handleViewModeChange}
compactLayout={compactLayout}
onDisplayOptionChange={handleDisplayOptionChange}
/>
</div>- Container:
sticky-below-navbar bg-white shadow-sm border-b border-gray-200 rounded-t-lg py-3 sm:py-4 - Z-index:
40(same as navbar, ensuring proper layering) - Positioning: Uses CSS variables for consistent top positioning
- Sticky elements z-index:
40(used by.sticky-below-navbarand mobile sticky headers) - Sticky container z-index:
40(same as other sticky elements for proper layering) - Search suggestions z-index:
50(defined in FuzzySearchInput component to appear above filters) - Modal overlays z-index:
200(for full-screen overlays and modals)
- Top value: Uses CSS variables
--sticky-offset-desktopand--sticky-offset-mobile(both 64px) - Responsive: Same positioning for both mobile and desktop (64px from top)
- Sticky behavior: Uses
position: stickyfor smooth scrolling
- Between search and filters: No borders or margins - uses padding only
- Container padding:
py-3 sm:py-4for consistent responsive padding - Background: Solid white background with shadow for visual separation
// From /frontend/src/pages/DiveSites.js
<div className='sticky-below-navbar bg-white shadow-sm border-b border-gray-200 rounded-t-lg py-3 sm:py-4'>
{/* Desktop Search Bar - Only visible on desktop/tablet */}
{!isMobile && (
<DesktopSearchBar
searchValue={filters.search_query}
onSearchChange={value => handleFilterChange('search_query', value)}
onSearchSelect={selectedItem => {
handleFilterChange('search_query', selectedItem.name);
}}
data={diveSites?.results || []}
configType='diveSites'
placeholder='Search dive sites by name, country, region, or description...'
/>
)}
<ResponsiveFilterBar
showFilters={showAdvancedFilters}
onToggleFilters={() => setShowAdvancedFilters(!showAdvancedFilters)}
onClearFilters={clearFilters}
activeFiltersCount={getActiveFiltersCount()}
filters={{ ...filters, availableTags, user }}
onFilterChange={handleFilterChange}
onQuickFilter={handleQuickFilter}
quickFilter={quickFilter}
variant='inline'
showQuickFilters={true}
showAdvancedToggle={true}
searchQuery={filters.search_query}
onSearchChange={value => handleFilterChange('search_query', value)}
onSearchSubmit={() => {}}
sortBy={sortBy}
sortOrder={sortOrder}
sortOptions={getSortOptions('dive-sites')}
onSortChange={handleSortChange}
onReset={resetSorting}
viewMode={viewMode}
onViewModeChange={handleViewModeChange}
compactLayout={compactLayout}
onDisplayOptionChange={handleDisplayOptionChange}
/>
</div>// From /frontend/src/pages/DivingCenters.js
<div className='sticky-below-navbar bg-white shadow-lg border border-gray-200 rounded-lg mx-3 sm:mx-4 lg:mx-6 xl:mx-8 mb-6'>
{/* Desktop Search Bar - Only visible on desktop/tablet */}
{!isMobile && (
<DivingCentersDesktopSearchBar
searchValue={filters.search}
onSearchChange={value => handleSearchChange({ target: { name: 'search', value } })}
onSearchSelect={selectedItem => {
handleSearchChange({ target: { name: 'search', value: selectedItem.name } });
}}
data={divingCenters || []}
configType='divingCenters'
placeholder='Search diving centers by name, location, or services...'
/>
)}
<DivingCentersResponsiveFilterBar
showFilters={showAdvancedFilters}
onToggleFilters={() => setShowAdvancedFilters(!showAdvancedFilters)}
onClearFilters={clearFilters}
activeFiltersCount={getActiveFiltersCount()}
filters={{ ...filters, availableTags, user }}
onFilterChange={handleFilterChange}
onQuickFilter={handleQuickFilter}
quickFilter={quickFilter}
variant='inline'
showQuickFilters={true}
showAdvancedToggle={true}
searchQuery={filters.search}
onSearchChange={value => handleSearchChange({ target: { name: 'search', value } })}
onSearchSubmit={() => {}}
sortBy={sortBy}
sortOrder={sortOrder}
sortOptions={getSortOptions('diving-centers')}
onSortChange={handleSortChange}
onReset={resetSorting}
viewMode={viewMode}
onViewModeChange={handleViewModeChange}
compactLayout={compactLayout}
onDisplayOptionChange={handleDisplayOptionChange}
/>
</div>// From /frontend/src/pages/DiveTrips.js
<div className='sticky-below-navbar bg-white shadow-sm border-b border-gray-200 rounded-t-lg px-3 sm:px-4 lg:px-6 xl:px-8 py-3 sm:py-4'>
<div className='flex flex-col sm:flex-row gap-3 sm:gap-4 items-end'>
{/* Smart Fuzzy Search Input - Enhanced search experience */}
<div className='flex-1 w-full'>
<label htmlFor='search-query' className='block text-sm font-medium text-gray-700 mb-2'>
Search
</label>
<FuzzySearchInput
data={trips || []}
searchValue={filters.search_query}
onSearchChange={value => handleFilterChange('search_query', value)}
onSearchSelect={selectedItem => {
handleFilterChange('search_query', selectedItem.name);
}}
configType='diveTrips'
placeholder='Search dive trips by name, location, or description...'
minQueryLength={2}
maxSuggestions={8}
debounceDelay={300}
showSuggestions={true}
highlightMatches={true}
showScore={false}
showClearButton={true}
className='w-full'
inputClassName='w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base'
suggestionsClassName='absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-96 overflow-y-auto'
highlightClass='bg-blue-100 font-medium'
/>
</div>
{/* Filter Controls */}
<ResponsiveFilterBar
showFilters={showAdvancedFilters}
onToggleFilters={() => setShowAdvancedFilters(!showAdvancedFilters)}
onClearFilters={clearFilters}
activeFiltersCount={getActiveFiltersCount()}
filters={{ ...filters, availableTags, user }}
onFilterChange={handleFilterChange}
onQuickFilter={handleQuickFilter}
quickFilter={quickFilter}
variant='inline'
showQuickFilters={true}
showAdvancedToggle={true}
searchQuery={filters.search_query}
onSearchChange={value => handleFilterChange('search_query', value)}
onSearchSubmit={() => {}}
sortBy={sortBy}
sortOrder={sortOrder}
sortOptions={getSortOptions('dive-trips')}
onSortChange={handleSortChange}
onReset={resetSorting}
viewMode={viewMode}
onViewModeChange={handleViewModeChange}
compactLayout={compactLayout}
onDisplayOptionChange={handleDisplayOptionChange}
/>
</div>
</div>- Always use CSS variables for positioning:
--sticky-offset-desktopand--sticky-offset-mobile - Maintain consistency with navbar height variables:
--navbar-height-desktopand--navbar-height-mobile - Avoid hardcoded values - use the
.sticky-below-navbarclass instead of custom positioning
- Use ResponsiveFilterBar for consistent filter functionality across pages
- Use DesktopSearchBar for desktop search with fuzzy search capabilities
- Use FuzzySearchInput directly for custom search implementations
- Pass all required props to maintain functionality
- Sticky container: Use
z-index: 40(same as other sticky elements) - Search suggestions: Use
z-index: 50or higher - Modal overlays: Use
z-index: 200or higher - Avoid conflicts by checking existing z-index values before adding new elements
/* Z-Index Hierarchy in Divemap */
z-index: 10 /* Dropdown menus, tooltips */
z-index: 40 /* Sticky elements (navbar, search filters) */
z-index: 50 /* Search suggestions, floating elements */
z-index: 100 /* Fixed overlays */
z-index: 200 /* Modal dialogs, full-screen overlays */- Mobile-first approach: Design for mobile first, then enhance for desktop
- Consistent spacing: Use
py-3 sm:py-4for responsive padding - Conditional rendering: Use
!isMobilefor desktop-only components
- Centralize filter state in the parent component
- Use URL parameters for filter persistence
- Implement proper cleanup for filter changes
- Debounce search input to avoid excessive API calls (300ms recommended)
- Use React.memo for filter components that don't change frequently
- Lazy load filter options when possible
- Optimize re-renders by using useCallback for event handlers
- Touch targets: Minimum 44px height for mobile touch targets
- Keyboard navigation: Ensure all interactive elements are keyboard accessible
- Screen readers: Use proper ARIA labels and roles
- Focus management: Maintain focus order when toggling filters
- Color contrast: Ensure sufficient contrast for text and interactive elements
- Sticky container uses
.sticky-below-navbarclass - Search box and filters are direct children of sticky container
- No borders between search and filters sections
- Z-index is set to
40(same as navbar) - Background is solid white with appropriate shadow
- CSS variables are used for positioning
- Search box and filters appear together below navbar
- No gaps between navbar and search box
- No gaps between search box and filters
- Both elements remain visible when scrolling
- Navbar remains visible during scrolling
- Shadow provides proper visual separation
- Desktop: Elements float at 64px from top (below navbar)
- Mobile: Elements float at 64px from top (below navbar)
- Both device types show identical floating behavior
- No horizontal scrolling issues on mobile
- Touch targets are appropriate for mobile (44px minimum)
Location: /frontend/src/components/ResponsiveFilterBar.js
Purpose: Provides a responsive filter bar with search, sorting, and view mode controls.
Key Props:
showFilters: Boolean to control advanced filters visibilityonToggleFilters: Function to toggle advanced filtersonClearFilters: Function to clear all filtersactiveFiltersCount: Number of active filtersfilters: Object containing filter values and available optionsonFilterChange: Function to handle filter changessearchQuery: Current search query valueonSearchChange: Function to handle search input changessortBy,sortOrder,sortOptions: Sorting configurationviewMode,onViewModeChange: View mode controlscompactLayout: Display option (thumbnails removed)
Usage Pattern:
<ResponsiveFilterBar
showFilters={showAdvancedFilters}
onToggleFilters={() => setShowAdvancedFilters(!showAdvancedFilters)}
onClearFilters={clearFilters}
activeFiltersCount={getActiveFiltersCount()}
filters={{ ...filters, availableTags, user }}
onFilterChange={handleFilterChange}
searchQuery={filters.search_query}
onSearchChange={value => handleFilterChange('search_query', value)}
// ... other props
/>Location: /frontend/src/components/DesktopSearchBar.js
Purpose: Provides desktop-specific search functionality with fuzzy search capabilities.
Key Props:
searchValue: Current search valueonSearchChange: Function to handle search input changesonSearchSelect: Function to handle search result selectiondata: Array of data to search throughconfigType: Configuration type for search behaviorplaceholder: Search input placeholder text
Usage Pattern:
<DesktopSearchBar
searchValue={filters.search_query}
onSearchChange={value => handleFilterChange('search_query', value)}
onSearchSelect={selectedItem => {
handleFilterChange('search_query', selectedItem.name);
}}
data={diveSites?.results || []}
configType='diveSites'
placeholder='Search dive sites by name, country, region, or description...'
/>Purpose: Positions elements to float below the navbar during scrolling.
CSS Properties:
position: stickytop: var(--sticky-offset-desktop)(desktop)top: var(--sticky-offset-mobile)(mobile)z-index: 40
Usage:
<div className='sticky-below-navbar bg-white shadow-sm border-b border-gray-200'>
{/* Content */}
</div>- Desktop view: Open page in desktop browser, scroll down to verify floating
- Mobile view: Resize browser to mobile dimensions (375x667), scroll to verify floating
- Scroll behavior: Scroll up/down to ensure smooth sticky behavior
- Content visibility: Verify no background content shows through floating elements
- Touch interaction: Test touch targets on mobile devices
- Check z-index values: Use "Computed" tab to verify actual z-index values
- Inspect stacking context: Look for
position: relativeorposition: absolutethat might create new stacking contexts - Verify CSS variables: Check that
--sticky-offset-desktopand--sticky-offset-mobileare properly defined - Test responsive behavior: Use device emulation to test mobile vs desktop behavior
// Check if sticky positioning is working
const stickyElement = document.querySelector('.sticky-below-navbar');
const computedStyle = window.getComputedStyle(stickyElement);
console.log('Position:', computedStyle.position);
console.log('Top:', computedStyle.top);
console.log('Z-index:', computedStyle.zIndex);
// Check CSS variables
const root = document.documentElement;
const stickyOffset = getComputedStyle(root).getPropertyValue('--sticky-offset-desktop');
console.log('Sticky offset:', stickyOffset);// Test floating behavior on mobile
await page.setViewportSize({ width: 375, height: 667 });
await page.evaluate(() => window.scrollTo(0, 300));
// Verify elements are visible after scrolling
const searchBox = await page.$('input[placeholder*="Search"]');
const filtersButton = await page.$('button');
const searchBoxVisible = await searchBox.isVisible();
const filtersButtonVisible = await filtersButton.isVisible();
// Both should be visible (floating)
assert(searchBoxVisible && filtersButtonVisible);// Check element positions after scrolling
const searchBoxRect = await searchBox.boundingBox();
const filtersButtonRect = await filtersButton.boundingBox();
// Both should be above viewport top (floating)
assert(searchBoxRect.y >= 0);
assert(filtersButtonRect.y >= 0);Solution: Ensure both elements are direct children of the same sticky container using .sticky-below-navbar class
Solution: Check z-index values - sticky container should use z-index: 40 (same as other sticky elements). If elements still appear behind, check for conflicting z-index values in parent containers.
Solution: Remove borders and use consistent padding only (py-3 sm:py-4)
Solution: Use CSS variables --sticky-offset-desktop and --sticky-offset-mobile - both are 64px
Solution: Ensure search suggestions use z-index: 50 or higher to appear above filters
Solution: Use min-h-[44px] class for touch targets and test on actual mobile devices
Solution:
- Check the z-index hierarchy reference above
- Use browser dev tools to inspect element stacking context
- Ensure parent containers don't create new stacking contexts
- Use
z-index: 50or higher for elements that need to appear above search filters
Solution: Ensure search suggestions use z-index: 50 or higher. Check that the suggestions container doesn't have position: relative with a lower z-index that creates a new stacking context.
When implementing floating search and filters on a new page:
- Import required components (
ResponsiveFilterBar,DesktopSearchBar,FuzzySearchInput) - Add
.sticky-below-navbarclass to container div - Include proper background and shadow classes
- Set up state management for filters and search
- Pass all required props to components
- Implement debounced search (300ms recommended)
- Set up URL parameter persistence
- Configure proper z-index values
- Test on both desktop and mobile viewports
- Verify z-index layering is correct
- Test keyboard navigation
- Verify screen reader compatibility
- Ensure touch targets meet 44px minimum
- Add proper ARIA labels and roles
- Test focus management
- Verify color contrast ratios
- Optimize re-renders with useCallback
- Consider React.memo for static components
- Implement lazy loading where appropriate
- Test with slow network conditions
const FloatingSearchFilters = ({
searchComponent,
filtersComponent,
className = ''
}) => {
return (
<div className={`sticky-below-navbar bg-white shadow-sm border-b border-gray-200 rounded-t-lg py-3 sm:py-4 ${className}`}>
{searchComponent}
{filtersComponent}
</div>
);
};The implementation is successful when:
- Search box and filters float together as one unit
- Both elements remain visible during scrolling
- No gaps exist between navbar, search, and filters
- Behavior is identical on desktop and mobile
- Navbar remains visible and accessible
- No content shows through the floating elements
- Touch targets are appropriate for mobile devices
- Z-index layering works correctly
- This solution works because both mobile and desktop navbars have the same height (64px)
- CSS variables ensure consistent positioning across devices
- Z-index management prevents layering conflicts
- The sticky container approach ensures both elements move together as one unit
- Mobile-first responsive design ensures optimal experience on all devices
- JavaScript Style Rules
- Frontend Development Guidelines
- Testing Strategy
- CSS and Sticky Positioning Guide
Last Updated: September 13, 2025
Author: AI Assistant
Version: 2.1 - Enhanced with debugging tools, performance guidelines, and accessibility considerations