Skip to content

Custom UI with Render Props

Build completely custom step interfaces using React's render props pattern.

Default Render Prop

The ROnboardingWrapper provides a render prop with full access to step data and navigation:

tsx
<ROnboardingWrapper ref={wrapperRef} steps={steps}>
  {({ step, next, previous, exit, isFirst, isLast, index }) => (
    {/* Your custom UI here */}
  )}
</ROnboardingWrapper>

Render Props

PropTypeDescription
stepStepCurrent step configuration
next() => voidGo to next step (or finish if last)
previous() => voidGo to previous step
exit() => voidExit the tour
isFirstbooleanIs this the first step?
isLastbooleanIs this the last step?
indexnumberCurrent step index (0-based)

Using ROnboardingStep

Important

When using custom render content, wrap it with ROnboardingStep to maintain proper positioning and overlay functionality.

tsx
<ROnboardingWrapper ref={wrapperRef} steps={steps}>
  {({ step, next, previous, exit, isFirst, isLast, index }) => (
    <ROnboardingStep>
      {/* Custom content goes inside ROnboardingStep */}
      <div className="my-custom-tooltip">
        <h3>{step.content?.title}</h3>
        <p>{step.content?.description}</p>
        <button onClick={next}>
          {isLast ? 'Finish' : 'Next'}
        </button>
      </div>
    </ROnboardingStep>
  )}
</ROnboardingWrapper>

Conditional Custom UI

Show custom UI for specific steps only:

tsx
<ROnboardingWrapper ref={wrapperRef} steps={steps}>
  {({ step, next, previous, exit, isFirst, isLast, index }) => (
    <>
      {/* Custom UI for step 3 */}
      {index === 2 ? (
        <ROnboardingStep>
          <MySpecialTooltip
            title={step.content?.title}
            onNext={next}
            onClose={exit}
          />
        </ROnboardingStep>
      ) : (
        /* Default UI for other steps */
        <ROnboardingStep />
      )}
    </>
  )}
</ROnboardingWrapper>

Complete Custom Example

Here's a full example with a completely custom design:

tsx
import { useRef } from 'react'
import { ROnboardingWrapper, ROnboardingStep, useROnboarding } from 'r-onboarding'

const steps = [
  {
    attachTo: { element: '#feature-1' },
    content: { title: 'Welcome!', description: 'Let us show you around.' }
  },
  {
    attachTo: { element: '#feature-2' },
    content: { title: 'Features', description: 'Here are key features.' }
  },
  {
    attachTo: { element: '#feature-3' },
    content: { title: 'Get Started', description: 'Ready to begin?' }
  }
]

function App() {
  const wrapperRef = useRef(null)
  const { start } = useROnboarding(wrapperRef)

  return (
    <>
      <ROnboardingWrapper ref={wrapperRef} steps={steps}>
        {({ step, next, previous, exit, isFirst, isLast, index }) => (
          <ROnboardingStep>
            <div className="custom-tooltip">
              {/* Header */}
              <div className="tooltip-header">
                <span className="step-badge">Step {index + 1}</span>
                <button onClick={exit} className="close-btn">×</button>
              </div>

              {/* Content */}
              <h3 className="tooltip-title">{step.content?.title}</h3>
              <p className="tooltip-description">{step.content?.description}</p>

              {/* Progress */}
              <div className="progress-dots">
                {steps.map((_, i) => (
                  <span key={i} className={`dot ${i === index ? 'active' : ''}`} />
                ))}
              </div>

              {/* Actions */}
              <div className="tooltip-actions">
                {!isFirst && (
                  <button onClick={previous} className="btn-secondary">
                    Back
                  </button>
                )}
                <button onClick={next} className="btn-primary">
                  {isLast ? 'Done' : 'Continue'}
                </button>
              </div>
            </div>
          </ROnboardingStep>
        )}
      </ROnboardingWrapper>

      <div id="feature-1">Feature 1</div>
      <div id="feature-2">Feature 2</div>
      <div id="feature-3">Feature 3</div>
      <button onClick={start}>Start Tour</button>
    </>
  )
}
css
.custom-tooltip {
  background: white;
  border-radius: 12px;
  padding: 24px;
  max-width: 320px;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}

.tooltip-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}

.step-badge {
  background: #d4ff00;
  color: #000;
  padding: 4px 12px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 600;
}

.close-btn {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  color: #999;
}

.tooltip-title {
  font-size: 18px;
  font-weight: 600;
  margin-bottom: 8px;
}

.tooltip-description {
  color: #666;
  font-size: 14px;
  line-height: 1.5;
}

.progress-dots {
  display: flex;
  gap: 6px;
  margin: 16px 0;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #ddd;
}

.dot.active {
  background: #d4ff00;
}

.tooltip-actions {
  display: flex;
  gap: 12px;
  margin-top: 20px;
}

.btn-primary {
  flex: 1;
  padding: 10px 20px;
  background: #d4ff00;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
}

.btn-secondary {
  padding: 10px 20px;
  background: transparent;
  border: 1px solid #ddd;
  border-radius: 8px;
  cursor: pointer;
}

Hiding the Arrow

For custom designs that don't need the pointer arrow:

css
/* Hide arrow for custom tooltip */
.custom-tooltip ~ [data-popper-arrow] {
  display: none;
}

Or style it to match your design:

css
.custom-tooltip ~ [data-popper-arrow]::before {
  background: white !important;
  border: none !important;
}

Released under the MIT License.