vwf .

Developer Docs

Quick reference guides with props tables, usage examples, and best practices for rapid development

Components/Docs/SecondaryNav

SecondaryNav

Tab navigation component with 4 visual variants, responsive design, and icon support

SecondaryNav

Standardized secondary navigation component for tab-based interfaces. Features 4 visual variants, 3 sizes, responsive behavior, and HMR state persistence.

Quick Example

🔧 Props Reference

All available props for the SecondaryNav component:

PropTypeDefaultDescription
tabsArray[]Array of tab objects (see Tab Object below)
activeTabvan.Statevan.state(tabs[0]?.id)Currently active tab ID state
defaultTabstringtabs[0]?.idInitial active tab ID
variantstring'default'Style variant: 'default', 'pills', 'underline', 'cards'
sizestring'md'Size variant: 'sm', 'md', 'lg'
responsivebooleantrueEnable responsive mobile abbreviations
showDescriptionbooleanfalseShow tab descriptions (cards variant)
onTabChangeFunctionnullCallback when tab changes: (tabId) => {}
classstring''Additional CSS classes

Tab Object Structure

PropTypeDefaultDescription
idstringrequiredUnique tab identifier
labelstringrequiredDisplay text for the tab
iconstringundefinedOracle JET icon class (e.g., "oj-ux-ico-home")
descriptionstringundefinedOptional description (requires showDescription=true)

💻 Basic Usage

Simple example showing basic SecondaryNav usage with tabs:

const activeTab = van.state('overview');

const tabs = [
  { id: 'overview', label: 'Overview', icon: 'oj-ux-ico-dashboard' },
  { id: 'analytics', label: 'Analytics', icon: 'oj-ux-ico-bar-chart' },
  { id: 'reports', label: 'Reports', icon: 'oj-ux-ico-document' }
];

SecondaryNav({
  tabs,
  activeTab
})

🎨 Variants

SecondaryNav supports 4 visual variants for different use cases:

Default

Sky background for active tabs, white for inactive

SecondaryNav({
  tabs,
  variant: 'default'
})

Pills

White pills in cloud container, shadow on active

SecondaryNav({
  tabs,
  variant: 'pills'
})

Underline

Sunrise bottom border on active tab

SecondaryNav({
  tabs,
  variant: 'underline'
})

Cards

Gradient cards in grid layout with icons and descriptions

SecondaryNav({
  tabs: [
    {
      id: 'feed',
      label: 'Home Feed',
      icon: 'oj-ux-ico-home',
      description: "See what's happening"
    }
  ],
  variant: 'cards',
  showDescription: true
})

📏 Size Variants

SecondaryNav supports three size options to match your design needs:

Small

SecondaryNav({ tabs, size: 'sm' })

Medium (Default)

SecondaryNav({ tabs, size: 'md' })

Large

SecondaryNav({ tabs, size: 'lg' })

🔄 Tab Change Handler

Use the onTabChange callback to respond to tab changes and update your application state:

const activeTab = van.state('all');
const items = van.state([]);

SecondaryNav({
  tabs: [
    { id: 'all', label: 'All' },
    { id: 'active', label: 'Active' },
    { id: 'archived', label: 'Archived' }
  ],
  activeTab,
  onTabChange: (tabId) => {
    console.log('Tab changed to:', tabId);

    // Filter items based on tab
    switch (tabId) {
      case 'all':
        items.val = allItems;
        break;
      case 'active':
        items.val = allItems.filter(item => item.active);
        break;
      case 'archived':
        items.val = allItems.filter(item => item.archived);
        break;
    }
  }
})

📱 Responsive Behavior

SecondaryNav adapts to mobile screens by default (responsive=true):

Mobile Abbreviations

✓Long labels show first word only on mobile

✓Full labels visible on desktop (sm:inline)

✓Descriptions hidden on mobile (hidden md:inline)

✓Horizontal scrolling enabled if needed

// Example: "Service Dashboard" → "Service" on mobile
{ id: 'dashboard', label: 'Service Dashboard' }

// Disable responsive behavior:
SecondaryNav({ tabs, responsive: false })

🔥 HMR State Persistence

SecondaryNav preserves active tab across hot module reloads:

// Internal state stored globally
window.__secondaryNavState = {
  [navId]: activeTab
};

// Benefits:
// - Save code → tab stays active
// - Fast iteration without losing context
// - Automatic across all SecondaryNav instances

⚡️ Best Practices

✓ Do This

✓Use unique tab IDs

✓Keep labels short (1-2 words)

✓Use icons for visual scanning

✓Match variant to context (pills for filters)

✓Provide onTabChange handler

✓Test responsive behavior

✓Use descriptions for cards variant

✗ Avoid This

✗Use duplicate tab IDs

✗Have too many tabs (>6)

