AdoptionNudge
AdoptionNudge component: auto-showing prompt that appears when a user has not adopted a tracked feature after a threshold
Overview
AdoptionNudge is a component that automatically displays nudges for unadopted features based on the nudge configuration in AdoptionProvider. It handles scheduling, display, and user interactions out of the box.
Basic Usage
import { AdoptionNudge } from '@tour-kit/adoption'
export default function App() {
return (
<div className="app">
{/* Your app content */}
<AdoptionNudge />
</div>
)
}That's it! The component will automatically:
- Wait for the configured delay
- Show the highest-priority pending nudge
- Track when nudges are shown
- Handle dismissal and clicks
Props
Prop
Type
Examples
Default Nudge
The default styling provides a clean, shadcn/ui-style nudge:
<AdoptionNudge />Renders a card with:
- Feature name as heading
- Feature description (if provided)
- "Try it" button (tracks usage and dismisses)
- "Dismiss" button (dismisses permanently)
Custom Position
Control where the nudge appears:
<AdoptionNudge position="top-right" />
<AdoptionNudge position="bottom-left" />Custom Delay
Show nudges immediately or with custom timing:
{/* Show immediately */}
<AdoptionNudge delay={0} />
{/* Wait 10 seconds */}
<AdoptionNudge delay={10000} />Custom Render
Take full control of the UI:
<AdoptionNudge
render={({ feature, onClick, onDismiss, onSnooze }) => (
<div className="custom-nudge">
<h3>{feature.name}</h3>
<p>{feature.description}</p>
<div className="actions">
<button onClick={onClick}>Try Now</button>
<button onClick={() => onSnooze(3600000)}>Remind me in 1h</button>
<button onClick={onDismiss}>Not interested</button>
</div>
</div>
)}
/>Prop
Type
With Snooze Options
Add snooze functionality to default UI:
<AdoptionNudge
render={({ feature, onClick, onDismiss, onSnooze }) => (
<div className="nudge-card">
<h4>{feature.name}</h4>
<p>{feature.description}</p>
<div className="actions">
<button onClick={onClick}>Try it</button>
<details>
<summary>Remind me later</summary>
<button onClick={() => onSnooze(3600000)}>1 hour</button>
<button onClick={() => onSnooze(86400000)}>Tomorrow</button>
<button onClick={() => onSnooze(604800000)}>Next week</button>
</details>
<button onClick={onDismiss}>Dismiss</button>
</div>
</div>
)}
/>Animated Nudge
Add entrance/exit animations:
import { useState, useEffect } from 'react'
function AnimatedNudge() {
const [isVisible, setIsVisible] = useState(false)
return (
<AdoptionNudge
render={({ feature, onClick, onDismiss }) => {
useEffect(() => {
// Animate in
setIsVisible(true)
}, [])
const handleDismiss = () => {
setIsVisible(false)
setTimeout(onDismiss, 300) // Wait for animation
}
return (
<div className={`nudge-toast ${isVisible ? 'visible' : ''}`}>
<h4>{feature.name}</h4>
<button onClick={onClick}>Try</button>
<button onClick={handleDismiss}>✕</button>
</div>
)
}}
/>
)
}.nudge-toast {
transform: translateY(100px);
opacity: 0;
transition: all 0.3s ease;
}
.nudge-toast.visible {
transform: translateY(0);
opacity: 1;
}With Tour Integration
Launch a tour when user clicks the nudge:
import { useTour } from '@tour-kit/react'
function NudgeWithTour() {
const { startTour } = useTour()
return (
<AdoptionNudge
render={({ feature, onClick, onDismiss }) => {
const handleTryIt = () => {
onClick()
if (feature.resources?.tourId) {
startTour(feature.resources.tourId)
}
}
return (
<div className="nudge">
<h4>{feature.name}</h4>
<button onClick={handleTryIt}>
{feature.resources?.tourId ? 'Take Tour' : 'Try Now'}
</button>
<button onClick={onDismiss}>Dismiss</button>
</div>
)
}}
/>
)
}Rich Content Nudge
Show images, videos, or rich formatting:
<AdoptionNudge
render={({ feature, onClick, onDismiss }) => (
<div className="rich-nudge">
{feature.category === 'video' && (
<video
src={`/features/${feature.id}.mp4`}
autoPlay
loop
muted
className="nudge-video"
/>
)}
<div className="content">
<h4>{feature.name}</h4>
<p>{feature.description}</p>
{feature.premium && (
<span className="badge-premium">Premium</span>
)}
</div>
<div className="actions">
<button onClick={onClick}>Try it free</button>
<button onClick={onDismiss}>Dismiss</button>
</div>
</div>
)}
/>Category-Specific Styling
Style nudges differently based on feature category:
<AdoptionNudge
render={({ feature, onClick, onDismiss }) => (
<div className={`nudge nudge-${feature.category || 'default'}`}>
<div className="nudge-icon">
{getCategoryIcon(feature.category)}
</div>
<div className="nudge-content">
<h4>{feature.name}</h4>
<p>{feature.description}</p>
</div>
<div className="nudge-actions">
<button onClick={onClick}>Try</button>
<button onClick={onDismiss}>✕</button>
</div>
</div>
)}
/>Progress Indicator
Show how many uses until adoption:
import { useFeature } from '@tour-kit/adoption'
<AdoptionNudge
render={({ feature, onClick, onDismiss }) => {
const { usage } = useFeature(feature.id)
const criteria = feature.adoptionCriteria || { minUses: 3 }
const progress = (usage.useCount / criteria.minUses) * 100
return (
<div className="nudge">
<h4>{feature.name}</h4>
<p>{feature.description}</p>
<div className="progress">
<div className="progress-bar" style={{ width: `${progress}%` }} />
<span>{usage.useCount} / {criteria.minUses} uses</span>
</div>
<button onClick={onClick}>Try Now</button>
<button onClick={onDismiss}>Dismiss</button>
</div>
)
}}
/>Styling
Variant Styling
Use built-in size and position variants:
<AdoptionNudge size="sm" position="top-right" />
<AdoptionNudge size="lg" position="bottom-left" />Custom Classes
Extend default styling:
<AdoptionNudge className="shadow-xl border-2 border-primary" />CSS Variables
Customize via CSS variables:
.adoption-nudge {
--nudge-bg: hsl(0 0% 100%);
--nudge-border: hsl(0 0% 90%);
--nudge-text: hsl(0 0% 10%);
--nudge-radius: 0.5rem;
}Using asChild
Compose with your own elements:
import { Card } from '@/components/ui/card'
<AdoptionNudge asChild>
<Card className="nudge-card">
{/* Default nudge content rendered inside Card */}
</Card>
</AdoptionNudge>The asChild pattern follows Radix UI conventions, merging props into the child element.
Behavior
Automatic Scheduling
The component respects nudge configuration:
<AdoptionProvider
features={features}
nudge={{
enabled: true,
initialDelay: 5000, // Component waits this + its own delay
cooldown: 86400000, // Won't show nudges more frequently
maxPerSession: 3, // Max nudges this session
maxFeatures: 1, // One feature nudge at a time
}}
>
<App />
<AdoptionNudge delay={2000} />
</AdoptionProvider>Total delay = nudge.initialDelay + AdoptionNudge.delay
Priority Ordering
Shows highest-priority feature first:
const features = [
{ id: 'basic', name: 'Basic', trigger: '#basic', priority: 1 },
{ id: 'important', name: 'Important', trigger: '#important', priority: 10 },
{ id: 'critical', name: 'Critical', trigger: '#critical', priority: 100 },
]
// <AdoptionNudge /> will show "Critical" firstDismissal
When dismissed:
- Nudge disappears
- Feature marked as permanently dismissed
- Won't appear in future sessions
- Analytics event fired (if configured)
Click Behavior
When "Try it" is clicked:
- Feature usage is tracked
- Nudge is dismissed
onClickhandler runs (if in custom render)- Analytics events fired
Accessibility
The default nudge includes:
- Semantic HTML (
divwith proper heading hierarchy) - Focus management (buttons are keyboard accessible)
- Clear dismiss action
- No motion if
prefers-reduced-motionis set
Screen Reader Support
Add ARIA attributes in custom renders:
<AdoptionNudge
render={({ feature, onClick, onDismiss }) => (
<div
role="region"
aria-label="Feature suggestion"
aria-live="polite"
>
<h4 id="nudge-heading">{feature.name}</h4>
<p id="nudge-desc">{feature.description}</p>
<div role="group" aria-labelledby="nudge-heading">
<button
onClick={onClick}
aria-describedby="nudge-desc"
>
Try Now
</button>
<button
onClick={onDismiss}
aria-label={`Dismiss ${feature.name} suggestion`}
>
Dismiss
</button>
</div>
</div>
)}
/>Use aria-live="polite" to announce nudges to screen readers without interrupting.
TypeScript
Fully typed render props:
import { AdoptionNudge, type NudgeRenderProps, type Feature } from '@tour-kit/adoption'
<AdoptionNudge
render={(props: NudgeRenderProps) => {
const { feature, onClick, onDismiss, onSnooze } = props
// All typed correctly
feature.name // string
feature.description // string | undefined
onClick() // () => void
onSnooze(3600000) // (durationMs: number) => void
return <div>...</div>
}}
/>Performance
The component efficiently manages state:
- Only re-renders when nudge state changes
- Automatically cleans up timers
- Memoized callbacks prevent unnecessary re-renders
For optimal performance, define render functions outside the component:
const renderNudge = ({ feature, onClick, onDismiss }: NudgeRenderProps) => (
<div>
<h4>{feature.name}</h4>
<button onClick={onClick}>Try</button>
<button onClick={onDismiss}>Dismiss</button>
</div>
)
function App() {
return <AdoptionNudge render={renderNudge} />
}Common Patterns
Multiple Nudge Instances
Show different nudges in different locations:
function App() {
return (
<div>
<AdoptionNudge position="top-right" delay={5000} />
<AdoptionNudge position="bottom-left" delay={30000} />
</div>
)
}Multiple instances will show the same nudge. Use maxFeatures config to control how many features are nudged at once.
Conditional Display
Only show nudges in certain contexts:
function ConditionalNudge() {
const { user } = useAuth()
// Don't nudge trial users
if (user.plan === 'trial') return null
// Don't nudge during onboarding
if (!user.onboardingComplete) return null
return <AdoptionNudge />
}Persistent Nudges
Keep nudge visible until explicitly dismissed:
<AdoptionNudge
render={({ feature, onClick, onDismiss }) => (
<div className="sticky-nudge">
<p>Don't forget to try {feature.name}!</p>
<button onClick={onClick}>Try</button>
<button onClick={onDismiss}>Dismiss</button>
</div>
)}
/>Best Practices
- Position strategically - Don't block critical UI:
// Good: Bottom corner, out of the way
<AdoptionNudge position="bottom-right" />
// Bad: Could block important content
<div className="fixed top-0 left-0 w-full">
<AdoptionNudge />
</div>- Provide context in feature descriptions:
const features = [
{
id: 'shortcuts',
name: 'Keyboard Shortcuts',
description: 'Work faster with keyboard shortcuts', // Explains value
trigger: '#shortcuts',
},
]- Test delays with real usage patterns
- Make dismissal obvious - always provide a clear close button
- Respect user decisions - dismissed means dismissed
Next Steps
- useNudge Hook - Manual nudge control
- FeatureButton Component - Button with tracking
- Analytics Integration - Track nudge interactions