diff --git a/README.md b/README.md
index a96d1dc..5f52c52 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,18 @@ const App = () => {
options: {
highLightPadding: 12,
infoBoxHeight: 220,
+ infoBoxWidth: '24rem',
infoBoxMargin: 24,
+ overlayColor: 'rgba(15, 23, 42, 0.6)',
+ highlightBorderColor: '#22c55e',
+ highlightBorderRadius: 16,
+ zIndex: 3000,
+ labels: {
+ prev: 'Back',
+ next: 'Continue',
+ skip: 'Dismiss',
+ done: 'Finish',
+ },
keyboardNavigation: true,
closeOnOverlayClick: true,
onClose: () => {
@@ -87,6 +98,10 @@ const App = () => {
`highLightPadding` expands the highlight frame around the target element. It defaults to `8` pixels and applies to the rendered highlight box as well as the info box anchor position.
+Use `overlayColor`, `highlightBorderColor`, `highlightBorderRadius`, `zIndex`, and `infoBoxWidth` to match the built-in UI to your product without changing the overlay, highlight, or info box structure. When omitted, the existing defaults remain in place.
+
+Use `labels` to override the built-in button text. The default labels are `이전`, `다음`, `건너뛰기`, and `완료`.
+
Keyboard navigation is enabled by default while the overlay is open:
- `Escape` closes the tutorial.
diff --git a/packages/document/src/pages/docs/tutorial-overlay.mdx b/packages/document/src/pages/docs/tutorial-overlay.mdx
index 68aa0c7..059aaf2 100644
--- a/packages/document/src/pages/docs/tutorial-overlay.mdx
+++ b/packages/document/src/pages/docs/tutorial-overlay.mdx
@@ -28,6 +28,10 @@ Because `tutorial.open()` returns a Promise, you can keep ``
The highlight frame uses `options.highLightPadding` to expand around the target. If you do not provide it, the overlay uses an `8px` padding by default.
+Visual customization stays global for now. Use `options.overlayColor`, `options.highlightBorderColor`, `options.highlightBorderRadius`, `options.zIndex`, and `options.infoBoxWidth` when you need the built-in overlay UI to better match your product chrome.
+
+Use `options.labels` to replace the built-in `이전`, `다음`, `건너뛰기`, and `완료` button text without replacing the UI itself.
+
By default, the mounted overlay listens for `Escape`, `ArrowLeft`, and `ArrowRight` while it is open. You can disable that with `options.keyboardNavigation = false`.
Backdrop clicks are ignored by default. Set `options.closeOnOverlayClick = true` if you want clicking the dimmed overlay area to close the tutorial.
diff --git a/packages/document/src/pages/docs/tutorial.mdx b/packages/document/src/pages/docs/tutorial.mdx
index 8a985e5..3ea025e 100644
--- a/packages/document/src/pages/docs/tutorial.mdx
+++ b/packages/document/src/pages/docs/tutorial.mdx
@@ -29,7 +29,18 @@ function App() {
options: {
highLightPadding: 12,
infoBoxHeight: 220,
+ infoBoxWidth: '24rem',
infoBoxMargin: 24,
+ overlayColor: 'rgba(15, 23, 42, 0.6)',
+ highlightBorderColor: '#22c55e',
+ highlightBorderRadius: 16,
+ zIndex: 3000,
+ labels: {
+ prev: 'Back',
+ next: 'Continue',
+ skip: 'Dismiss',
+ done: 'Finish',
+ },
keyboardNavigation: true,
closeOnOverlayClick: true,
},
@@ -76,7 +87,13 @@ function App() {
- `highLightPadding`: expands the highlight frame around the target in pixels. Defaults to `8`.
- `infoBoxHeight`: sets the info box height in pixels.
+- `infoBoxWidth`: sets the info box width with a CSS length such as `360` or `'24rem'`. Defaults to `'20rem'`.
- `infoBoxMargin`: controls the vertical gap between the target and the info box.
+- `overlayColor`: sets the backdrop color. Defaults to `rgba(0, 0, 0, 0.5)`.
+- `highlightBorderColor`: sets the highlight border color. Defaults to `#ff0000`.
+- `highlightBorderRadius`: sets the highlight border radius with a CSS length. Defaults to the current padding-based radius.
+- `zIndex`: sets the overlay stack level. Defaults to `9999`.
+- `labels`: overrides the built-in `prev`, `next`, `skip`, and `done` button labels.
- `keyboardNavigation`: enables `Escape`, `ArrowLeft`, and `ArrowRight` shortcuts while the overlay is open. Defaults to `true`.
- `closeOnOverlayClick`: closes the tutorial when the backdrop itself is clicked. Defaults to `false`.
- `onClose`: runs when the tutorial is closed.
diff --git a/packages/main/src/components/content.tsx b/packages/main/src/components/content.tsx
index a47306a..d232618 100644
--- a/packages/main/src/components/content.tsx
+++ b/packages/main/src/components/content.tsx
@@ -2,15 +2,17 @@ import React, { useId } from 'react';
import { useTutorialStore } from '../core/store';
import { skipTutorial, tutorial } from '../core/tutorial';
import { styled } from 'goober';
+import { DEFAULT_INFO_BOX_WIDTH, INFO_BOX_Z_INDEX_OFFSET, getBaseZIndex, getLabels } from '../core/options';
export const Content = React.forwardRef((_, ref) => {
const {
index,
- tutorial: { steps },
+ tutorial: { steps, options },
} = useTutorialStore();
const currentStep = steps[index];
const titleId = useId();
const contentId = useId();
+ const labels = getLabels(options);
const handlePrev = () => {
tutorial.prev();
@@ -31,11 +33,15 @@ export const Content = React.forwardRef((_, ref) => {
aria-labelledby={titleId}
aria-describedby={currentStep.content ? contentId : undefined}
tabIndex={-1}
+ style={{
+ width: options?.infoBoxWidth ?? DEFAULT_INFO_BOX_WIDTH,
+ zIndex: getBaseZIndex(options) + INFO_BOX_Z_INDEX_OFFSET,
+ }}
>
{currentStep.title}
-
+
{currentStep.content ?? ''}
@@ -44,8 +50,8 @@ export const Content = React.forwardRef((_, ref) => {
{`${index + 1} / ${steps.length}`}
- {index !== 0 && }
-
+ {index !== 0 && }
+
@@ -56,7 +62,7 @@ Content.displayName = 'Content';
const Wrapper = styled('div', React.forwardRef)`
position: absolute;
top: 6.25rem;
- z-index: 999;
+ z-index: 10001;
width: 20rem;
min-height: 7.5rem;
display: flex;
diff --git a/packages/main/src/components/tutorial-overlay.tsx b/packages/main/src/components/tutorial-overlay.tsx
index 81b117b..61bceee 100644
--- a/packages/main/src/components/tutorial-overlay.tsx
+++ b/packages/main/src/components/tutorial-overlay.tsx
@@ -4,10 +4,17 @@ import { useTutorialStore } from '../core/store';
import { setup, styled } from 'goober';
import { Content } from './content';
import { tutorial } from '../core/tutorial';
+import {
+ DEFAULT_HIGHLIGHT_BORDER_COLOR,
+ DEFAULT_HIGHLIGHT_PADDING,
+ DEFAULT_OVERLAY_COLOR,
+ DEFAULT_Z_INDEX,
+ HIGHLIGHT_Z_INDEX_OFFSET,
+ getBaseZIndex,
+} from '../core/options';
setup(React.createElement);
-const DEFAULT_HIGHLIGHT_PADDING = 8;
const MIN_VIEWPORT_OFFSET = 10;
const FOCUSABLE_SELECTOR = [
'button:not([disabled])',
@@ -34,6 +41,7 @@ export const TutorialOverlay = React.memo(({}: TutorialOverlayProps) => {
const previouslyFocusedElement = useRef(null);
const wasOpen = useRef(open);
const timeout = useRef();
+ const baseZIndex = getBaseZIndex(options);
function shouldIgnoreKeyboardEvent(): boolean {
const activeElement = document.activeElement;
@@ -284,14 +292,26 @@ export const TutorialOverlay = React.memo(({}: TutorialOverlayProps) => {
};
return open ? (
-
+
{rectStyles.map((style) => (
))}
@@ -302,16 +322,16 @@ const Wrapper = styled('div')`
position: fixed;
top: 0;
left: 0;
- z-index: 9999;
+ z-index: ${DEFAULT_Z_INDEX};
height: 100vh;
width: 100vw;
- background-color: rgba(0, 0, 0, 0.5);
+ background-color: ${DEFAULT_OVERLAY_COLOR};
`;
const Hightlight = styled('div')`
position: absolute;
- z-index: 9999;
+ z-index: ${DEFAULT_Z_INDEX + HIGHLIGHT_Z_INDEX_OFFSET};
box-sizing: border-box;
- border: 2px solid #ff0000;
+ border: 2px solid ${DEFAULT_HIGHLIGHT_BORDER_COLOR};
border-radius: 0.625rem;
`;
diff --git a/packages/main/src/core/options.ts b/packages/main/src/core/options.ts
new file mode 100644
index 0000000..8382276
--- /dev/null
+++ b/packages/main/src/core/options.ts
@@ -0,0 +1,27 @@
+import type { Labels, Options } from './types';
+
+export const DEFAULT_HIGHLIGHT_PADDING = 8;
+export const DEFAULT_OVERLAY_COLOR = 'rgba(0, 0, 0, 0.5)';
+export const DEFAULT_HIGHLIGHT_BORDER_COLOR = '#ff0000';
+export const DEFAULT_INFO_BOX_WIDTH = '20rem';
+export const DEFAULT_Z_INDEX = 9999;
+export const HIGHLIGHT_Z_INDEX_OFFSET = 1;
+export const INFO_BOX_Z_INDEX_OFFSET = 2;
+
+export const DEFAULT_LABELS: Required = {
+ prev: '이전',
+ next: '다음',
+ skip: '건너뛰기',
+ done: '완료',
+};
+
+export function getLabels(options?: Options): Required {
+ return {
+ ...DEFAULT_LABELS,
+ ...options?.labels,
+ };
+}
+
+export function getBaseZIndex(options?: Options): number {
+ return options?.zIndex ?? DEFAULT_Z_INDEX;
+}
diff --git a/packages/main/src/core/types.ts b/packages/main/src/core/types.ts
index 1eb0b64..e3198d8 100644
--- a/packages/main/src/core/types.ts
+++ b/packages/main/src/core/types.ts
@@ -1,3 +1,5 @@
+export type StyleValue = number | string;
+
export interface Step {
targetIds: string[];
content?: string;
@@ -7,10 +9,23 @@ export interface Step {
onNextStep?: () => void;
}
+export interface Labels {
+ prev?: string;
+ next?: string;
+ skip?: string;
+ done?: string;
+}
+
export interface Options {
highLightPadding?: number;
infoBoxHeight?: number;
+ infoBoxWidth?: StyleValue;
infoBoxMargin?: number;
+ overlayColor?: string;
+ highlightBorderColor?: string;
+ highlightBorderRadius?: StyleValue;
+ zIndex?: number;
+ labels?: Labels;
keyboardNavigation?: boolean;
closeOnOverlayClick?: boolean;
onClose?: () => void;
@@ -33,5 +48,5 @@ export interface ElementStyle {
top: number;
width: number;
height: number;
- borderRadius?: number;
+ borderRadius?: StyleValue;
}
diff --git a/packages/main/test/content.test.tsx b/packages/main/test/content.test.tsx
index c8ff211..359f8b3 100644
--- a/packages/main/test/content.test.tsx
+++ b/packages/main/test/content.test.tsx
@@ -14,6 +14,35 @@ function renderOverlay() {
}
describe('Content', () => {
+ test('reads built-in button labels from options.labels', () => {
+ renderOverlay();
+
+ act(() => {
+ tutorial.open({
+ steps: [
+ { title: 'Step 1', content: 'Step 1 content', targetIds: ['first-target'] },
+ { title: 'Step 2', content: 'Step 2 content', targetIds: ['second-target'] },
+ ],
+ options: {
+ labels: {
+ prev: 'Back',
+ next: 'Continue',
+ skip: 'Dismiss',
+ done: 'Finish',
+ },
+ },
+ });
+ });
+
+ expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
+
+ expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Finish' })).toBeInTheDocument();
+ });
+
test('button navigation invokes each step callback once', () => {
const onNextStep = jest.fn();
const onPrevStep = jest.fn();
diff --git a/packages/main/test/tutorial-overlay.test.tsx b/packages/main/test/tutorial-overlay.test.tsx
index a4e2f81..928a5ee 100644
--- a/packages/main/test/tutorial-overlay.test.tsx
+++ b/packages/main/test/tutorial-overlay.test.tsx
@@ -345,4 +345,31 @@ describe('TutorialOverlay', () => {
jest.useRealTimers();
});
+
+ test('applies custom overlay, highlight, and info box styles from options', () => {
+ renderOverlay();
+ mockTargetRect('first-target', { left: 120, top: 96, width: 140, height: 48 });
+
+ openTutorial({
+ overlayColor: 'rgba(12, 34, 56, 0.7)',
+ highlightBorderColor: 'rgb(0, 255, 136)',
+ highlightBorderRadius: 24,
+ zIndex: 4321,
+ infoBoxWidth: '28rem',
+ });
+
+ expect(screen.getByTestId('tutorial-overlay-backdrop')).toHaveStyle({
+ backgroundColor: 'rgba(12, 34, 56, 0.7)',
+ zIndex: '4321',
+ });
+ expect(screen.getByRole('dialog', { name: 'Step 1' })).toHaveStyle({
+ width: '28rem',
+ zIndex: '4323',
+ });
+ expect(screen.getByTestId('tutorial-overlay-highlight-first-target')).toHaveStyle({
+ borderColor: 'rgb(0, 255, 136)',
+ borderRadius: '24px',
+ zIndex: '4322',
+ });
+ });
});