
Implementing Navigation
- 54 installs
- 426 repo stars
- Updated December 11, 2025
- ancoleman/ai-design-components
implementing-navigation is a Claude Code skill that implements navigation patterns and routing for both React/TypeScript frontends and Python backends, including menus, tabs, breadcrumbs, and client- and server-side rout
About
This skill implements navigation patterns and routing across frontend and backend applications. On the frontend it covers menus, tabs, breadcrumbs, and client-side routing with React Router or Next.js; on the backend it covers route configuration for Flask, Django, and FastAPI. Developers use it when building navigation systems, and it includes a decision framework mapping information architecture to navigation patterns plus WCAG 2.1 AA accessibility.
- Navigation and routing for frontend (React/TS) and backend (Python) apps
- Covers menus, tabs, breadcrumbs, client-side routing, and server-side route config
- Includes a navigation decision framework and WCAG 2.1 AA accessibility patterns
Implementing Navigation by the numbers
- 54 all-time installs (skills.sh)
- Ranked #1,275 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
implementing-navigation capabilities & compatibility
- Capabilities
- menu patterns · breadcrumbs · client routing · server routing
- Use cases
- frontend · ui design · api development
- Pricing
- Free
What implementing-navigation says it does
Implements navigation patterns and routing for both frontend (React/TS) and backend (Python) including menus, tabs, breadcrumbs, client-side routing, and server-side route configuration.
5-7 primary links maximum for cognitive load
npx skills add https://github.com/ancoleman/ai-design-components --skill implementing-navigationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 54 |
|---|---|
| repo stars | ★ 426 |
| Last updated | December 11, 2025 |
| Repository | ancoleman/ai-design-components ↗ |
What it does
Building navigation systems and routing (menus, tabs, breadcrumbs, React Router/Next.js, Flask/Django/FastAPI).
Who is it for?
Full-stack apps needing accessible navigation UIs plus React/Next.js and Python routing.
Skip if: Single-page prototypes with no real navigation hierarchy.
When should I use this skill?
You are building navigation systems or setting up client-side or server-side routing.
What you get
Accessible navigation components and routing configured for the app's information architecture.
- Navigation components (menus, tabs, breadcrumbs)
- Client-side route configuration
- Server-side route configuration
By the numbers
- 5-7 primary links maximum recommended for top nav
- 3-5 primary actions for bottom navigation
Files
Navigation Patterns & Routing Implementation
Purpose
This skill provides comprehensive guidance for implementing navigation systems across both frontend and backend applications. It covers client-side navigation patterns (menus, tabs, breadcrumbs) and routing (React Router, Next.js) as well as server-side route configuration (Flask, Django, FastAPI).
When to Use
Use this skill when:
- Building primary navigation (top, side, mega menus)
- Implementing secondary navigation (breadcrumbs, tabs, pagination)
- Setting up client-side routing (React Router, Next.js)
- Configuring server-side routes (Flask, Django, FastAPI)
- Creating mobile navigation patterns (hamburger, bottom nav)
- Implementing keyboard-accessible navigation
- Building command palettes or search-driven navigation
- Creating multi-step wizards or steppers
- Ensuring WCAG 2.1 AA compliance for navigation
Navigation Decision Framework
Information Architecture → Navigation Pattern
Flat (1-2 levels) → Top Navigation
Deep (3+ levels) → Side Navigation
E-commerce/Large → Mega Menu
Linear Process → Stepper/Wizard
Long Content → Table of Contents
Power Users → Command Palette
Multi-section Page → Tabs
Large Data Sets → PaginationFrontend Navigation Components
Primary Navigation Patterns
Top Navigation (Horizontal)
- Best for shallow hierarchies, marketing sites
- 5-7 primary links maximum for cognitive load
- See
references/menu-patterns.mdfor implementation
Side Navigation (Vertical)
- Best for deep hierarchies, admin panels, dashboards
- Supports multi-level nesting and collapsible sections
- See
references/menu-patterns.mdfor sidebar patterns
Mega Menu
- Best for e-commerce, content-heavy sites
- Rich content with images and descriptions
- See
references/menu-patterns.mdfor mega menu structure
Secondary Navigation Components
Breadcrumbs
- Shows hierarchical path and current location
- Essential for deep sites and e-commerce
- See
references/navigation-components.mdfor breadcrumb patterns
Tabs
- Content switching without page reload
- URL synchronization for bookmarking
- See
references/navigation-components.mdfor tab implementation
Pagination
- For search results, product lists, articles
- Consider virtualization for performance
- See
references/navigation-components.mdfor pagination patterns
Client-Side Routing
React Router (Industry Standard)
- Type-safe routing with loader patterns
- Nested routes and lazy loading support
- See
references/client-routing.mdfor React Router patterns
Next.js App Router
- File-based routing with RSC support
- Parallel and intercepting routes
- See
references/client-routing.mdfor Next.js routing
Backend Routing Patterns
Python Web Frameworks
Flask
- Blueprint-based organization
- Route decorators and URL rules
- See
references/flask-routing.mdfor Flask patterns
Django
- URL configuration with namespaces
- Path converters and regex patterns
- See
references/django-urls.mdfor Django URL conf
FastAPI
- Router-based organization
- Path operations and dependencies
- See
references/fastapi-routing.mdfor FastAPI routers
Mobile Navigation
Patterns for Touch Devices
Hamburger Menu
- Slide-out drawer for primary navigation
- See
references/menu-patterns.mdfor mobile drawer
Bottom Navigation
- 3-5 primary actions, thumb-friendly
- See
references/menu-patterns.mdfor bottom nav
Tab Bar
- Horizontal scrollable tabs with swipe
- Natural for mobile-first applications
Accessibility Requirements
Keyboard Navigation
Tab → Move forward through links
Shift+Tab → Move backward through links
Enter → Activate link/button
Space → Activate button
Arrow keys → Navigate within menus
Escape → Close dropdowns/modalsARIA Patterns
Essential ARIA attributes for accessible navigation:
- See
references/accessibility-navigation.mdfor complete ARIA patterns - Includes landmark roles, states, and properties
- Screen reader optimization techniques
Focus Management
- Visible focus indicators (2px minimum, 3:1 contrast)
- Focus trap for modals and dropdowns
- Skip navigation link for keyboard users
- See
references/accessibility-navigation.mdfor focus patterns
Implementation Utilities
Navigation Structure Management
Generate and validate navigation trees:
# Validate navigation structure
node scripts/validate_navigation_tree.js nav-config.json
# Generate breadcrumb trails
node scripts/calculate_breadcrumbs.js current-pathRoute Generation (Python)
Generate route configurations:
# Generate Flask/Django/FastAPI routes
python scripts/generate_routes.py --framework flask --config routes.yamlCode Examples
Frontend Examples
For working navigation implementations:
examples/horizontal-menu.tsx- Responsive top navigationexamples/tab-navigation.tsx- Tabs with URL syncexamples/mobile-navigation.tsx- Hamburger and drawer
Backend Examples
For routing configuration:
examples/flask_routes.py- Flask blueprint setupexamples/django_urls.py- Django URL patternsexamples/fastapi_routes.py- FastAPI router organization
Navigation Configuration
For complex navigation structures, use the configuration schema:
assets/navigation-config-schema.json- Navigation tree schemaassets/route-templates.json- Common route patterns
Validate configurations before implementation using the validation script.
Library Recommendations
Frontend Routing
React Router is the recommended solution for React applications:
- Industry standard with excellent TypeScript support
- Built-in accessibility with NavLink active states
- See
references/library-comparison.mdfor alternatives
Component Libraries
For rapid development, consider:
- Headless UI libraries (Radix UI, React Aria)
- Accessible by default
- Work with any styling approach
Progressive Enhancement
Build navigation that works without JavaScript:
- Server-rendered HTML navigation
- Progressive enhancement with client-side routing
- Fallback for JavaScript failures
Performance Considerations
- Lazy load route components
- Prefetch navigation targets
- Use route-based code splitting
- Implement loading states for navigation
Testing Navigation
Essential navigation tests:
- Keyboard navigation flow
- Screen reader announcements
- Mobile touch interactions
- Route parameter validation
- Deep linking functionality
Next Steps
1. Analyze the information architecture 2. Select appropriate navigation pattern 3. Implement with accessibility first 4. Add progressive enhancement 5. Test across devices and assistive technologies
For detailed implementation guides, explore the referenced documentation files based on specific requirements.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Navigation Configuration Schema",
"description": "Schema for defining navigation structure with accessibility and responsive support",
"type": "object",
"required": ["items"],
"properties": {
"skipLink": {
"type": "boolean",
"description": "Include skip navigation link for accessibility",
"default": true
},
"keyboardNavigation": {
"type": "boolean",
"description": "Enable keyboard navigation support",
"default": true
},
"landmarkRole": {
"type": "string",
"description": "ARIA landmark role for navigation",
"enum": ["navigation", "menu"],
"default": "navigation"
},
"mobile": {
"type": "object",
"description": "Mobile navigation configuration",
"properties": {
"breakpoint": {
"type": "number",
"description": "Responsive breakpoint in pixels",
"default": 768
},
"type": {
"type": "string",
"description": "Mobile navigation pattern",
"enum": ["hamburger", "bottom-nav", "tab-bar"],
"default": "hamburger"
},
"animation": {
"type": "string",
"description": "Animation type for mobile menu",
"enum": ["slide", "fade", "none"],
"default": "slide"
}
}
},
"focusIndicators": {
"type": "object",
"description": "Focus indicator styling",
"properties": {
"style": {
"type": "string",
"enum": ["outline", "box-shadow", "underline"],
"default": "outline"
},
"color": {
"type": "string",
"description": "CSS color value",
"default": "#0066cc"
},
"width": {
"type": "number",
"description": "Width in pixels",
"default": 2
},
"offset": {
"type": "number",
"description": "Offset in pixels",
"default": 2
}
}
},
"items": {
"type": "array",
"description": "Navigation items",
"items": {
"$ref": "#/definitions/navigationItem"
}
},
"footer": {
"type": "array",
"description": "Footer navigation items",
"items": {
"$ref": "#/definitions/navigationItem"
}
},
"breadcrumbs": {
"type": "object",
"description": "Breadcrumb configuration",
"properties": {
"enabled": {
"type": "boolean",
"default": true
},
"separator": {
"type": "string",
"description": "Separator character or icon",
"default": "/"
},
"maxItems": {
"type": "number",
"description": "Maximum visible items before collapsing",
"default": 5
},
"homeLabel": {
"type": "string",
"description": "Label for home breadcrumb",
"default": "Home"
}
}
},
"search": {
"type": "object",
"description": "Search configuration in navigation",
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"placeholder": {
"type": "string",
"default": "Search..."
},
"position": {
"type": "string",
"enum": ["left", "right", "center"],
"default": "right"
},
"hotkey": {
"type": "string",
"description": "Keyboard shortcut (e.g., 'cmd+k')",
"default": "cmd+k"
}
}
},
"theme": {
"type": "object",
"description": "Navigation theming",
"properties": {
"variant": {
"type": "string",
"enum": ["light", "dark", "transparent", "custom"],
"default": "light"
},
"sticky": {
"type": "boolean",
"description": "Sticky navigation on scroll",
"default": true
},
"shadow": {
"type": "boolean",
"description": "Show shadow/border",
"default": true
},
"height": {
"type": "number",
"description": "Navigation height in pixels",
"default": 64
}
}
},
"analytics": {
"type": "object",
"description": "Analytics tracking configuration",
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"trackClicks": {
"type": "boolean",
"default": true
},
"trackHover": {
"type": "boolean",
"default": false
},
"trackSearch": {
"type": "boolean",
"default": true
}
}
}
},
"definitions": {
"navigationItem": {
"type": "object",
"required": ["label"],
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the navigation item"
},
"label": {
"type": "string",
"description": "Display text for the navigation item"
},
"href": {
"type": "string",
"description": "URL or route path"
},
"target": {
"type": "string",
"description": "Link target attribute",
"enum": ["_self", "_blank", "_parent", "_top"]
},
"external": {
"type": "boolean",
"description": "Mark as external link",
"default": false
},
"icon": {
"type": "string",
"description": "Icon identifier or URL"
},
"iconPosition": {
"type": "string",
"enum": ["left", "right"],
"default": "left"
},
"badge": {
"type": ["string", "number"],
"description": "Badge content (e.g., notification count)"
},
"badgeColor": {
"type": "string",
"description": "Badge color variant",
"enum": ["primary", "secondary", "success", "danger", "warning", "info"]
},
"children": {
"type": "array",
"description": "Nested navigation items",
"items": {
"$ref": "#/definitions/navigationItem"
}
},
"active": {
"type": "boolean",
"description": "Mark as currently active",
"default": false
},
"disabled": {
"type": "boolean",
"description": "Disable this navigation item",
"default": false
},
"hidden": {
"type": "boolean",
"description": "Hide this item",
"default": false
},
"roles": {
"type": "array",
"description": "Required user roles to see this item",
"items": {
"type": "string"
}
},
"permissions": {
"type": "array",
"description": "Required permissions to see this item",
"items": {
"type": "string"
}
},
"divider": {
"type": "boolean",
"description": "Show divider after this item",
"default": false
},
"className": {
"type": "string",
"description": "Additional CSS class names"
},
"ariaLabel": {
"type": "string",
"description": "Accessible label if different from label"
},
"ariaHaspopup": {
"type": "boolean",
"description": "Indicates item has a submenu"
},
"ariaExpanded": {
"type": "boolean",
"description": "Indicates if submenu is expanded"
},
"ariaCurrent": {
"type": "string",
"description": "Indicates current item",
"enum": ["page", "step", "location", "date", "time", "true", "false"]
},
"dataAttributes": {
"type": "object",
"description": "Custom data attributes",
"additionalProperties": {
"type": "string"
}
},
"onClick": {
"type": "string",
"description": "Click handler function name"
},
"onHover": {
"type": "string",
"description": "Hover handler function name"
},
"tooltip": {
"type": "string",
"description": "Tooltip text on hover"
},
"metadata": {
"type": "object",
"description": "Additional metadata",
"additionalProperties": true
}
}
}
},
"examples": [
{
"skipLink": true,
"keyboardNavigation": true,
"mobile": {
"breakpoint": 768,
"type": "hamburger"
},
"items": [
{
"id": "home",
"label": "Home",
"href": "/",
"icon": "home",
"active": true,
"ariaCurrent": "page"
},
{
"id": "products",
"label": "Products",
"href": "/products",
"icon": "shopping-bag",
"ariaHaspopup": true,
"children": [
{
"label": "Electronics",
"href": "/products/electronics"
},
{
"label": "Clothing",
"href": "/products/clothing"
}
]
},
{
"id": "about",
"label": "About",
"href": "/about",
"icon": "info"
},
{
"id": "contact",
"label": "Contact",
"href": "/contact",
"icon": "mail",
"badge": "New",
"badgeColor": "success"
}
]
}
]
}{
"templates": {
"react-router": {
"name": "React Router v6 Templates",
"description": "Common route patterns for React Router",
"patterns": [
{
"name": "Basic Routes",
"template": {
"path": "/",
"element": "Layout",
"children": [
{
"index": true,
"element": "Home"
},
{
"path": "about",
"element": "About"
},
{
"path": "contact",
"element": "Contact"
}
]
}
},
{
"name": "Nested Routes",
"template": {
"path": "products",
"element": "ProductsLayout",
"children": [
{
"index": true,
"element": "ProductList"
},
{
"path": ":productId",
"element": "ProductDetail",
"loader": "productLoader"
},
{
"path": ":productId/edit",
"element": "ProductEdit",
"action": "editProductAction"
}
]
}
},
{
"name": "Protected Routes",
"template": {
"path": "dashboard",
"element": "ProtectedRoute",
"children": [
{
"index": true,
"element": "Dashboard"
},
{
"path": "profile",
"element": "Profile"
},
{
"path": "settings",
"element": "Settings"
}
]
}
},
{
"name": "Error Routes",
"template": {
"path": "*",
"element": "NotFound"
}
}
]
},
"nextjs": {
"name": "Next.js App Router Templates",
"description": "Common patterns for Next.js App Router",
"patterns": [
{
"name": "Basic Structure",
"files": [
"app/layout.tsx",
"app/page.tsx",
"app/about/page.tsx",
"app/contact/page.tsx"
]
},
{
"name": "Dynamic Routes",
"files": [
"app/products/page.tsx",
"app/products/[productId]/page.tsx",
"app/products/[productId]/edit/page.tsx"
]
},
{
"name": "Route Groups",
"files": [
"app/(marketing)/layout.tsx",
"app/(marketing)/page.tsx",
"app/(marketing)/about/page.tsx",
"app/(dashboard)/layout.tsx",
"app/(dashboard)/dashboard/page.tsx"
]
},
{
"name": "Parallel Routes",
"files": [
"app/dashboard/@analytics/page.tsx",
"app/dashboard/@team/page.tsx",
"app/dashboard/layout.tsx"
]
},
{
"name": "Intercepting Routes",
"files": [
"app/@modal/(.)products/[id]/page.tsx",
"app/products/[id]/page.tsx"
]
}
]
},
"flask": {
"name": "Flask Route Templates",
"description": "Common Flask routing patterns",
"patterns": [
{
"name": "Basic Routes",
"routes": [
{
"path": "/",
"method": "GET",
"function": "index",
"template": "index.html"
},
{
"path": "/about",
"method": "GET",
"function": "about",
"template": "about.html"
},
{
"path": "/contact",
"methods": ["GET", "POST"],
"function": "contact",
"template": "contact.html"
}
]
},
{
"name": "RESTful Routes",
"blueprint": "api",
"prefix": "/api/v1",
"routes": [
{
"path": "/resources",
"method": "GET",
"function": "get_resources"
},
{
"path": "/resources",
"method": "POST",
"function": "create_resource"
},
{
"path": "/resources/<int:id>",
"method": "GET",
"function": "get_resource"
},
{
"path": "/resources/<int:id>",
"method": "PUT",
"function": "update_resource"
},
{
"path": "/resources/<int:id>",
"method": "DELETE",
"function": "delete_resource"
}
]
},
{
"name": "Admin Routes",
"blueprint": "admin",
"prefix": "/admin",
"auth_required": true,
"routes": [
{
"path": "/",
"method": "GET",
"function": "admin_dashboard",
"roles": ["admin"]
},
{
"path": "/users",
"method": "GET",
"function": "manage_users",
"roles": ["admin", "moderator"]
}
]
}
]
},
"django": {
"name": "Django URL Templates",
"description": "Common Django URL patterns",
"patterns": [
{
"name": "Basic URLs",
"urlpatterns": [
{
"pattern": "",
"view": "views.index",
"name": "index"
},
{
"pattern": "about/",
"view": "views.about",
"name": "about"
},
{
"pattern": "contact/",
"view": "views.ContactView.as_view()",
"name": "contact"
}
]
},
{
"name": "Model CRUD URLs",
"app_name": "products",
"urlpatterns": [
{
"pattern": "",
"view": "views.ProductListView.as_view()",
"name": "product_list"
},
{
"pattern": "<int:pk>/",
"view": "views.ProductDetailView.as_view()",
"name": "product_detail"
},
{
"pattern": "create/",
"view": "views.ProductCreateView.as_view()",
"name": "product_create"
},
{
"pattern": "<int:pk>/update/",
"view": "views.ProductUpdateView.as_view()",
"name": "product_update"
},
{
"pattern": "<int:pk>/delete/",
"view": "views.ProductDeleteView.as_view()",
"name": "product_delete"
}
]
},
{
"name": "Date-based Archives",
"urlpatterns": [
{
"pattern": "archive/",
"view": "views.ArchiveIndexView.as_view()",
"name": "archive_index"
},
{
"pattern": "archive/<int:year>/",
"view": "views.YearArchiveView.as_view()",
"name": "archive_year"
},
{
"pattern": "archive/<int:year>/<int:month>/",
"view": "views.MonthArchiveView.as_view()",
"name": "archive_month"
},
{
"pattern": "archive/<int:year>/<int:month>/<int:day>/",
"view": "views.DayArchiveView.as_view()",
"name": "archive_day"
}
]
}
]
},
"fastapi": {
"name": "FastAPI Route Templates",
"description": "Common FastAPI routing patterns",
"patterns": [
{
"name": "Basic Routes",
"routes": [
{
"path": "/",
"method": "get",
"function": "root",
"response_model": "dict",
"summary": "Root endpoint"
},
{
"path": "/health",
"method": "get",
"function": "health_check",
"tags": ["system"]
}
]
},
{
"name": "CRUD Operations",
"router": "products",
"prefix": "/products",
"tags": ["products"],
"routes": [
{
"path": "/",
"method": "get",
"function": "get_products",
"response_model": "List[Product]"
},
{
"path": "/",
"method": "post",
"function": "create_product",
"response_model": "Product",
"status_code": 201
},
{
"path": "/{product_id}",
"method": "get",
"function": "get_product",
"response_model": "Product"
},
{
"path": "/{product_id}",
"method": "put",
"function": "update_product",
"response_model": "Product"
},
{
"path": "/{product_id}",
"method": "delete",
"function": "delete_product",
"status_code": 204
}
]
},
{
"name": "WebSocket Routes",
"routes": [
{
"path": "/ws",
"type": "websocket",
"function": "websocket_endpoint"
},
{
"path": "/ws/chat/{room_id}",
"type": "websocket",
"function": "chat_endpoint"
}
]
},
{
"name": "File Operations",
"routes": [
{
"path": "/upload",
"method": "post",
"function": "upload_file",
"consumes": "multipart/form-data"
},
{
"path": "/download/{file_id}",
"method": "get",
"function": "download_file",
"response_class": "FileResponse"
}
]
}
]
}
},
"common_patterns": {
"authentication": {
"routes": [
"/login",
"/logout",
"/register",
"/forgot-password",
"/reset-password/{token}",
"/verify-email/{token}"
]
},
"user_management": {
"routes": [
"/profile",
"/profile/edit",
"/settings",
"/settings/security",
"/settings/notifications",
"/settings/privacy"
]
},
"e_commerce": {
"routes": [
"/products",
"/products/{category}",
"/products/{category}/{product}",
"/cart",
"/cart/add",
"/cart/remove/{item_id}",
"/checkout",
"/checkout/shipping",
"/checkout/payment",
"/checkout/confirm",
"/orders",
"/orders/{order_id}"
]
},
"blog": {
"routes": [
"/blog",
"/blog/{year}",
"/blog/{year}/{month}",
"/blog/{year}/{month}/{day}",
"/blog/{year}/{month}/{day}/{slug}",
"/blog/category/{category}",
"/blog/tag/{tag}",
"/blog/author/{author}",
"/blog/search"
]
},
"api_versioning": {
"patterns": [
"/api/v1/{resource}",
"/api/v2/{resource}",
"/v1/api/{resource}",
"/api/latest/{resource}"
]
}
}
}"""
Django URL Pattern Organization
Demonstrates best practices for organizing URL patterns in Django applications.
"""
from django.urls import path, include
from django.contrib import admin
from . import views
# Root URL configuration (myproject/urls.py)
urlpatterns = [
# Admin interface
path('admin/', admin.site.urls),
# API endpoints (namespaced)
path('api/v1/', include('api.urls', namespace='api')),
# App-specific URLs (include pattern)
path('blog/', include('blog.urls')),
path('shop/', include('shop.urls')),
path('accounts/', include('accounts.urls')),
# Root pages
path('', views.home, name='home'),
path('about/', views.about, name='about'),
path('contact/', views.contact, name='contact'),
]
# App-level URL configuration (blog/urls.py)
app_name = 'blog' # Namespace for reverse URL lookups
urlpatterns = [
# List view
path('', views.PostListView.as_view(), name='post_list'),
# Detail view with slug
path('<slug:slug>/', views.PostDetailView.as_view(), name='post_detail'),
# Create, update, delete
path('create/', views.PostCreateView.as_view(), name='post_create'),
path('<int:pk>/edit/', views.PostUpdateView.as_view(), name='post_edit'),
path('<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
# Category filtering
path('category/<slug:category_slug>/', views.PostByCategoryView.as_view(), name='posts_by_category'),
# Tag filtering
path('tag/<slug:tag_slug>/', views.PostByTagView.as_view(), name='posts_by_tag'),
# Comments (nested)
path('<int:post_id>/comments/', include('comments.urls')),
]
# API URL configuration (api/urls.py)
from rest_framework.routers import DefaultRouter
from .views import UserViewSet, PostViewSet, CommentViewSet
app_name = 'api'
# Router for ViewSets (automatic CRUD endpoints)
router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
router.register(r'posts', PostViewSet, basename='post')
router.register(r'comments', CommentViewSet, basename='comment')
urlpatterns = [
# Router-generated URLs
path('', include(router.urls)),
# Custom API endpoints
path('auth/login/', views.LoginAPIView.as_view(), name='login'),
path('auth/logout/', views.LogoutAPIView.as_view(), name='logout'),
path('auth/refresh/', views.RefreshTokenAPIView.as_view(), name='refresh'),
# Nested resources
path('posts/<int:post_id>/comments/', views.PostCommentsAPIView.as_view(), name='post_comments'),
# Search endpoints
path('search/', views.SearchAPIView.as_view(), name='search'),
path('search/posts/', views.PostSearchAPIView.as_view(), name='post_search'),
]
# E-commerce URL patterns (shop/urls.py)
app_name = 'shop'
urlpatterns = [
# Product listings
path('', views.ProductListView.as_view(), name='product_list'),
path('products/<slug:slug>/', views.ProductDetailView.as_view(), name='product_detail'),
path('category/<slug:category>/', views.CategoryView.as_view(), name='category'),
# Cart operations
path('cart/', views.CartView.as_view(), name='cart'),
path('cart/add/<int:product_id>/', views.add_to_cart, name='add_to_cart'),
path('cart/remove/<int:item_id>/', views.remove_from_cart, name='remove_from_cart'),
path('cart/update/<int:item_id>/', views.update_cart_item, name='update_cart_item'),
# Checkout flow
path('checkout/', views.CheckoutView.as_view(), name='checkout'),
path('checkout/shipping/', views.ShippingView.as_view(), name='checkout_shipping'),
path('checkout/payment/', views.PaymentView.as_view(), name='checkout_payment'),
path('checkout/confirm/', views.ConfirmOrderView.as_view(), name='checkout_confirm'),
# Order management
path('orders/', views.OrderListView.as_view(), name='order_list'),
path('orders/<int:pk>/', views.OrderDetailView.as_view(), name='order_detail'),
]
# URL pattern with multiple parameters
urlpatterns = [
# Date-based archive
path(
'archive/<int:year>/<int:month>/<int:day>/',
views.DayArchiveView.as_view(),
name='day_archive'
),
# Nested resources with multiple IDs
path(
'projects/<int:project_id>/tasks/<int:task_id>/comments/',
views.TaskCommentsView.as_view(),
name='task_comments'
),
]
# Advanced URL patterns with converters
from django.urls import register_converter
class FourDigitYearConverter:
regex = '[0-9]{4}'
def to_python(self, value):
return int(value)
def to_url(self, value):
return '%04d' % value
register_converter(FourDigitYearConverter, 'yyyy')
urlpatterns = [
# Custom converter usage
path('archive/<yyyy:year>/', views.YearArchiveView.as_view(), name='year_archive'),
# Built-in converters
path('user/<uuid:user_id>/', views.UserProfileView.as_view(), name='user_profile'),
path('post/<slug:slug>/', views.PostDetailView.as_view(), name='post'),
path('page/<path:page_path>/', views.PageView.as_view(), name='page'),
]
# Reverse URL lookups (usage in views/templates)
"""
# In views.py
from django.urls import reverse
from django.shortcuts import redirect
def create_post(request):
# ... create post logic ...
return redirect('blog:post_detail', slug=post.slug)
# Using reverse()
url = reverse('blog:post_detail', kwargs={'slug': 'my-post'})
# Result: /blog/my-post/
url = reverse('api:post-detail', kwargs={'pk': 123})
# Result: /api/v1/posts/123/
# In templates
{% url 'blog:post_detail' slug=post.slug %}
{% url 'shop:product_detail' slug='laptop-pro' %}
{% url 'api:user-list' %}
"""
# Best Practices Summary
"""
1. Use include() for app-specific URLs
2. Use app_name for namespacing
3. Use descriptive names for URL patterns
4. Group related URLs logically
5. Use slug fields for SEO-friendly URLs
6. Version API endpoints (api/v1/, api/v2/)
7. Use routers for ViewSets (DRF)
8. Keep URL patterns DRY (Don't Repeat Yourself)
9. Use path() over re_path() when possible
10. Document complex URL patterns
"""
"""
FastAPI Router Organization
Demonstrates best practices for organizing routes in FastAPI applications.
"""
from fastapi import FastAPI, APIRouter, Depends, HTTPException, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import List, Optional
# Main application instance (main.py)
app = FastAPI(
title="MyApp API",
version="1.0.0",
description="API for MyApp with organized routers"
)
# ============================================================================
# AUTHENTICATION ROUTER (auth.py)
# ============================================================================
auth_router = APIRouter(
prefix="/auth",
tags=["authentication"],
responses={401: {"description": "Unauthorized"}},
)
class LoginRequest(BaseModel):
email: str
password: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
@auth_router.post("/login", response_model=TokenResponse)
async def login(credentials: LoginRequest):
"""Authenticate user and return access token"""
# Authentication logic here
return TokenResponse(access_token="fake-token", token_type="bearer")
@auth_router.post("/logout")
async def logout():
"""Invalidate user session"""
return {"message": "Successfully logged out"}
@auth_router.post("/refresh", response_model=TokenResponse)
async def refresh_token():
"""Refresh access token"""
return TokenResponse(access_token="new-fake-token")
# ============================================================================
# USERS ROUTER (users.py)
# ============================================================================
users_router = APIRouter(
prefix="/users",
tags=["users"],
responses={404: {"description": "User not found"}},
)
class User(BaseModel):
id: int
email: str
name: str
class UserCreate(BaseModel):
email: str
name: str
password: str
@users_router.get("/", response_model=List[User])
async def list_users(skip: int = 0, limit: int = 10):
"""List all users with pagination"""
return [
User(id=1, email="user@example.com", name="John Doe"),
User(id=2, email="jane@example.com", name="Jane Smith"),
]
@users_router.get("/{user_id}", response_model=User)
async def get_user(user_id: int):
"""Get user by ID"""
if user_id == 1:
return User(id=1, email="user@example.com", name="John Doe")
raise HTTPException(status_code=404, detail="User not found")
@users_router.post("/", response_model=User, status_code=status.HTTP_201_CREATED)
async def create_user(user: UserCreate):
"""Create a new user"""
return User(id=1, email=user.email, name=user.name)
@users_router.put("/{user_id}", response_model=User)
async def update_user(user_id: int, user: UserCreate):
"""Update existing user"""
return User(id=user_id, email=user.email, name=user.name)
@users_router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(user_id: int):
"""Delete user"""
return None
# ============================================================================
# POSTS ROUTER (posts.py)
# ============================================================================
posts_router = APIRouter(
prefix="/posts",
tags=["posts"],
)
class Post(BaseModel):
id: int
title: str
content: str
author_id: int
published: bool = False
class PostCreate(BaseModel):
title: str
content: str
@posts_router.get("/", response_model=List[Post])
async def list_posts(
skip: int = 0,
limit: int = 10,
published: Optional[bool] = None,
):
"""List posts with optional filtering by published status"""
return [
Post(id=1, title="First Post", content="Content here", author_id=1, published=True),
]
@posts_router.get("/{post_id}", response_model=Post)
async def get_post(post_id: int):
"""Get post by ID"""
return Post(id=post_id, title="Post Title", content="Post content", author_id=1)
@posts_router.post("/", response_model=Post, status_code=status.HTTP_201_CREATED)
async def create_post(post: PostCreate):
"""Create a new post"""
return Post(id=1, title=post.title, content=post.content, author_id=1)
# ============================================================================
# NESTED RESOURCES (posts/{post_id}/comments)
# ============================================================================
comments_router = APIRouter(
prefix="/posts/{post_id}/comments",
tags=["comments"],
)
class Comment(BaseModel):
id: int
post_id: int
content: str
author_id: int
class CommentCreate(BaseModel):
content: str
@comments_router.get("/", response_model=List[Comment])
async def list_post_comments(post_id: int):
"""List all comments for a specific post"""
return [
Comment(id=1, post_id=post_id, content="Great post!", author_id=2),
]
@comments_router.post("/", response_model=Comment, status_code=status.HTTP_201_CREATED)
async def create_comment(post_id: int, comment: CommentCreate):
"""Create a comment on a post"""
return Comment(id=1, post_id=post_id, content=comment.content, author_id=1)
# ============================================================================
# API VERSIONING (api/v1/ and api/v2/)
# ============================================================================
# API v1 router
v1_router = APIRouter(prefix="/api/v1")
v1_router.include_router(auth_router)
v1_router.include_router(users_router)
v1_router.include_router(posts_router)
# API v2 router (with breaking changes)
v2_router = APIRouter(prefix="/api/v2")
v2_users_router = APIRouter(
prefix="/users",
tags=["users-v2"],
)
class UserV2(BaseModel):
id: int
email: str
full_name: str # Changed from 'name'
is_active: bool = True # New field
@v2_users_router.get("/", response_model=List[UserV2])
async def list_users_v2():
"""List users (API v2 with breaking changes)"""
return [
UserV2(id=1, email="user@example.com", full_name="John Doe", is_active=True),
]
v2_router.include_router(v2_users_router)
# ============================================================================
# REGISTER ALL ROUTERS
# ============================================================================
# Register versioned API routers
app.include_router(v1_router)
app.include_router(v2_router)
# Additional routers (no versioning)
app.include_router(comments_router)
# ============================================================================
# DEPENDENCY INJECTION PATTERN
# ============================================================================
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
"""Dependency to get current authenticated user"""
# Verify token and return user
if token == "fake-token":
return User(id=1, email="user@example.com", name="John Doe")
raise HTTPException(status_code=401, detail="Invalid token")
# Protected route using dependency
@app.get("/me", response_model=User)
async def get_current_user_info(current_user: User = Depends(get_current_user)):
"""Get current user information (protected route)"""
return current_user
# ============================================================================
# ADVANCED ROUTING PATTERNS
# ============================================================================
# Optional path parameters
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: Optional[str] = None):
"""Item detail with optional query parameter"""
if q:
return {"item_id": item_id, "query": q}
return {"item_id": item_id}
# Enum path parameters
from enum import Enum
class ModelName(str, Enum):
gpt4 = "gpt-4"
claude = "claude-3"
llama = "llama-3"
@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
"""Model selection with enum validation"""
if model_name == ModelName.gpt4:
return {"model": "GPT-4", "provider": "OpenAI"}
return {"model": model_name.value}
# File path parameters
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
"""Read file with path parameter (allows slashes)"""
return {"file_path": file_path}
# ============================================================================
# BEST PRACTICES SUMMARY
# ============================================================================
"""
1. Use APIRouter for modular organization
2. Group related endpoints with tags
3. Use prefix for common path segments
4. Version APIs (api/v1, api/v2) for breaking changes
5. Use dependencies for authentication/authorization
6. Use Pydantic models for request/response validation
7. Document endpoints with docstrings
8. Use HTTP status codes correctly (201 for created, 204 for no content)
9. Handle errors with HTTPException
10. Keep routers in separate files for large projects
Project Structure:
myapp/
├── main.py # FastAPI app instance
├── routers/
│ ├── __init__.py
│ ├── auth.py # Authentication routes
│ ├── users.py # User CRUD routes
│ ├── posts.py # Post CRUD routes
│ └── comments.py # Comment routes
├── models/
│ └── schemas.py # Pydantic models
├── dependencies/
│ └── auth.py # Authentication dependencies
└── database/
└── db.py # Database connection
"""
"""
Flask Blueprint Organization Example
This example demonstrates:
- Blueprint-based route organization
- Route guards and authentication
- Error handling
- RESTful API patterns
- Navigation hierarchy
"""
from flask import Flask, Blueprint, render_template, jsonify, request, redirect, url_for
from flask_login import login_required, current_user
from functools import wraps
from typing import Dict, List, Any
# Create Flask app
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
# ====================
# Main Navigation Routes
# ====================
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def home():
"""Home page with navigation structure."""
nav_items = get_navigation_structure()
return render_template('index.html', navigation=nav_items)
@main_bp.route('/about')
def about():
"""About page."""
breadcrumbs = [
{'label': 'Home', 'href': url_for('main.home')},
{'label': 'About', 'href': url_for('main.about'), 'current': True}
]
return render_template('about.html', breadcrumbs=breadcrumbs)
@main_bp.route('/contact', methods=['GET', 'POST'])
def contact():
"""Contact page with form handling."""
if request.method == 'POST':
# Process contact form
name = request.form.get('name')
email = request.form.get('email')
message = request.form.get('message')
# Send email or save to database
process_contact_form(name, email, message)
return redirect(url_for('main.contact_success'))
return render_template('contact.html')
# ====================
# Product Navigation
# ====================
products_bp = Blueprint('products', __name__, url_prefix='/products')
@products_bp.route('/')
def product_list():
"""Product listing with filtering."""
# Get filter parameters
category = request.args.get('category')
sort = request.args.get('sort', 'name')
page = request.args.get('page', 1, type=int)
# Fetch products
products = get_products(category=category, sort=sort, page=page)
# Build breadcrumbs
breadcrumbs = [
{'label': 'Home', 'href': url_for('main.home')},
{'label': 'Products', 'href': url_for('products.product_list'), 'current': True}
]
return render_template('products/list.html',
products=products,
breadcrumbs=breadcrumbs,
current_category=category)
@products_bp.route('/category/<category_slug>')
def product_category(category_slug: str):
"""Product category page."""
category = get_category_by_slug(category_slug)
if not category:
abort(404)
products = get_products_by_category(category_slug)
# Build breadcrumbs with category
breadcrumbs = [
{'label': 'Home', 'href': url_for('main.home')},
{'label': 'Products', 'href': url_for('products.product_list')},
{'label': category['name'], 'href': url_for('products.product_category', category_slug=category_slug), 'current': True}
]
return render_template('products/category.html',
category=category,
products=products,
breadcrumbs=breadcrumbs)
@products_bp.route('/<int:product_id>')
def product_detail(product_id: int):
"""Product detail page."""
product = get_product(product_id)
if not product:
abort(404)
# Build full breadcrumb trail
breadcrumbs = build_product_breadcrumbs(product)
# Get related navigation
related_products = get_related_products(product_id)
return render_template('products/detail.html',
product=product,
breadcrumbs=breadcrumbs,
related_products=related_products)
# ====================
# Admin Routes with Authentication
# ====================
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
def admin_required(f):
"""Decorator for admin-only routes."""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
return redirect(url_for('auth.login', next=request.url))
if not current_user.is_admin:
abort(403)
return f(*args, **kwargs)
return decorated_function
@admin_bp.route('/')
@admin_required
def admin_dashboard():
"""Admin dashboard with navigation stats."""
stats = {
'total_pages': count_pages(),
'total_products': count_products(),
'navigation_depth': calculate_navigation_depth(),
'broken_links': check_broken_links()
}
return render_template('admin/dashboard.html', stats=stats)
@admin_bp.route('/navigation')
@admin_required
def manage_navigation():
"""Manage site navigation structure."""
nav_structure = get_full_navigation_structure()
return render_template('admin/navigation.html', navigation=nav_structure)
@admin_bp.route('/navigation/edit/<int:item_id>', methods=['GET', 'POST'])
@admin_required
def edit_navigation_item(item_id: int):
"""Edit navigation item."""
nav_item = get_navigation_item(item_id)
if request.method == 'POST':
nav_item['label'] = request.form.get('label')
nav_item['href'] = request.form.get('href')
nav_item['parent_id'] = request.form.get('parent_id', type=int)
nav_item['order'] = request.form.get('order', type=int)
save_navigation_item(nav_item)
return redirect(url_for('admin.manage_navigation'))
return render_template('admin/edit_navigation.html', item=nav_item)
# ====================
# API Routes for Dynamic Navigation
# ====================
api_bp = Blueprint('api', __name__, url_prefix='/api/v1')
@api_bp.route('/navigation')
def api_navigation():
"""Get navigation structure as JSON."""
nav_structure = get_navigation_structure()
return jsonify(nav_structure)
@api_bp.route('/navigation/breadcrumbs')
def api_breadcrumbs():
"""Calculate breadcrumbs for current path."""
path = request.args.get('path', '/')
breadcrumbs = calculate_breadcrumbs(path)
return jsonify(breadcrumbs)
@api_bp.route('/navigation/search')
def api_navigation_search():
"""Search navigation items."""
query = request.args.get('q', '')
results = search_navigation(query)
return jsonify(results)
@api_bp.route('/navigation/sitemap')
def api_sitemap():
"""Generate sitemap for SEO."""
sitemap = generate_sitemap()
return jsonify(sitemap)
# ====================
# Helper Functions
# ====================
def get_navigation_structure() -> List[Dict[str, Any]]:
"""Get main navigation structure."""
return [
{
'id': 'home',
'label': 'Home',
'href': url_for('main.home'),
'icon': 'home'
},
{
'id': 'products',
'label': 'Products',
'href': url_for('products.product_list'),
'icon': 'shopping-cart',
'children': [
{
'label': 'Electronics',
'href': url_for('products.product_category', category_slug='electronics')
},
{
'label': 'Clothing',
'href': url_for('products.product_category', category_slug='clothing')
},
{
'label': 'Books',
'href': url_for('products.product_category', category_slug='books')
}
]
},
{
'id': 'about',
'label': 'About',
'href': url_for('main.about'),
'icon': 'info'
},
{
'id': 'contact',
'label': 'Contact',
'href': url_for('main.contact'),
'icon': 'mail'
}
]
def calculate_breadcrumbs(path: str) -> List[Dict[str, Any]]:
"""Calculate breadcrumb trail for given path."""
breadcrumbs = [
{'label': 'Home', 'href': '/'}
]
# Parse path and build breadcrumbs
segments = [s for s in path.split('/') if s]
current_path = ''
for i, segment in enumerate(segments):
current_path += '/' + segment
is_last = i == len(segments) - 1
# Look up proper label for segment
label = get_label_for_segment(segment, current_path)
breadcrumbs.append({
'label': label,
'href': current_path,
'current': is_last
})
return breadcrumbs
def build_product_breadcrumbs(product: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Build breadcrumb trail for product."""
breadcrumbs = [
{'label': 'Home', 'href': url_for('main.home')},
{'label': 'Products', 'href': url_for('products.product_list')}
]
if product.get('category'):
breadcrumbs.append({
'label': product['category']['name'],
'href': url_for('products.product_category',
category_slug=product['category']['slug'])
})
breadcrumbs.append({
'label': product['name'],
'href': url_for('products.product_detail', product_id=product['id']),
'current': True
})
return breadcrumbs
# ====================
# Register Blueprints
# ====================
app.register_blueprint(main_bp)
app.register_blueprint(products_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(api_bp)
# ====================
# Error Handlers
# ====================
@app.errorhandler(404)
def not_found(error):
"""Handle 404 errors with navigation context."""
if request.path.startswith('/api'):
return jsonify({'error': 'Not found'}), 404
return render_template('errors/404.html',
navigation=get_navigation_structure()), 404
@app.errorhandler(403)
def forbidden(error):
"""Handle 403 errors."""
if request.path.startswith('/api'):
return jsonify({'error': 'Forbidden'}), 403
return render_template('errors/403.html',
navigation=get_navigation_structure()), 403
if __name__ == '__main__':
app.run(debug=True)/**
* Responsive Horizontal Navigation Menu
*
* Features:
* - Responsive design with mobile hamburger menu
* - Keyboard navigation support
* - ARIA compliance
* - Active state management
* - Dropdown submenus
*/
import React, { useState, useRef, useEffect } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import './horizontal-menu.css';
interface NavItem {
id: string;
label: string;
href: string;
children?: NavItem[];
external?: boolean;
icon?: React.ReactNode;
}
interface HorizontalMenuProps {
items: NavItem[];
logo?: React.ReactNode;
className?: string;
}
export const HorizontalMenu: React.FC<HorizontalMenuProps> = ({
items,
logo,
className = ''
}) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
const [focusedIndex, setFocusedIndex] = useState(-1);
const menuRef = useRef<HTMLElement>(null);
const location = useLocation();
// Close mobile menu on route change
useEffect(() => {
setMobileMenuOpen(false);
setActiveDropdown(null);
}, [location]);
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setActiveDropdown(null);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// Keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
setFocusedIndex((prev) => (prev + 1) % items.length);
break;
case 'ArrowLeft':
e.preventDefault();
setFocusedIndex((prev) => (prev - 1 + items.length) % items.length);
break;
case 'ArrowDown':
if (items[index].children) {
e.preventDefault();
setActiveDropdown(items[index].id);
}
break;
case 'Escape':
e.preventDefault();
setActiveDropdown(null);
break;
case 'Enter':
case ' ':
if (items[index].children) {
e.preventDefault();
setActiveDropdown(
activeDropdown === items[index].id ? null : items[index].id
);
}
break;
case 'Home':
e.preventDefault();
setFocusedIndex(0);
break;
case 'End':
e.preventDefault();
setFocusedIndex(items.length - 1);
break;
}
};
const toggleDropdown = (itemId: string) => {
setActiveDropdown(activeDropdown === itemId ? null : itemId);
};
const renderNavItem = (item: NavItem, index: number) => {
const hasChildren = item.children && item.children.length > 0;
const isActive = activeDropdown === item.id;
if (hasChildren) {
return (
<li key={item.id} className="nav-item has-dropdown">
<button
className="nav-link dropdown-trigger"
aria-expanded={isActive}
aria-haspopup="true"
aria-controls={`dropdown-${item.id}`}
onClick={() => toggleDropdown(item.id)}
onKeyDown={(e) => handleKeyDown(e, index)}
tabIndex={focusedIndex === index ? 0 : -1}
>
{item.icon && <span className="nav-icon">{item.icon}</span>}
<span>{item.label}</span>
<svg
className={`dropdown-arrow ${isActive ? 'open' : ''}`}
width="12"
height="8"
viewBox="0 0 12 8"
fill="currentColor"
aria-hidden="true"
>
<path d="M1 1l5 5 5-5" stroke="currentColor" strokeWidth="2" fill="none" />
</svg>
</button>
{isActive && (
<ul
id={`dropdown-${item.id}`}
className="dropdown-menu"
role="menu"
aria-label={`${item.label} submenu`}
>
{item.children.map((child) => (
<li key={child.id} role="none">
{child.external ? (
<a
href={child.href}
className="dropdown-link"
role="menuitem"
target="_blank"
rel="noopener noreferrer"
>
{child.label}
<span className="sr-only">(opens in new tab)</span>
</a>
) : (
<NavLink
to={child.href}
className={({ isActive }) =>
`dropdown-link ${isActive ? 'active' : ''}`
}
role="menuitem"
>
{child.label}
</NavLink>
)}
</li>
))}
</ul>
)}
</li>
);
}
return (
<li key={item.id} className="nav-item">
{item.external ? (
<a
href={item.href}
className="nav-link"
target="_blank"
rel="noopener noreferrer"
tabIndex={focusedIndex === index ? 0 : -1}
onKeyDown={(e) => handleKeyDown(e, index)}
>
{item.icon && <span className="nav-icon">{item.icon}</span>}
<span>{item.label}</span>
</a>
) : (
<NavLink
to={item.href}
className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`}
tabIndex={focusedIndex === index ? 0 : -1}
onKeyDown={(e) => handleKeyDown(e, index)}
aria-current={({ isActive }) => (isActive ? 'page' : undefined)}
>
{item.icon && <span className="nav-icon">{item.icon}</span>}
<span>{item.label}</span>
</NavLink>
)}
</li>
);
};
return (
<nav
ref={menuRef}
className={`horizontal-menu ${className}`}
aria-label="Main navigation"
>
<div className="nav-container">
{logo && (
<div className="nav-logo">
<NavLink to="/" aria-label="Home">
{logo}
</NavLink>
</div>
)}
{/* Desktop Navigation */}
<ul className="nav-list desktop-nav" role="menubar">
{items.map((item, index) => renderNavItem(item, index))}
</ul>
{/* Mobile Menu Toggle */}
<button
className={`mobile-menu-toggle ${mobileMenuOpen ? 'open' : ''}`}
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileMenuOpen}
aria-controls="mobile-nav"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
<span className="hamburger">
<span></span>
<span></span>
<span></span>
</span>
</button>
{/* Mobile Navigation */}
<div
id="mobile-nav"
className={`mobile-nav ${mobileMenuOpen ? 'open' : ''}`}
aria-hidden={!mobileMenuOpen}
>
<ul className="nav-list" role="menubar">
{items.map((item, index) => renderNavItem(item, index))}
</ul>
</div>
</div>
</nav>
);
};
// Example usage
export const HorizontalMenuExample: React.FC = () => {
const navigationItems: NavItem[] = [
{
id: 'home',
label: 'Home',
href: '/',
icon: '🏠'
},
{
id: 'products',
label: 'Products',
href: '/products',
icon: '📦',
children: [
{
id: 'electronics',
label: 'Electronics',
href: '/products/electronics'
},
{
id: 'clothing',
label: 'Clothing',
href: '/products/clothing'
},
{
id: 'books',
label: 'Books',
href: '/products/books'
}
]
},
{
id: 'services',
label: 'Services',
href: '/services',
icon: '⚡',
children: [
{
id: 'consulting',
label: 'Consulting',
href: '/services/consulting'
},
{
id: 'support',
label: 'Support',
href: '/services/support'
}
]
},
{
id: 'about',
label: 'About',
href: '/about',
icon: 'ℹ️'
},
{
id: 'contact',
label: 'Contact',
href: '/contact',
icon: '📧'
}
];
return (
<HorizontalMenu
items={navigationItems}
logo={<img src="/logo.svg" alt="Company Logo" />}
/>
);
};import React, { useState } from 'react';
import { Menu, X, Home, User, Settings, LogOut } from 'lucide-react';
/**
* Mobile Navigation Example
*
* Features:
* - Hamburger menu toggle
* - Slide-in drawer navigation
* - Backdrop overlay
* - Active link highlighting
* - Accessible (keyboard navigation, ARIA labels)
*/
interface NavItem {
label: string;
href: string;
icon: React.ReactNode;
}
const navItems: NavItem[] = [
{ label: 'Home', href: '/', icon: <Home size={20} /> },
{ label: 'Profile', href: '/profile', icon: <User size={20} /> },
{ label: 'Settings', href: '/settings', icon: <Settings size={20} /> },
];
export function MobileNavigation() {
const [isOpen, setIsOpen] = useState(false);
const [activePath, setActivePath] = useState('/');
const toggleMenu = () => setIsOpen(!isOpen);
const handleNavClick = (href: string) => {
setActivePath(href);
setIsOpen(false);
};
return (
<>
{/* Mobile Header */}
<header
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
height: '60px',
backgroundColor: '#fff',
borderBottom: '1px solid #e0e0e0',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 16px',
zIndex: 1000,
}}
>
<h1 style={{ margin: 0, fontSize: '20px', fontWeight: 600 }}>MyApp</h1>
<button
onClick={toggleMenu}
aria-label="Toggle menu"
aria-expanded={isOpen}
aria-controls="mobile-menu"
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{isOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</header>
{/* Backdrop Overlay */}
{isOpen && (
<div
onClick={() => setIsOpen(false)}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 1100,
animation: 'fadeIn 0.3s ease',
}}
/>
)}
{/* Slide-in Drawer */}
<nav
id="mobile-menu"
role="navigation"
aria-label="Main navigation"
style={{
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
width: '280px',
maxWidth: '80vw',
backgroundColor: '#fff',
boxShadow: '-2px 0 8px rgba(0, 0, 0, 0.1)',
transform: isOpen ? 'translateX(0)' : 'translateX(100%)',
transition: 'transform 0.3s ease',
zIndex: 1200,
display: 'flex',
flexDirection: 'column',
paddingTop: '80px',
}}
>
{/* Navigation Links */}
<ul
style={{
listStyle: 'none',
margin: 0,
padding: '0 16px',
flex: 1,
}}
>
{navItems.map((item) => {
const isActive = activePath === item.href;
return (
<li key={item.href} style={{ marginBottom: '8px' }}>
<a
href={item.href}
onClick={(e) => {
e.preventDefault();
handleNavClick(item.href);
}}
aria-current={isActive ? 'page' : undefined}
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '12px 16px',
borderRadius: '8px',
textDecoration: 'none',
color: isActive ? '#2563eb' : '#333',
backgroundColor: isActive ? '#eff6ff' : 'transparent',
fontWeight: isActive ? 600 : 400,
transition: 'all 0.2s ease',
}}
>
{item.icon}
{item.label}
</a>
</li>
);
})}
</ul>
{/* Footer Action */}
<div
style={{
borderTop: '1px solid #e0e0e0',
padding: '16px',
}}
>
<button
onClick={() => console.log('Logout')}
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
width: '100%',
padding: '12px 16px',
backgroundColor: 'transparent',
border: 'none',
borderRadius: '8px',
color: '#ef4444',
fontWeight: 600,
cursor: 'pointer',
textAlign: 'left',
}}
>
<LogOut size={20} />
Logout
</button>
</div>
</nav>
{/* Inline styles for animations */}
<style>{`
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`}</style>
</>
);
}
export default MobileNavigation;
/**
* Tab Navigation with URL Synchronization
*
* Features:
* - URL parameter synchronization
* - Keyboard navigation (arrow keys, home, end)
* - ARIA compliant tab pattern
* - Lazy loading of tab content
* - Animated transitions
* - Responsive design
*/
import React, { useState, useRef, useEffect, Suspense, lazy } from 'react';
import { useSearchParams } from 'react-router-dom';
import './tab-navigation.css';
interface Tab {
id: string;
label: string;
icon?: React.ReactNode;
content: React.ReactNode | (() => React.ReactNode);
disabled?: boolean;
badge?: string | number;
lazy?: boolean;
}
interface TabNavigationProps {
tabs: Tab[];
defaultTab?: string;
urlParam?: string;
orientation?: 'horizontal' | 'vertical';
variant?: 'default' | 'pills' | 'underline';
onTabChange?: (tabId: string) => void;
}
export const TabNavigation: React.FC<TabNavigationProps> = ({
tabs,
defaultTab,
urlParam = 'tab',
orientation = 'horizontal',
variant = 'default',
onTabChange
}) => {
const [searchParams, setSearchParams] = useSearchParams();
const activeTabId = searchParams.get(urlParam) || defaultTab || tabs[0]?.id;
const [focusedIndex, setFocusedIndex] = useState(-1);
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
// Find active tab index
const activeTabIndex = tabs.findIndex(tab => tab.id === activeTabId);
// Update URL when tab changes
const handleTabClick = (tabId: string) => {
const tab = tabs.find(t => t.id === tabId);
if (tab?.disabled) return;
setSearchParams(prev => {
prev.set(urlParam, tabId);
return prev;
});
onTabChange?.(tabId);
};
// Keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
let newIndex = index;
const enabledTabs = tabs
.map((tab, idx) => ({ tab, idx }))
.filter(({ tab }) => !tab.disabled);
const currentEnabledIndex = enabledTabs.findIndex(({ idx }) => idx === index);
switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
if (orientation === 'horizontal' && e.key === 'ArrowDown') return;
if (orientation === 'vertical' && e.key === 'ArrowRight') return;
const nextEnabled = enabledTabs[currentEnabledIndex + 1];
if (nextEnabled) {
newIndex = nextEnabled.idx;
} else if (enabledTabs.length > 0) {
newIndex = enabledTabs[0].idx; // Wrap to first
}
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
if (orientation === 'horizontal' && e.key === 'ArrowUp') return;
if (orientation === 'vertical' && e.key === 'ArrowLeft') return;
const prevEnabled = enabledTabs[currentEnabledIndex - 1];
if (prevEnabled) {
newIndex = prevEnabled.idx;
} else if (enabledTabs.length > 0) {
newIndex = enabledTabs[enabledTabs.length - 1].idx; // Wrap to last
}
break;
case 'Home':
e.preventDefault();
if (enabledTabs.length > 0) {
newIndex = enabledTabs[0].idx;
}
break;
case 'End':
e.preventDefault();
if (enabledTabs.length > 0) {
newIndex = enabledTabs[enabledTabs.length - 1].idx;
}
break;
case 'Enter':
case ' ':
e.preventDefault();
handleTabClick(tabs[index].id);
return;
default:
return;
}
setFocusedIndex(newIndex);
tabRefs.current[newIndex]?.focus();
};
// Focus management
useEffect(() => {
if (focusedIndex >= 0 && tabRefs.current[focusedIndex]) {
tabRefs.current[focusedIndex]?.focus();
}
}, [focusedIndex]);
// Render tab content
const renderTabContent = (tab: Tab) => {
if (typeof tab.content === 'function') {
return tab.content();
}
return tab.content;
};
return (
<div
className={`tab-navigation ${orientation} ${variant}`}
data-orientation={orientation}
>
<div
role="tablist"
aria-label="Tab navigation"
aria-orientation={orientation}
className="tab-list"
>
{tabs.map((tab, index) => {
const isActive = tab.id === activeTabId;
const isDisabled = tab.disabled;
return (
<button
key={tab.id}
ref={(el) => (tabRefs.current[index] = el)}
role="tab"
id={`tab-${tab.id}`}
aria-selected={isActive}
aria-disabled={isDisabled}
aria-controls={`panel-${tab.id}`}
tabIndex={isActive ? 0 : -1}
className={`tab-button ${isActive ? 'active' : ''} ${
isDisabled ? 'disabled' : ''
}`}
onClick={() => handleTabClick(tab.id)}
onKeyDown={(e) => handleKeyDown(e, index)}
disabled={isDisabled}
>
{tab.icon && <span className="tab-icon">{tab.icon}</span>}
<span className="tab-label">{tab.label}</span>
{tab.badge !== undefined && (
<span className="tab-badge" aria-label={`${tab.badge} items`}>
{tab.badge}
</span>
)}
</button>
);
})}
</div>
<div className="tab-panels">
{tabs.map((tab) => {
const isActive = tab.id === activeTabId;
return (
<div
key={tab.id}
role="tabpanel"
id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={!isActive}
tabIndex={0}
className={`tab-panel ${isActive ? 'active' : ''}`}
>
{tab.lazy ? (
<Suspense fallback={<TabPanelLoader />}>
{isActive && renderTabContent(tab)}
</Suspense>
) : (
renderTabContent(tab)
)}
</div>
);
})}
</div>
</div>
);
};
// Loading component for lazy tabs
const TabPanelLoader: React.FC = () => (
<div className="tab-panel-loader">
<div className="loader-spinner" />
<p>Loading content...</p>
</div>
);
// Example lazy-loaded components
const OverviewPanel = lazy(() => import('./panels/OverviewPanel'));
const SettingsPanel = lazy(() => import('./panels/SettingsPanel'));
const AnalyticsPanel = lazy(() => import('./panels/AnalyticsPanel'));
// Example usage
export const TabNavigationExample: React.FC = () => {
const tabs: Tab[] = [
{
id: 'overview',
label: 'Overview',
icon: '📊',
content: <OverviewPanel />,
lazy: true
},
{
id: 'activity',
label: 'Activity',
icon: '📈',
badge: 12,
content: (
<div>
<h2>Recent Activity</h2>
<ul>
<li>User logged in - 2 minutes ago</li>
<li>File uploaded - 15 minutes ago</li>
<li>Settings updated - 1 hour ago</li>
</ul>
</div>
)
},
{
id: 'settings',
label: 'Settings',
icon: '⚙️',
content: <SettingsPanel />,
lazy: true
},
{
id: 'analytics',
label: 'Analytics',
icon: '📉',
content: <AnalyticsPanel />,
lazy: true
},
{
id: 'disabled',
label: 'Coming Soon',
icon: '🔒',
content: <div>This feature is coming soon!</div>,
disabled: true
}
];
return (
<>
<h1>Horizontal Tabs Example</h1>
<TabNavigation
tabs={tabs}
defaultTab="overview"
variant="underline"
onTabChange={(tabId) => console.log('Tab changed to:', tabId)}
/>
<h1 style={{ marginTop: '3rem' }}>Vertical Pills Example</h1>
<div style={{ display: 'flex', gap: '2rem' }}>
<TabNavigation
tabs={tabs}
defaultTab="activity"
orientation="vertical"
variant="pills"
urlParam="vertical-tab"
/>
</div>
</>
);
};
// Scrollable tabs for many items
export const ScrollableTabs: React.FC<{ tabs: Tab[] }> = ({ tabs }) => {
const [showLeftScroll, setShowLeftScroll] = useState(false);
const [showRightScroll, setShowRightScroll] = useState(false);
const tabListRef = useRef<HTMLDivElement>(null);
const checkScroll = () => {
if (!tabListRef.current) return;
const { scrollLeft, scrollWidth, clientWidth } = tabListRef.current;
setShowLeftScroll(scrollLeft > 0);
setShowRightScroll(scrollLeft < scrollWidth - clientWidth);
};
useEffect(() => {
checkScroll();
window.addEventListener('resize', checkScroll);
return () => window.removeEventListener('resize', checkScroll);
}, [tabs]);
const scroll = (direction: 'left' | 'right') => {
if (!tabListRef.current) return;
const scrollAmount = 200;
tabListRef.current.scrollBy({
left: direction === 'left' ? -scrollAmount : scrollAmount,
behavior: 'smooth'
});
setTimeout(checkScroll, 300);
};
return (
<div className="scrollable-tabs-container">
{showLeftScroll && (
<button
className="scroll-button left"
onClick={() => scroll('left')}
aria-label="Scroll tabs left"
>
←
</button>
)}
<div
ref={tabListRef}
className="scrollable-tab-list"
onScroll={checkScroll}
>
<TabNavigation tabs={tabs} />
</div>
{showRightScroll && (
<button
className="scroll-button right"
onClick={() => scroll('right')}
aria-label="Scroll tabs right"
>
→
</button>
)}
</div>
);
};skill: "implementing-navigation"
version: "1.0"
domain: "frontend"
# Base outputs required for all navigation implementations
base_outputs:
- path: "src/components/"
must_contain: []
reason: "Component directory for navigation implementations"
- path: "src/config/"
must_contain: []
reason: "Navigation configuration and route definitions"
# Conditional outputs based on configuration
conditional_outputs:
maturity:
starter:
# Frontend navigation components (starter)
- path: "src/components/HorizontalMenu.tsx"
must_contain: ["NavLink", "useState", "aria-label"]
reason: "Basic horizontal navigation with accessibility"
- path: "src/components/MobileMenu.tsx"
must_contain: ["hamburger", "drawer", "aria-expanded"]
reason: "Mobile hamburger menu with drawer"
- path: "src/config/navigation.ts"
must_contain: ["NavItem", "label", "href"]
reason: "Navigation structure configuration"
- path: "src/App.tsx"
must_contain: ["BrowserRouter", "Routes", "Route"]
reason: "Basic client-side routing setup (React Router)"
# Backend routing (starter)
- path: "app/routes.py"
must_contain: ["@app.route", "def"]
reason: "Basic Flask route definitions"
intermediate:
# Frontend navigation (intermediate)
- path: "src/components/HorizontalMenu.tsx"
must_contain: ["NavLink", "dropdown", "useRef", "keyboard navigation"]
reason: "Horizontal menu with dropdowns and keyboard nav"
- path: "src/components/SideNavigation.tsx"
must_contain: ["collapsible", "nested", "aria-expanded"]
reason: "Side navigation with collapsible sections"
- path: "src/components/TabNavigation.tsx"
must_contain: ["useSearchParams", "aria-selected", "role=\"tablist\""]
reason: "Tabs with URL synchronization and ARIA compliance"
- path: "src/components/Breadcrumbs.tsx"
must_contain: ["aria-label=\"breadcrumb\"", "aria-current"]
reason: "Breadcrumb navigation with ARIA landmarks"
- path: "src/components/MobileNavigation.tsx"
must_contain: ["hamburger", "bottom navigation", "useMediaQuery"]
reason: "Responsive mobile navigation patterns"
- path: "src/config/routes.ts"
must_contain: ["path", "component", "children", "lazy"]
reason: "Route configuration with lazy loading"
- path: "src/hooks/useNavigation.ts"
must_contain: ["useNavigate", "useLocation", "useParams"]
reason: "Navigation utility hook"
# Backend routing (intermediate)
- path: "app/routes/__init__.py"
must_contain: ["Blueprint", "register_blueprint"]
reason: "Flask blueprint organization"
- path: "app/urls.py"
must_contain: ["path", "include", "app_name"]
reason: "Django URL configuration with namespaces"
- path: "app/routers/__init__.py"
must_contain: ["APIRouter", "include_router"]
reason: "FastAPI router organization"
advanced:
# Frontend navigation (advanced)
- path: "src/components/navigation/HorizontalMenu.tsx"
must_contain: ["NavLink", "dropdown", "mega menu", "useRef", "FocusTrap"]
reason: "Advanced horizontal menu with mega menu and focus management"
- path: "src/components/navigation/SideNavigation.tsx"
must_contain: ["collapsible", "nested", "virtualized", "search"]
reason: "Advanced side nav with search and virtualization"
- path: "src/components/navigation/TabNavigation.tsx"
must_contain: ["useSearchParams", "lazy loading", "prefetch", "animations"]
reason: "Advanced tabs with lazy loading and prefetching"
- path: "src/components/navigation/Breadcrumbs.tsx"
must_contain: ["dynamic", "schema.org", "aria-current"]
reason: "Dynamic breadcrumbs with structured data"
- path: "src/components/navigation/CommandPalette.tsx"
must_contain: ["search", "keyboard shortcuts", "cmdk"]
reason: "Command palette for power users"
- path: "src/components/navigation/Pagination.tsx"
must_contain: ["aria-label=\"pagination\"", "useSearchParams", "ellipsis"]
reason: "Accessible pagination with URL sync"
- path: "src/components/navigation/TableOfContents.tsx"
must_contain: ["IntersectionObserver", "active section", "smooth scroll"]
reason: "Automatic table of contents with scroll spy"
- path: "src/components/navigation/Stepper.tsx"
must_contain: ["multi-step", "progress", "validation"]
reason: "Multi-step wizard navigation"
- path: "src/config/navigation-tree.json"
must_contain: ["id", "label", "href", "children", "permissions"]
reason: "Hierarchical navigation structure with permissions"
- path: "src/config/routes.ts"
must_contain: ["lazy", "prefetch", "guards", "metadata"]
reason: "Advanced route configuration with guards and metadata"
- path: "src/hooks/useNavigation.ts"
must_contain: ["useNavigate", "useLocation", "useParams", "useMatches"]
reason: "Comprehensive navigation utilities"
- path: "src/hooks/useBreadcrumbs.ts"
must_contain: ["useMatches", "breadcrumb trail", "dynamic"]
reason: "Dynamic breadcrumb generation hook"
- path: "src/hooks/useKeyboardShortcuts.ts"
must_contain: ["useEffect", "addEventListener", "key combinations"]
reason: "Keyboard shortcut management for navigation"
- path: "src/utils/navigation-tree-validator.ts"
must_contain: ["validate", "schema", "errors"]
reason: "Navigation structure validation"
# Backend routing (advanced)
- path: "app/routes/__init__.py"
must_contain: ["Blueprint", "before_request", "error_handler"]
reason: "Flask blueprints with middleware and error handling"
- path: "app/routes/api/v1/__init__.py"
must_contain: ["Blueprint", "versioning"]
reason: "API versioning structure"
- path: "app/urls.py"
must_contain: ["path", "include", "namespace", "middleware"]
reason: "Django URLs with middleware and namespaces"
- path: "app/routers/api/v1/__init__.py"
must_contain: ["APIRouter", "dependencies", "tags"]
reason: "FastAPI routers with dependencies and OpenAPI tags"
- path: "app/middleware/auth.py"
must_contain: ["authentication", "authorization"]
reason: "Route-level authentication middleware"
frontend_framework:
react:
- path: "src/components/HorizontalMenu.tsx"
must_contain: ["NavLink", "useState", "useRef"]
reason: "React horizontal navigation component"
- path: "src/components/TabNavigation.tsx"
must_contain: ["useSearchParams", "useState"]
reason: "React tab navigation with hooks"
- path: "src/config/routes.tsx"
must_contain: ["RouteObject", "BrowserRouter", "Routes"]
reason: "React Router configuration"
- path: "src/App.tsx"
must_contain: ["BrowserRouter", "Routes", "Route"]
reason: "React Router setup"
- path: "package.json"
must_contain: ["react-router-dom"]
reason: "React Router dependency"
vue:
- path: "src/components/HorizontalMenu.vue"
must_contain: ["<template>", "<script setup>", "RouterLink"]
reason: "Vue horizontal navigation component"
- path: "src/components/TabNavigation.vue"
must_contain: ["<template>", "useRoute", "useRouter"]
reason: "Vue tab navigation with Composition API"
- path: "src/router/index.ts"
must_contain: ["createRouter", "createWebHistory", "routes"]
reason: "Vue Router configuration"
- path: "package.json"
must_contain: ["vue-router"]
reason: "Vue Router dependency"
nextjs:
- path: "app/layout.tsx"
must_contain: ["Link", "usePathname"]
reason: "Next.js App Router layout with navigation"
- path: "components/Navigation.tsx"
must_contain: ["Link", "usePathname", "next/link"]
reason: "Next.js navigation component"
- path: "app/(routes)/page.tsx"
must_contain: []
reason: "Next.js file-based routing structure"
svelte:
- path: "src/components/HorizontalMenu.svelte"
must_contain: ["<script>", "$app/stores", "page"]
reason: "Svelte navigation component"
- path: "src/routes/+layout.svelte"
must_contain: ["<slot />"]
reason: "SvelteKit layout with navigation"
backend_framework:
flask:
- path: "app/routes/__init__.py"
must_contain: ["Blueprint", "register_blueprint"]
reason: "Flask blueprint registration"
- path: "app/routes/main.py"
must_contain: ["@bp.route", "Blueprint"]
reason: "Flask route definitions with blueprints"
- path: "app/__init__.py"
must_contain: ["Flask", "register_blueprint"]
reason: "Flask app initialization with blueprints"
- path: "config.py"
must_contain: ["class Config"]
reason: "Flask configuration"
django:
- path: "app/urls.py"
must_contain: ["path", "include", "urlpatterns"]
reason: "Django URL configuration"
- path: "app/views.py"
must_contain: ["def", "HttpResponse"]
reason: "Django view functions"
- path: "project/urls.py"
must_contain: ["admin.site.urls", "include"]
reason: "Django project-level URL configuration"
- path: "project/settings.py"
must_contain: ["INSTALLED_APPS", "MIDDLEWARE"]
reason: "Django settings"
fastapi:
- path: "app/routers/__init__.py"
must_contain: ["APIRouter", "include_router"]
reason: "FastAPI router registration"
- path: "app/routers/items.py"
must_contain: ["APIRouter", "@router.get", "@router.post"]
reason: "FastAPI route definitions"
- path: "app/main.py"
must_contain: ["FastAPI", "include_router"]
reason: "FastAPI app initialization"
- path: "app/dependencies.py"
must_contain: ["Depends"]
reason: "FastAPI dependency injection"
styling:
tailwind:
- path: "src/components/HorizontalMenu.tsx"
must_contain: ["className", "hover:", "focus:"]
reason: "Tailwind-styled navigation component"
- path: "tailwind.config.js"
must_contain: ["theme", "extend"]
reason: "Tailwind configuration"
css_modules:
- path: "src/components/HorizontalMenu.module.css"
must_contain: [".menu", ".menuItem", ".active"]
reason: "CSS Modules for navigation styling"
- path: "src/components/HorizontalMenu.tsx"
must_contain: ["styles.", "import styles"]
reason: "Component using CSS Modules"
styled_components:
- path: "src/components/HorizontalMenu.tsx"
must_contain: ["styled.", "theme."]
reason: "Styled-components navigation"
scss:
- path: "src/components/HorizontalMenu.scss"
must_contain: [".menu", "&:hover", "&.active"]
reason: "SCSS navigation styles"
state_management:
context:
- path: "src/context/NavigationContext.tsx"
must_contain: ["createContext", "Provider", "useContext"]
reason: "Navigation state via React Context"
zustand:
- path: "src/stores/navigationStore.ts"
must_contain: ["create", "zustand"]
reason: "Navigation state via Zustand"
redux:
- path: "src/store/navigationSlice.ts"
must_contain: ["createSlice", "PayloadAction"]
reason: "Navigation state via Redux Toolkit"
pinia:
- path: "src/stores/navigation.ts"
must_contain: ["defineStore", "pinia"]
reason: "Navigation state via Pinia (Vue)"
# Scaffolding files that should be created as starting points
scaffolding:
- path: "src/config/navigation.ts"
reason: "Initialize navigation structure configuration"
- path: "src/components/Navigation.tsx"
reason: "Base navigation component wrapper"
- path: "src/hooks/useNavigation.ts"
reason: "Navigation utility hooks"
- path: "src/types/navigation.ts"
reason: "TypeScript types for navigation items"
- path: "src/styles/navigation.css"
reason: "Base navigation styles"
- path: "README.md"
reason: "Document navigation implementation and usage"
# Metadata
metadata:
primary_blueprints: ["dashboard", "frontend"]
contributes_to:
- "Navigation components (horizontal, side, mobile, tabs, breadcrumbs)"
- "Routing integration (React Router, Next.js, Vue Router)"
- "Mobile navigation patterns (hamburger, bottom nav)"
- "Accessibility (ARIA, keyboard navigation, screen readers)"
- "Server-side routing (Flask, Django, FastAPI)"
- "Navigation state management"
- "URL synchronization and deep linking"
- "Command palette and search-driven navigation"
common_patterns:
- "NavLink for active state management"
- "URL parameter synchronization with useSearchParams"
- "ARIA landmarks and roles (navigation, tablist, breadcrumb)"
- "Keyboard navigation (Tab, Arrow keys, Enter, Escape)"
- "Focus management with useRef and FocusTrap"
- "Responsive design with useMediaQuery"
- "Lazy loading with React.lazy and Suspense"
- "Route-based code splitting"
- "Progressive enhancement (works without JavaScript)"
- "Flask blueprints for route organization"
- "Django URL namespaces and includes"
- "FastAPI routers with dependencies"
integration_points:
theming_components: "Navigation uses design tokens for colors, spacing, typography"
implementing_forms: "Navigation often contains search forms"
implementing_dashboards: "Dashboards require side or top navigation"
implementing_auth: "Navigation shows/hides based on auth state"
implementing_search: "Command palette integrates with search functionality"
typical_directory_structure: |
project/
├── src/
│ ├── components/
│ │ └── navigation/
│ │ ├── HorizontalMenu.tsx # Top navigation
│ │ ├── SideNavigation.tsx # Sidebar navigation
│ │ ├── MobileNavigation.tsx # Mobile hamburger/drawer
│ │ ├── TabNavigation.tsx # Tab switching
│ │ ├── Breadcrumbs.tsx # Breadcrumb trail
│ │ ├── Pagination.tsx # Page navigation
│ │ ├── CommandPalette.tsx # Search-driven nav
│ │ ├── Stepper.tsx # Multi-step wizard
│ │ └── TableOfContents.tsx # Auto-generated TOC
│ ├── config/
│ │ ├── navigation.ts # Nav structure config
│ │ ├── routes.ts # Route definitions
│ │ └── navigation-tree.json # Hierarchical nav data
│ ├── hooks/
│ │ ├── useNavigation.ts # Navigation utilities
│ │ ├── useBreadcrumbs.ts # Dynamic breadcrumbs
│ │ └── useKeyboardShortcuts.ts # Keyboard navigation
│ ├── types/
│ │ └── navigation.ts # TypeScript types
│ ├── utils/
│ │ └── navigation-tree-validator.ts # Validation utilities
│ └── App.tsx # Router setup
│
├── app/ (Backend - Flask/Django/FastAPI)
│ ├── routes/ # Flask blueprints
│ │ ├── __init__.py
│ │ ├── main.py
│ │ └── api/
│ │ └── v1/
│ ├── urls.py # Django URL config
│ └── routers/ # FastAPI routers
│ ├── __init__.py
│ └── items.py
│
├── examples/ # Working examples
│ ├── horizontal-menu.tsx
│ ├── tab-navigation.tsx
│ ├── mobile-navigation.tsx
│ ├── flask_routes.py
│ ├── django_urls.py
│ └── fastapi_routes.py
│
├── references/ # Documentation
│ ├── menu-patterns.md
│ ├── navigation-components.md
│ ├── client-routing.md
│ ├── flask-routing.md
│ ├── django-urls.md
│ ├── fastapi-routing.md
│ ├── accessibility-navigation.md
│ └── library-comparison.md
│
├── scripts/ # Utilities
│ ├── validate_navigation_tree.js # Nav structure validation
│ ├── calculate_breadcrumbs.js # Breadcrumb generation
│ └── generate_routes.py # Route config generator
│
├── assets/
│ ├── navigation-config-schema.json # Nav tree schema
│ └── route-templates.json # Route patterns
│
└── package.json / requirements.txt # Dependencies
tools_required:
- name: "react-router-dom"
version: "^6.x"
purpose: "Client-side routing for React"
install: "npm install react-router-dom"
condition: "frontend_framework: react"
- name: "vue-router"
version: "^4.x"
purpose: "Client-side routing for Vue"
install: "npm install vue-router"
condition: "frontend_framework: vue"
- name: "next"
version: "^14.x"
purpose: "Next.js App Router"
install: "npm install next"
condition: "frontend_framework: nextjs"
- name: "Flask"
purpose: "Python web framework with blueprints"
install: "pip install Flask"
condition: "backend_framework: flask"
- name: "Django"
purpose: "Python web framework with URL configuration"
install: "pip install Django"
condition: "backend_framework: django"
- name: "FastAPI"
purpose: "Modern Python API framework with routers"
install: "pip install fastapi uvicorn"
condition: "backend_framework: fastapi"
- name: "cmdk"
version: "^1.x"
purpose: "Command palette component (optional, for advanced)"
install: "npm install cmdk"
condition: "maturity: advanced"
validation_checks:
- "All navigation links use semantic HTML (<nav>, <a>)"
- "ARIA landmarks properly applied (role='navigation', aria-label)"
- "Keyboard navigation supported (Tab, Arrow keys, Enter, Escape)"
- "Active states indicated with aria-current"
- "Focus indicators visible (2px minimum, 3:1 contrast)"
- "Mobile navigation responsive (hamburger menu, bottom nav)"
- "URL synchronization for tabs and filters"
- "Route lazy loading for performance"
- "Breadcrumbs use aria-label='breadcrumb'"
- "Dropdowns use aria-expanded state"
- "Navigation works without JavaScript (progressive enhancement)"
- "Backend routes organized with blueprints/routers/URL conf"
- "Route parameters validated"
- "Deep linking functional"
anti_patterns:
- name: "Using <div> for navigation links"
avoid: "<div onClick={...}>Link</div>"
use: "<a href='...' onClick={...}>Link</a> or <NavLink>"
- name: "Missing ARIA attributes"
avoid: "No aria-label or aria-current"
use: "aria-label='Main navigation', aria-current='page'"
- name: "No keyboard navigation"
avoid: "Only mouse/touch support"
use: "Tab, Arrow keys, Enter, Escape handlers"
- name: "Hardcoded routes in components"
avoid: "const routes = [{...}] in component"
use: "Centralized routes.ts configuration file"
- name: "No mobile responsiveness"
avoid: "Desktop-only navigation"
use: "Responsive design with hamburger menu"
- name: "Missing active state"
avoid: "All links look the same"
use: "NavLink with aria-current='page'"
- name: "No URL synchronization"
avoid: "Local state only for tabs/filters"
use: "useSearchParams for bookmarkable state"
- name: "Monolithic route file"
avoid: "All routes in single app.py/urls.py"
use: "Blueprints/routers/namespaces for organization"
- name: "No loading states"
avoid: "Blank screen during lazy loading"
use: "Suspense with fallback UI"
- name: "Physical CSS properties"
avoid: "margin-left, padding-right"
use: "margin-inline-start, padding-inline-end (for RTL)"
Accessible Navigation Patterns
Table of Contents
- ARIA Patterns
- Keyboard Navigation
- Focus Management
- Screen Reader Support
- Mobile Accessibility
- Testing & Validation
ARIA Patterns
Navigation Landmarks
<!-- Main navigation with proper ARIA -->
<nav aria-label="Main navigation" role="navigation">
<ul role="menubar" aria-label="Site sections">
<li role="none">
<a role="menuitem" href="/" aria-current="page">Home</a>
</li>
<li role="none">
<button
role="menuitem"
aria-haspopup="true"
aria-expanded="false"
aria-controls="products-menu"
>
Products
<span aria-hidden="true">▼</span>
</button>
<ul role="menu" id="products-menu" aria-label="Products">
<li role="none">
<a role="menuitem" href="/products/category1">Category 1</a>
</li>
<li role="none">
<a role="menuitem" href="/products/category2">Category 2</a>
</li>
</ul>
</li>
</ul>
</nav>
<!-- Secondary navigation -->
<nav aria-label="Breadcrumb" role="navigation">
<ol aria-label="Breadcrumb trail">
<li><a href="/">Home</a></li>
<li aria-current="page">Current Page</li>
</ol>
</nav>
<!-- Page navigation -->
<nav aria-label="Pagination" role="navigation">
<ul>
<li>
<a href="?page=1" aria-label="Go to previous page">Previous</a>
</li>
<li>
<a href="?page=1" aria-label="Go to page 1">1</a>
</li>
<li>
<span aria-current="page" aria-label="Current page, page 2">2</span>
</li>
<li>
<a href="?page=3" aria-label="Go to page 3">3</a>
</li>
<li>
<a href="?page=3" aria-label="Go to next page">Next</a>
</li>
</ul>
</nav>Menu ARIA Patterns
// React implementation with proper ARIA
interface MenuProps {
items: MenuItem[];
}
const AccessibleMenu: React.FC<MenuProps> = ({ items }) => {
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
const [focusedIndex, setFocusedIndex] = useState<number>(-1);
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setFocusedIndex((prev) => Math.min(prev + 1, items.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setFocusedIndex((prev) => Math.max(prev - 1, 0));
break;
case 'ArrowRight':
if (items[index].children) {
e.preventDefault();
setActiveSubmenu(items[index].id);
// Focus first item in submenu
}
break;
case 'ArrowLeft':
case 'Escape':
e.preventDefault();
setActiveSubmenu(null);
break;
case 'Home':
e.preventDefault();
setFocusedIndex(0);
break;
case 'End':
e.preventDefault();
setFocusedIndex(items.length - 1);
break;
case ' ':
case 'Enter':
e.preventDefault();
if (items[index].children) {
setActiveSubmenu(
activeSubmenu === items[index].id ? null : items[index].id
);
} else if (items[index].href) {
window.location.href = items[index].href;
}
break;
}
};
return (
<ul role="menubar" aria-label="Main navigation">
{items.map((item, index) => (
<li key={item.id} role="none">
{item.children ? (
<>
<button
role="menuitem"
aria-haspopup="true"
aria-expanded={activeSubmenu === item.id}
aria-controls={`submenu-${item.id}`}
tabIndex={focusedIndex === index ? 0 : -1}
onKeyDown={(e) => handleKeyDown(e, index)}
onClick={() => setActiveSubmenu(
activeSubmenu === item.id ? null : item.id
)}
>
{item.label}
</button>
{activeSubmenu === item.id && (
<ul
role="menu"
id={`submenu-${item.id}`}
aria-label={`${item.label} submenu`}
>
{item.children.map((child) => (
<li key={child.id} role="none">
<a role="menuitem" href={child.href} tabIndex={-1}>
{child.label}
</a>
</li>
))}
</ul>
)}
</>
) : (
<a
role="menuitem"
href={item.href}
tabIndex={focusedIndex === index ? 0 : -1}
onKeyDown={(e) => handleKeyDown(e, index)}
aria-current={isCurrentPage(item.href) ? 'page' : undefined}
>
{item.label}
</a>
)}
</li>
))}
</ul>
);
};Tab ARIA Pattern
const AccessibleTabs: React.FC<{ tabs: Tab[] }> = ({ tabs }) => {
const [activeTab, setActiveTab] = useState(0);
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
let newIndex = index;
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
newIndex = (index + 1) % tabs.length;
break;
case 'ArrowLeft':
e.preventDefault();
newIndex = index === 0 ? tabs.length - 1 : index - 1;
break;
case 'Home':
e.preventDefault();
newIndex = 0;
break;
case 'End':
e.preventDefault();
newIndex = tabs.length - 1;
break;
default:
return;
}
setActiveTab(newIndex);
tabRefs.current[newIndex]?.focus();
};
return (
<div className="tabs">
<div role="tablist" aria-label="Tabs">
{tabs.map((tab, index) => (
<button
key={tab.id}
ref={(el) => (tabRefs.current[index] = el)}
role="tab"
id={`tab-${tab.id}`}
aria-selected={activeTab === index}
aria-controls={`panel-${tab.id}`}
tabIndex={activeTab === index ? 0 : -1}
onClick={() => setActiveTab(index)}
onKeyDown={(e) => handleKeyDown(e, index)}
>
{tab.label}
</button>
))}
</div>
{tabs.map((tab, index) => (
<div
key={tab.id}
role="tabpanel"
id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={activeTab !== index}
tabIndex={0}
>
{tab.content}
</div>
))}
</div>
);
};Keyboard Navigation
Key Bindings Reference
// Keyboard navigation patterns for different components
const NAVIGATION_KEYS = {
// Basic navigation
TAB: 'Tab',
SHIFT_TAB: 'Shift+Tab',
ENTER: 'Enter',
SPACE: ' ',
ESCAPE: 'Escape',
// Arrow navigation
ARROW_UP: 'ArrowUp',
ARROW_DOWN: 'ArrowDown',
ARROW_LEFT: 'ArrowLeft',
ARROW_RIGHT: 'ArrowRight',
// Jump navigation
HOME: 'Home',
END: 'End',
PAGE_UP: 'PageUp',
PAGE_DOWN: 'PageDown'
};
// Component-specific patterns
const KEYBOARD_PATTERNS = {
menubar: {
[NAVIGATION_KEYS.ARROW_RIGHT]: 'Next menu item',
[NAVIGATION_KEYS.ARROW_LEFT]: 'Previous menu item',
[NAVIGATION_KEYS.ARROW_DOWN]: 'Open submenu or next item',
[NAVIGATION_KEYS.ARROW_UP]: 'Previous item',
[NAVIGATION_KEYS.HOME]: 'First menu item',
[NAVIGATION_KEYS.END]: 'Last menu item',
[NAVIGATION_KEYS.ENTER]: 'Activate menu item',
[NAVIGATION_KEYS.SPACE]: 'Activate menu item',
[NAVIGATION_KEYS.ESCAPE]: 'Close submenu'
},
tabs: {
[NAVIGATION_KEYS.ARROW_RIGHT]: 'Next tab',
[NAVIGATION_KEYS.ARROW_LEFT]: 'Previous tab',
[NAVIGATION_KEYS.HOME]: 'First tab',
[NAVIGATION_KEYS.END]: 'Last tab',
[NAVIGATION_KEYS.ENTER]: 'Activate tab (if not automatic)',
[NAVIGATION_KEYS.SPACE]: 'Activate tab (if not automatic)'
},
breadcrumb: {
[NAVIGATION_KEYS.TAB]: 'Next breadcrumb item',
[NAVIGATION_KEYS.SHIFT_TAB]: 'Previous breadcrumb item',
[NAVIGATION_KEYS.ENTER]: 'Navigate to breadcrumb'
},
pagination: {
[NAVIGATION_KEYS.TAB]: 'Next pagination control',
[NAVIGATION_KEYS.SHIFT_TAB]: 'Previous pagination control',
[NAVIGATION_KEYS.ENTER]: 'Navigate to page',
[NAVIGATION_KEYS.ARROW_RIGHT]: 'Next page (optional)',
[NAVIGATION_KEYS.ARROW_LEFT]: 'Previous page (optional)'
}
};Roving Tabindex Implementation
// Hook for implementing roving tabindex
const useRovingTabindex = (items: any[], loop = true) => {
const [focusedIndex, setFocusedIndex] = useState(0);
const refs = useRef<(HTMLElement | null)[]>([]);
useEffect(() => {
refs.current = refs.current.slice(0, items.length);
}, [items]);
const handleKeyDown = (e: React.KeyboardEvent, currentIndex: number) => {
let nextIndex = currentIndex;
switch (e.key) {
case 'ArrowDown':
case 'ArrowRight':
e.preventDefault();
if (currentIndex < items.length - 1) {
nextIndex = currentIndex + 1;
} else if (loop) {
nextIndex = 0;
}
break;
case 'ArrowUp':
case 'ArrowLeft':
e.preventDefault();
if (currentIndex > 0) {
nextIndex = currentIndex - 1;
} else if (loop) {
nextIndex = items.length - 1;
}
break;
case 'Home':
e.preventDefault();
nextIndex = 0;
break;
case 'End':
e.preventDefault();
nextIndex = items.length - 1;
break;
default:
return;
}
setFocusedIndex(nextIndex);
refs.current[nextIndex]?.focus();
};
const getTabIndex = (index: number) => {
return index === focusedIndex ? 0 : -1;
};
const setRef = (index: number) => (el: HTMLElement | null) => {
refs.current[index] = el;
};
return {
focusedIndex,
handleKeyDown,
getTabIndex,
setRef
};
};
// Usage example
const NavigationList: React.FC = () => {
const items = ['Home', 'Products', 'About', 'Contact'];
const { focusedIndex, handleKeyDown, getTabIndex, setRef } = useRovingTabindex(items);
return (
<ul role="list">
{items.map((item, index) => (
<li key={item}>
<a
ref={setRef(index)}
href={`#${item.toLowerCase()}`}
tabIndex={getTabIndex(index)}
onKeyDown={(e) => handleKeyDown(e, index)}
aria-current={focusedIndex === index ? 'true' : undefined}
>
{item}
</a>
</li>
))}
</ul>
);
};Focus Management
Skip Navigation Link
const SkipToContent: React.FC = () => {
return (
<>
<a
href="#main-content"
className="skip-link"
onClick={(e) => {
e.preventDefault();
const main = document.getElementById('main-content');
main?.focus();
main?.scrollIntoView();
}}
>
Skip to main content
</a>
<style jsx>{`
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--color-primary);
color: white;
padding: 8px 16px;
text-decoration: none;
z-index: 100;
border-radius: 0 0 4px 0;
}
.skip-link:focus {
top: 0;
}
`}</style>
</>
);
};Focus Trap for Modals/Drawers
const useFocusTrap = (isActive: boolean) => {
const containerRef = useRef<HTMLDivElement>(null);
const previousFocus = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isActive) {
// Store previous focus
previousFocus.current = document.activeElement as HTMLElement;
// Get focusable elements
const getFocusableElements = () => {
if (!containerRef.current) return [];
const focusableSelectors = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
return Array.from(
containerRef.current.querySelectorAll<HTMLElement>(focusableSelectors)
);
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
const focusableElements = getFocusableElements();
if (focusableElements.length === 0) return;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
// Tab
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
// Focus first element
const focusableElements = getFocusableElements();
if (focusableElements.length > 0) {
focusableElements[0].focus();
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
// Restore focus
previousFocus.current?.focus();
};
}
}, [isActive]);
return containerRef;
};
// Usage
const Modal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({
isOpen,
onClose,
children
}) => {
const focusTrapRef = useFocusTrap(isOpen);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div
ref={focusTrapRef}
className="modal-content"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
onClick={(e) => e.stopPropagation()}
>
<button
className="close-button"
onClick={onClose}
aria-label="Close modal"
>
×
</button>
{children}
</div>
</div>
);
};Focus Indicators
/* Visible focus indicators */
:focus {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Remove default outline, add custom */
a:focus,
button:focus,
input:focus,
select:focus,
textarea:focus {
outline: none;
box-shadow: 0 0 0 2px var(--color-background),
0 0 0 4px var(--color-primary);
}
/* Focus visible - only show focus on keyboard navigation */
:focus:not(:focus-visible) {
outline: none;
box-shadow: none;
}
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* High contrast mode support */
@media (prefers-contrast: high) {
:focus {
outline: 3px solid currentColor;
outline-offset: 2px;
}
}
/* Forced colors mode (Windows High Contrast) */
@media (forced-colors: active) {
:focus {
outline: 3px solid currentColor;
}
}Screen Reader Support
Announcing Navigation Changes
// Live region for route changes
const RouteAnnouncer: React.FC = () => {
const location = useLocation();
const [announcement, setAnnouncement] = useState('');
useEffect(() => {
// Get page title or route name
const pageTitle = document.title || 'New page';
setAnnouncement(`Navigated to ${pageTitle}`);
// Clear announcement after screen reader reads it
const timer = setTimeout(() => {
setAnnouncement('');
}, 1000);
return () => clearTimeout(timer);
}, [location]);
return (
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcement}
</div>
);
};
// CSS for screen reader only content
const srOnlyStyles = `
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
`;Descriptive Labels
// Proper labeling for navigation elements
const NavigationWithLabels: React.FC = () => {
return (
<nav aria-label="Main navigation">
{/* Icon-only buttons need labels */}
<button aria-label="Open menu">
<MenuIcon aria-hidden="true" />
</button>
{/* Current page indicator */}
<a href="/home" aria-current="page">
Home
<span className="sr-only">(current page)</span>
</a>
{/* Badge with context */}
<a href="/cart">
Cart
<span className="badge" aria-label="3 items in cart">
3
</span>
</a>
{/* Loading state */}
<div role="status" aria-label="Loading navigation">
<Spinner />
<span className="sr-only">Loading navigation items...</span>
</div>
{/* Search with proper labeling */}
<form role="search" aria-label="Site search">
<label htmlFor="search-input" className="sr-only">
Search the site
</label>
<input
id="search-input"
type="search"
placeholder="Search..."
aria-describedby="search-hint"
/>
<span id="search-hint" className="sr-only">
Press Enter to search
</span>
</form>
</nav>
);
};Mobile Accessibility
Touch Target Sizes
/* Minimum touch target size: 44x44px (iOS) or 48x48px (Android) */
.nav-link,
.nav-button {
min-height: 44px;
min-width: 44px;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
}
/* Ensure adequate spacing between targets */
.nav-list {
display: flex;
gap: 8px; /* Minimum 8px between targets */
}
/* Mobile-specific adjustments */
@media (max-width: 768px) {
.nav-link,
.nav-button {
min-height: 48px;
min-width: 48px;
font-size: 16px; /* Prevent zoom on iOS */
}
}Gesture Alternatives
// Swipe navigation with keyboard alternative
const SwipeableNavigation: React.FC = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const pages = ['Page 1', 'Page 2', 'Page 3'];
const goToNext = () => {
setCurrentIndex((prev) => Math.min(prev + 1, pages.length - 1));
};
const goToPrevious = () => {
setCurrentIndex((prev) => Math.max(prev - 1, 0));
};
const handlers = useSwipeable({
onSwipedLeft: goToNext,
onSwipedRight: goToPrevious,
preventDefaultTouchmoveEvent: true,
trackMouse: true
});
return (
<div {...handlers} role="region" aria-label="Swipeable content">
{/* Current page */}
<div aria-live="polite">
{pages[currentIndex]}
</div>
{/* Keyboard-accessible controls */}
<div className="navigation-controls">
<button
onClick={goToPrevious}
disabled={currentIndex === 0}
aria-label="Go to previous page"
>
Previous
</button>
<span aria-label={`Page ${currentIndex + 1} of ${pages.length}`}>
{currentIndex + 1} / {pages.length}
</span>
<button
onClick={goToNext}
disabled={currentIndex === pages.length - 1}
aria-label="Go to next page"
>
Next
</button>
</div>
</div>
);
};Testing & Validation
Accessibility Testing Checklist
// Automated testing with jest-axe
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe('Navigation Accessibility', () => {
test('should have no WCAG violations', async () => {
const { container } = render(<Navigation />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('should have proper ARIA attributes', () => {
const { getByRole } = render(<Navigation />);
const nav = getByRole('navigation');
expect(nav).toHaveAttribute('aria-label');
const menubar = getByRole('menubar');
expect(menubar).toBeInTheDocument();
const menuitems = getAllByRole('menuitem');
menuitems.forEach((item) => {
if (item.getAttribute('aria-haspopup')) {
expect(item).toHaveAttribute('aria-expanded');
}
});
});
test('should support keyboard navigation', () => {
const { getAllByRole } = render(<Navigation />);
const items = getAllByRole('menuitem');
// Focus first item
items[0].focus();
expect(document.activeElement).toBe(items[0]);
// Arrow right moves to next
fireEvent.keyDown(items[0], { key: 'ArrowRight' });
expect(document.activeElement).toBe(items[1]);
// Arrow left moves to previous
fireEvent.keyDown(items[1], { key: 'ArrowLeft' });
expect(document.activeElement).toBe(items[0]);
});
});
// Manual testing checklist
const ACCESSIBILITY_TESTS = {
keyboard: [
'Can navigate entire menu with keyboard only',
'Tab order is logical',
'Focus indicators are visible',
'Can activate all interactive elements',
'Escape key closes submenus',
'No keyboard traps'
],
screenReader: [
'All navigation items are announced',
'Current page is identified',
'Menu structure is conveyed',
'State changes are announced',
'Landmarks are properly labeled'
],
mobile: [
'Touch targets are at least 44x44px',
'Adequate spacing between targets',
'Works with screen reader gestures',
'Hamburger menu is accessible'
],
wcag: [
'Meets color contrast requirements (4.5:1)',
'Focus indicators meet contrast requirements (3:1)',
'Text can be resized to 200% without loss of functionality',
'Works without JavaScript',
'Proper heading hierarchy'
]
};Browser Testing Tools
// Accessibility testing script
const testNavigationAccessibility = () => {
// Check for skip links
const skipLinks = document.querySelectorAll('a[href^="#"]');
console.log(`Found ${skipLinks.length} skip links`);
// Check ARIA labels
const unlabeledNavs = document.querySelectorAll('nav:not([aria-label])');
if (unlabeledNavs.length > 0) {
console.warn('Found navigation elements without aria-label:', unlabeledNavs);
}
// Check focus order
const focusableElements = document.querySelectorAll(
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
let tabIndexes = [];
focusableElements.forEach((el) => {
const tabIndex = el.getAttribute('tabindex');
if (tabIndex && parseInt(tabIndex) > 0) {
tabIndexes.push({ element: el, tabIndex });
}
});
if (tabIndexes.length > 0) {
console.warn('Found elements with positive tabindex (avoid):', tabIndexes);
}
// Check color contrast
const checkContrast = (foreground, background) => {
// Simplified contrast calculation
const getLuminance = (color) => {
// Convert color to luminance
return 0.5; // Placeholder
};
const ratio = (Math.max(getLuminance(foreground), getLuminance(background)) + 0.05) /
(Math.min(getLuminance(foreground), getLuminance(background)) + 0.05);
return ratio >= 4.5; // WCAG AA standard
};
console.log('Accessibility check complete');
};Related skills
FAQ
How do I choose a navigation pattern?
The skill maps information architecture to patterns: flat hierarchies use top navigation, deep ones use side navigation, e-commerce uses mega menus, and linear processes use steppers.
Does it cover backend routing?
Yes. It covers server-side route configuration for Flask blueprints, Django URL conf, and FastAPI routers alongside frontend routing.