useNudge
useNudge hook: manage nudge visibility, dismissal, snooze, and user interaction state for feature discovery prompts
Overview
useNudge provides access to the nudge system, allowing you to show, dismiss, and snooze nudges for features. The hook integrates with the automatic nudge scheduler configured in AdoptionProvider.
Basic Usage
import { useNudge } from '@tour-kit/adoption'
function NudgeManager() {
const { pendingNudges, dismissNudge } = useNudge()
if (pendingNudges.length === 0) return null
const feature = pendingNudges[0]
return (
<div className="nudge">
<p>Try {feature.name}!</p>
<button onClick={() => dismissNudge(feature.id)}>Dismiss</button>
</div>
)
}Return Value
Prop
Type
Examples
Simple Nudge Display
Show the highest-priority pending nudge:
function SimpleNudge() {
const { pendingNudges, handleNudgeClick, dismissNudge } = useNudge()
if (pendingNudges.length === 0) return null
const feature = pendingNudges[0]
return (
<div className="nudge-toast">
<h4>{feature.name}</h4>
<p>{feature.description || 'Try this new feature!'}</p>
<div className="nudge-actions">
<button onClick={() => handleNudgeClick(feature.id)}>
Try it now
</button>
<button onClick={() => dismissNudge(feature.id)}>
Dismiss
</button>
</div>
</div>
)
}Nudge with Snooze
Allow users to postpone nudges:
function SnoozeableNudge() {
const { pendingNudges, handleNudgeClick, dismissNudge, snoozeNudge } = useNudge()
if (!pendingNudges[0]) return null
const feature = pendingNudges[0]
const handleSnooze = (hours: number) => {
snoozeNudge(feature.id, hours * 60 * 60 * 1000)
}
return (
<div className="nudge">
<p>Discover {feature.name}</p>
<div className="actions">
<button onClick={() => handleNudgeClick(feature.id)}>
Try Now
</button>
<button onClick={() => handleSnooze(1)}>Remind me in 1 hour</button>
<button onClick={() => handleSnooze(24)}>Remind me tomorrow</button>
<button onClick={() => dismissNudge(feature.id)}>
Don't show again
</button>
</div>
</div>
)
}Multi-Nudge Display
Show multiple nudges at once:
function MultiNudgeList() {
const { pendingNudges, handleNudgeClick, dismissNudge } = useNudge()
if (pendingNudges.length === 0) {
return <p>You've discovered all features!</p>
}
return (
<div className="nudge-list">
<h3>Features to Try ({pendingNudges.length})</h3>
{pendingNudges.map((feature) => (
<div key={feature.id} className="nudge-item">
<div className="nudge-content">
<h4>{feature.name}</h4>
{feature.description && <p>{feature.description}</p>}
{feature.category && (
<span className="category-badge">{feature.category}</span>
)}
</div>
<div className="nudge-actions">
<button onClick={() => handleNudgeClick(feature.id)}>Try</button>
<button onClick={() => dismissNudge(feature.id)}>✕</button>
</div>
</div>
))}
</div>
)
}Animated Nudge
Show nudges with entrance/exit animations:
function AnimatedNudge() {
const { pendingNudges, showNudge, dismissNudge } = useNudge()
const [visible, setVisible] = useState(false)
const [currentFeature, setCurrentFeature] = useState<Feature | null>(null)
useEffect(() => {
if (pendingNudges.length > 0 && !currentFeature) {
const feature = pendingNudges[0]
setCurrentFeature(feature)
showNudge(feature.id) // Track nudge shown
// Animate in after slight delay
setTimeout(() => setVisible(true), 100)
}
}, [pendingNudges, currentFeature, showNudge])
const handleDismiss = () => {
if (!currentFeature) return
setVisible(false)
setTimeout(() => {
dismissNudge(currentFeature.id)
setCurrentFeature(null)
}, 300) // Wait for exit animation
}
if (!currentFeature) return null
return (
<div className={`nudge-toast ${visible ? 'visible' : ''}`}>
<p>{currentFeature.name}</p>
<button onClick={handleDismiss}>Dismiss</button>
</div>
)
}Contextual Nudge Placement
Show nudges near related UI elements:
function FeatureButton({ featureId }: { featureId: string }) {
const { pendingNudges, handleNudgeClick, dismissNudge } = useNudge()
const shouldNudge = pendingNudges.some((f) => f.id === featureId)
return (
<div className="relative">
<button id={`feature-${featureId}`}>Feature Button</button>
{shouldNudge && (
<div className="nudge-tooltip">
<p>Try this feature!</p>
<button onClick={() => handleNudgeClick(featureId)}>OK</button>
<button onClick={() => dismissNudge(featureId)}>Dismiss</button>
</div>
)}
</div>
)
}Nudge with Tour Integration
Launch a tour when user clicks the nudge:
function NudgeWithTour() {
const { pendingNudges, handleNudgeClick, dismissNudge } = useNudge()
const { startTour } = useTour() // From @tour-kit/react
if (pendingNudges.length === 0) return null
const feature = pendingNudges[0]
const handleTryIt = () => {
handleNudgeClick(feature.id)
// Launch tour if available
if (feature.resources?.tourId) {
startTour(feature.resources.tourId)
}
}
return (
<div className="nudge">
<p>Learn about {feature.name}</p>
<button onClick={handleTryIt}>
{feature.resources?.tourId ? 'Take Tour' : 'Try Now'}
</button>
<button onClick={() => dismissNudge(feature.id)}>Dismiss</button>
</div>
)
}Nudge Scheduling UI
Show when nudges will appear:
function NudgeSchedule() {
const { pendingNudges } = useNudge()
const nudgeConfig = useNudgeConfig() // Your custom hook to get config
return (
<div className="nudge-settings">
<h3>Upcoming Nudges</h3>
{pendingNudges.length === 0 ? (
<p>No pending nudges</p>
) : (
<ul>
{pendingNudges.slice(0, 5).map((feature, index) => (
<li key={feature.id}>
<strong>{feature.name}</strong>
<span className="priority">Priority: {feature.priority || 0}</span>
</li>
))}
</ul>
)}
<div className="nudge-config">
<p>Nudge cooldown: {nudgeConfig.cooldown / 3600000}h</p>
<p>Max per session: {nudgeConfig.maxPerSession}</p>
</div>
</div>
)
}Nudge Priority
Pending nudges are automatically sorted by priority (highest first):
function PriorityNudges() {
const { pendingNudges } = useNudge()
return (
<div>
{pendingNudges.map((feature, index) => (
<div key={feature.id} className="nudge-item">
<span className="rank">#{index + 1}</span>
<span className="name">{feature.name}</span>
<span className="priority">Priority: {feature.priority || 0}</span>
</div>
))}
</div>
)
}Features with higher priority values appear first in pendingNudges.
Tracking Nudge Interactions
Manual Tracking
Use showNudge to track when a nudge is displayed:
function TrackedNudge() {
const { pendingNudges, showNudge } = useNudge()
useEffect(() => {
if (pendingNudges.length > 0) {
const feature = pendingNudges[0]
showNudge(feature.id) // Tracks "nudge shown" event
}
}, [pendingNudges, showNudge])
// ... render nudge
}Click Tracking
handleNudgeClick automatically:
- Tracks feature usage
- Dismisses the nudge
- Fires analytics events (if configured)
<button onClick={() => handleNudgeClick(feature.id)}>
Try Feature
</button>This is equivalent to:
const { trackUsage } = useFeature(feature.id)
const { dismissNudge } = useNudge()
<button
onClick={() => {
trackUsage()
dismissNudge(feature.id)
}}
>
Try Feature
</button>Snooze Duration
Common snooze durations:
const SNOOZE_DURATIONS = {
'1 hour': 60 * 60 * 1000,
'4 hours': 4 * 60 * 60 * 1000,
'1 day': 24 * 60 * 60 * 1000,
'1 week': 7 * 24 * 60 * 60 * 1000,
}
function CustomSnooze() {
const { snoozeNudge, pendingNudges } = useNudge()
if (!pendingNudges[0]) return null
return (
<div>
<p>Remind me:</p>
{Object.entries(SNOOZE_DURATIONS).map(([label, duration]) => (
<button
key={label}
onClick={() => snoozeNudge(pendingNudges[0].id, duration)}
>
{label}
</button>
))}
</div>
)
}Snoozed nudges won't appear in pendingNudges until the snooze duration expires.
Conditional Nudging
Only show nudges under certain conditions:
function ConditionalNudge() {
const { pendingNudges } = useNudge()
const user = useUser()
// Don't nudge trial users
if (user.plan === 'trial') return null
// Don't nudge during onboarding
if (user.onboardingComplete === false) return null
// Don't nudge if user is busy
if (user.isEditing) return null
return <NudgeDisplay features={pendingNudges} />
}Performance
The hook efficiently manages nudge state:
// ✓ Efficient: Only re-renders when pendingNudges changes
function OptimizedNudge() {
const { pendingNudges } = useNudge()
return <NudgeList nudges={pendingNudges} />
}
// ✓ Also efficient: Actions are stable callbacks
function StableActions() {
const { dismissNudge, snoozeNudge } = useNudge()
// These callbacks won't cause re-renders
return (
<NudgeActions
onDismiss={dismissNudge}
onSnooze={snoozeNudge}
/>
)
}TypeScript
Fully typed:
import { useNudge, type UseNudgeReturn, type Feature } from '@tour-kit/adoption'
function TypedNudge() {
const nudge: UseNudgeReturn = useNudge()
nudge.pendingNudges.forEach((feature: Feature) => {
console.log(feature.name)
})
nudge.dismissNudge('feature-id') // ✓ Type-safe
nudge.snoozeNudge('feature-id', 3600000) // ✓ Type-safe
}Accessibility
Best practices for accessible nudges:
function AccessibleNudge() {
const { pendingNudges, dismissNudge } = useNudge()
if (!pendingNudges[0]) return null
const feature = pendingNudges[0]
return (
<div
role="region"
aria-label="Feature suggestion"
aria-live="polite"
>
<h4 id="nudge-title">Try {feature.name}</h4>
<p id="nudge-desc">{feature.description}</p>
<div role="group" aria-labelledby="nudge-title">
<button
onClick={() => handleNudgeClick(feature.id)}
aria-describedby="nudge-desc"
>
Try Now
</button>
<button
onClick={() => dismissNudge(feature.id)}
aria-label={`Dismiss ${feature.name} suggestion`}
>
Dismiss
</button>
</div>
</div>
)
}Use aria-live="polite" for nudges to avoid interrupting screen reader users.
Common Patterns
Nudge Counter
Show how many nudges are pending:
function NudgeCounter() {
const { hasNudges, pendingNudges } = useNudge()
if (!hasNudges) return null
return (
<button className="nudge-indicator">
New Features
<span className="badge">{pendingNudges.length}</span>
</button>
)
}Dismiss All
Allow dismissing all nudges at once:
function DismissAllButton() {
const { pendingNudges, dismissNudge } = useNudge()
const handleDismissAll = () => {
pendingNudges.forEach((feature) => {
dismissNudge(feature.id)
})
}
if (pendingNudges.length === 0) return null
return (
<button onClick={handleDismissAll}>
Dismiss all ({pendingNudges.length})
</button>
)
}Nudge History
Track which nudges were dismissed:
function NudgeHistory() {
const [dismissed, setDismissed] = useState<string[]>([])
const { dismissNudge } = useNudge()
const handleDismiss = (featureId: string) => {
dismissNudge(featureId)
setDismissed((prev) => [...prev, featureId])
}
return (
<div>
<p>Dismissed: {dismissed.length} features</p>
{/* Your nudge UI */}
</div>
)
}Best Practices
- Limit nudge frequency to avoid overwhelming users:
nudge: {
cooldown: 86400000, // 24 hours minimum
maxPerSession: 2,
maxFeatures: 1,
}-
Respect user dismissals - don't re-show dismissed nudges
-
Provide context - explain why the feature is useful
-
Make dismissal easy - always provide a clear way to close nudges
-
Test nudge timing - ensure nudges appear at appropriate moments
Next Steps
- AdoptionNudge Component - Pre-built nudge UI
- Analytics Integration - Track nudge events
- AdoptionProvider - Configure nudge behavior