✗Use long labels (causes mobile wrapping)

✗Forget onTabChange callback

✗Mix SecondaryNav with NavigationList

✗Forget defaultTab prop

✗Use descriptions without showDescription=true

✨ Key Features

★

4 Visual Variants

Default, pills, underline, and rich card styles

★

3 Size Options

Small, medium, and large to match your design

★

Responsive Mobile

Auto-abbreviates labels, horizontal scroll

★

HMR Persistence

Tab state survives hot reloads

★

Accessible

ARIA roles, tab semantics, keyboard nav

★

Icon Support

Oracle JET icons for better visual hierarchy

💡 Common Use Cases

Common patterns for implementing SecondaryNav in your applications:

1. Filter Tabs (Pills Variant)

const activeFilter = van.state('all');
const items = van.state([]);

div(
  // Filter tabs
  SecondaryNav({
    tabs: [
      { id: 'all', label: 'All' },
      { id: 'active', label: 'Active' },
      { id: 'paused', label: 'Paused' }
    ],
    activeTab: activeFilter,
    variant: 'pills',
    onTabChange: (status) => {
      items.val = allItems.filter(item => item.status === status);
    }
  }),

  // Filtered content
  () => items.val.map(item => ItemCard(item))
)

2. Dashboard Sections (Default Variant)

const activeSection = van.state('overview');

div(
  SecondaryNav({
    tabs: [
      { id: 'overview', label: 'Overview', icon: 'oj-ux-ico-dashboard' },
      { id: 'analytics', label: 'Analytics', icon: 'oj-ux-ico-bar-chart' },
      { id: 'reports', label: 'Reports', icon: 'oj-ux-ico-document' }
    ],
    activeTab: activeSection
  }),

  // Section content
  () => {
    switch (activeSection.val) {
      case 'overview': return OverviewSection();
      case 'analytics': return AnalyticsSection();
      case 'reports': return ReportsSection();
    }
  }
)

3. Settings Categories (Underline Variant)

const activeSetting = van.state('profile');

div(
  SecondaryNav({
    tabs: [
      { id: 'profile', label: 'Profile', icon: 'oj-ux-ico-user' },
      { id: 'security', label: 'Security', icon: 'oj-ux-ico-lock' },
      { id: 'notifications', label: 'Notifications', icon: 'oj-ux-ico-bell' },
      { id: 'billing', label: 'Billing', icon: 'oj-ux-ico-credit-card' }
    ],
    activeTab: activeSetting,
    variant: 'underline'
  }),

  // Settings content
  () => {
    switch (activeSetting.val) {
      case 'profile': return ProfileSettings();
      case 'security': return SecuritySettings();
      case 'notifications': return NotificationSettings();
      case 'billing': return BillingSettings();
    }
  }
)

4. Dashboard Cards (Cards Variant)

const activeView = van.state('feed');

SecondaryNav({
  tabs: [
    {
      id: 'feed',
      label: 'Home Feed',
      icon: 'oj-ux-ico-home',
      description: "See what's happening in your neighborhood"
    },
    {
      id: 'services',
      label: 'Services',
      icon: 'oj-ux-ico-grid',
      description: 'Browse available services and offers'
    },
    {
      id: 'neighbors',
      label: 'Neighbors',
      icon: 'oj-ux-ico-people',
      description: 'Connect with people nearby'
    },
    {
      id: 'events',
      label: 'Events',
      icon: 'oj-ux-ico-calendar',
      description: 'Find local events and gatherings'
    }
  ],
  activeTab: activeView,
  variant: 'cards',
  showDescription: true
})

📚 Complete Example

Full working example showing SecondaryNav with data fetching and state management:

import van from 'vanjs-core';
import { SecondaryNav } from 'shared-components/layout/SecondaryNav.js';

const { div, h1 } = van.tags;

export const ServicesPage = () => {
  // State
  const activeTab = van.state('all');
  const services = van.state([]);

  // Tabs
  const tabs = [
    { id: 'all', label: 'All Services' },
    { id: 'mine', label: 'My Services' },
    { id: 'favorites', label: 'Favorites' }
  ];

  // Load data
  const loadServices = async (filter) => {
    const data = await fetch(`/api/services?filter=${filter}`);
    services.val = await data.json();
  };

  return div(
    { class: 'container mx-auto p-4' },

    h1({ class: 'text-3xl font-bold mb-6' }, 'Services'),

    // Navigation tabs
    SecondaryNav({
      tabs,
      activeTab,
      variant: 'pills',
      onTabChange: (tabId) => {
        loadServices(tabId);
      }
    }),

    // Content
    div(
      { class: 'mt-6 grid md:grid-cols-3 gap-4' },
      () => services.val.map(service => ServiceCard(service))
    )
  );
};

Explore More Components

Browse other component categories