Navigation Control Recipe
Advanced patterns for voice-controlled navigation in your application.
Overview
This recipe covers advanced navigation patterns, including dynamic routes, protected routes, navigation with state, and cross-tab navigation.
Dynamic Route Navigation
Route Parameters
Navigate to routes with dynamic parameters:
typescript
vowel.registerAction('viewProduct', {
description: 'View a specific product',
parameters: {
productId: {
type: 'string',
description: 'Product ID',
required: true
}
}
}, async ({ productId }) => {
await router.push(`/product/${productId}`);
// Fetch and announce product
const product = await getProduct(productId);
await vowel.notifyEvent(`Viewing ${product.name}`);
return { success: true };
});Category Navigation
typescript
vowel.registerAction('browseCategory', {
description: 'Browse products by category',
parameters: {
category: {
type: 'string',
description: 'Product category',
enum: ['electronics', 'clothing', 'books', 'home'],
required: true
}
}
}, async ({ category }) => {
await router.push(`/products/${category}`);
const count = await getProductCount(category);
await vowel.notifyEvent(
`Showing ${count} products in ${category}`
);
return { success: true };
});Search Navigation
typescript
vowel.registerAction('searchAndNavigate', {
description: 'Search and navigate to results',
parameters: {
query: {
type: 'string',
description: 'Search query',
required: true
},
category: {
type: 'string',
description: 'Category filter'
}
}
}, async ({ query, category }) => {
const params = new URLSearchParams({ q: query });
if (category) params.set('category', category);
await router.push(`/search?${params.toString()}`);
return { success: true };
});Protected Routes
Authentication Check
typescript
vowel.registerAction('goToAccount', {
description: 'Navigate to account settings',
parameters: {}
}, async () => {
const user = getCurrentUser();
if (!user) {
await vowel.notifyEvent('Please sign in to access your account');
await router.push('/login');
return {
success: false,
error: 'Authentication required'
};
}
await router.push('/account');
return { success: true };
});Role-Based Navigation
typescript
vowel.registerAction('goToAdmin', {
description: 'Navigate to admin dashboard',
parameters: {}
}, async () => {
const user = getCurrentUser();
if (!user) {
await vowel.notifyEvent('Please sign in');
await router.push('/login');
return { success: false, error: 'Authentication required' };
}
if (!user.roles.includes('admin')) {
await vowel.notifyEvent('You do not have admin access');
return { success: false, error: 'Insufficient permissions' };
}
await router.push('/admin');
await vowel.notifyEvent('Navigated to admin dashboard');
return { success: true };
});Navigation with State
Passing Context
typescript
vowel.registerAction('viewProductFromSearch', {
description: 'View product from search results',
parameters: {
productId: { type: 'string', required: true }
}
}, async ({ productId }) => {
await router.push(`/product/${productId}`, {
state: {
fromSearch: true,
searchQuery: getCurrentSearchQuery(),
returnUrl: '/search'
}
});
return { success: true };
});
// In product page
function ProductPage() {
const location = useLocation();
const state = location.state;
if (state?.fromSearch) {
// Show back to search button
// Highlight search terms
}
}Breadcrumb Navigation
typescript
class NavigationHistory {
private history: string[] = [];
push(path: string) {
this.history.push(path);
}
goBack() {
this.history.pop();
return this.history[this.history.length - 1];
}
getCrumbs() {
return this.history;
}
}
const navHistory = new NavigationHistory();
vowel.registerAction('goBack', {
description: 'Go back to previous page',
parameters: {}
}, async () => {
const previousPath = navHistory.goBack();
if (previousPath) {
await router.push(previousPath);
await vowel.notifyEvent('Navigated back');
return { success: true };
}
return {
success: false,
error: 'No previous page'
};
});Smart Navigation
Context-Aware Navigation
typescript
vowel.registerAction('goToCheckout', {
description: 'Proceed to checkout',
parameters: {}
}, async () => {
const cart = await getCart();
// Check if cart is empty
if (cart.items.length === 0) {
await vowel.notifyEvent('Your cart is empty. Add some items first.');
await router.push('/products');
return { success: false, error: 'Cart is empty' };
}
// Check authentication
const user = getCurrentUser();
if (!user) {
await vowel.notifyEvent('Please sign in to checkout');
await router.push('/login?redirect=/checkout');
return { success: false, error: 'Authentication required' };
}
// Navigate to checkout
await router.push('/checkout');
await vowel.notifyEvent(`Proceeding to checkout with ${cart.items.length} items`);
return { success: true };
});Conditional Navigation
typescript
vowel.registerAction('continueToPayment', {
description: 'Continue to payment',
parameters: {}
}, async () => {
const checkout = getCheckoutState();
// Validate shipping address
if (!checkout.shippingAddress) {
await vowel.notifyEvent('Please provide a shipping address');
await router.push('/checkout/shipping');
return { success: false, error: 'Shipping address required' };
}
// Validate shipping method
if (!checkout.shippingMethod) {
await vowel.notifyEvent('Please select a shipping method');
await router.push('/checkout/shipping');
return { success: false, error: 'Shipping method required' };
}
// Navigate to payment
await router.push('/checkout/payment');
await vowel.notifyEvent('Proceeding to payment');
return { success: true };
});Cross-Tab Navigation
Controlled Navigation
For traditional sites with page reloads (Shopify, WordPress):
typescript
// In voice agent tab
import { ControlledNavigationAdapter } from '@vowel.to/client';
const navigationAdapter = new ControlledNavigationAdapter({
channelName: 'vowel-nav'
});
const vowel = new Vowel({
appId: 'your-app-id',
navigationAdapter
});
// In content tabs
const navChannel = new BroadcastChannel('vowel-nav');
navChannel.onmessage = (event) => {
if (event.data.type === 'navigate') {
// Optionally show loading indicator
showLoadingIndicator();
// Navigate
window.location.href = event.data.url;
}
};Cross-Tab State Sync
typescript
// Sync navigation state across tabs
const stateChannel = new BroadcastChannel('vowel-state');
// Send navigation event
router.afterEach((to) => {
stateChannel.postMessage({
type: 'navigation',
path: to.path,
timestamp: Date.now()
});
});
// Receive in other tabs
stateChannel.onmessage = (event) => {
if (event.data.type === 'navigation') {
console.log('Navigation in another tab:', event.data.path);
// Update UI accordingly
}
};Navigation Notifications
Route Change Announcements
typescript
// Announce navigation
router.afterEach((to) => {
const routeNames: Record<string, string> = {
'/': 'Home',
'/products': 'Products',
'/cart': 'Shopping Cart',
'/checkout': 'Checkout'
};
const name = routeNames[to.path];
if (name) {
vowel.notifyEvent(`Navigated to ${name}`);
}
});Loading States
typescript
vowel.registerAction('navigateWithLoading', {
description: 'Navigate to page',
parameters: {
path: { type: 'string', required: true }
}
}, async ({ path }) => {
// Announce navigation start
await vowel.notifyEvent('Loading page...');
// Navigate
await router.push(path);
// Wait for page load
await waitForPageLoad();
// Announce completion
await vowel.notifyEvent('Page loaded');
return { success: true };
});
function waitForPageLoad(): Promise<void> {
return new Promise((resolve) => {
if (document.readyState === 'complete') {
resolve();
} else {
window.addEventListener('load', () => resolve(), { once: true });
}
});
}Multi-Step Navigation
Wizard Navigation
typescript
class CheckoutWizard {
private steps = ['cart', 'shipping', 'payment', 'review'];
private currentStep = 0;
async next() {
if (this.currentStep < this.steps.length - 1) {
this.currentStep++;
const step = this.steps[this.currentStep];
await router.push(`/checkout/${step}`);
await vowel.notifyEvent(`Step ${this.currentStep + 1}: ${step}`);
return true;
}
return false;
}
async previous() {
if (this.currentStep > 0) {
this.currentStep--;
const step = this.steps[this.currentStep];
await router.push(`/checkout/${step}`);
await vowel.notifyEvent(`Back to ${step}`);
return true;
}
return false;
}
getCurrentStep() {
return this.steps[this.currentStep];
}
}
const wizard = new CheckoutWizard();
vowel.registerAction('nextCheckoutStep', {
description: 'Continue to next checkout step',
parameters: {}
}, async () => {
const success = await wizard.next();
return {
success,
message: success ? 'Moved to next step' : 'Already at last step'
};
});
vowel.registerAction('previousCheckoutStep', {
description: 'Go back to previous checkout step',
parameters: {}
}, async () => {
const success = await wizard.previous();
return {
success,
message: success ? 'Moved to previous step' : 'Already at first step'
};
});Deep Linking
URL State Management
typescript
vowel.registerAction('filterAndNavigate', {
description: 'Filter products and navigate',
parameters: {
category: { type: 'string' },
minPrice: { type: 'number' },
maxPrice: { type: 'number' },
sortBy: { type: 'string', enum: ['price', 'name', 'rating'] }
}
}, async (params) => {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
searchParams.set(key, value.toString());
}
});
await router.push(`/products?${searchParams.toString()}`);
return { success: true };
});Shareable URLs
typescript
vowel.registerAction('shareCurrentPage', {
description: 'Share current page',
parameters: {}
}, async () => {
const url = window.location.href;
// Copy to clipboard
await navigator.clipboard.writeText(url);
await vowel.notifyEvent('Page URL copied to clipboard');
return {
success: true,
data: { url }
};
});Best Practices
- Clear Descriptions - Provide clear route descriptions
- Validate State - Check prerequisites before navigation
- Handle Errors - Gracefully handle navigation failures
- Provide Feedback - Announce navigation changes
- Context Awareness - Consider user state and permissions
- Loading States - Show feedback during navigation
- Deep Linking - Support URL parameters for shareable links
- Breadcrumbs - Maintain navigation history
- Protected Routes - Enforce authentication and authorization
- Cross-Tab Sync - Coordinate navigation across tabs
Related
- Adapters Guide - Navigation adapter details
- React Router - React Router integration
- Next.js - Next.js integration
- API Reference - Complete API documentation