Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/dialog-drawer-pointer-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@zag-js/dialog": patch
"@zag-js/drawer": patch
---

Avoid setting inline `pointer-events` when modal, letting the dismissable layer manage it instead.
6 changes: 0 additions & 6 deletions .changeset/file-upload-duplicate.md

This file was deleted.

9 changes: 0 additions & 9 deletions .changeset/toast-region-role.md

This file was deleted.

20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.

> For v0.x changelog, see the [v0 branch](https://github.com/chakra-ui/zag/blob/v0/CHANGELOG.md)

## [1.38.1](./#1.38.1) - 2026-03-25

### Fixed

- **File Upload**: Automatically reject duplicate files with `FILE_EXISTS` error. Previously, uploading the same file
twice was silently accepted and deleting one duplicate removed all of them.

- **Toast**: Restore `role="region"` on the toast group element. The role was previously removed to reduce screen reader
landmark noise, but this caused an axe `aria-prohibited-attr` violation since `aria-label` is not permitted on a `div`
without a valid role.

- **Tour**
- Fix step navigation events (`next`, `prev`, `setStep`) firing when the tour is inactive, bypassing the `start` flow
- Fix popper styles not being cleaned up when transitioning from a tooltip step to a dialog/non-tooltip step

### Changed

- **Core**: Validate compound states at machine creation — throw if a state has child states but no `initial`, or if
`initial` references a nonexistent child.

## [1.38.0](./#1.38.0) - 2026-03-24

### Added
Expand Down
140 changes: 140 additions & 0 deletions examples/next-ts/pages/tour/conditional.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Portal, normalizeProps, useMachine } from "@zag-js/react"
import * as tour from "@zag-js/tour"
import { X } from "lucide-react"
import { useId } from "react"
import { StateVisualizer } from "../../components/state-visualizer"
import { Toolbar } from "../../components/toolbar"

const steps: tour.StepDetails[] = [
{
id: "choose",
type: "dialog",
title: "Choose Your Path",
description: "Are you new here or a returning user?",
actions: [
{
label: "I'm new",
action: (actionMap) => actionMap.goto("beginner"),
},
{
label: "I know the basics",
action: (actionMap) => actionMap.goto("advanced"),
},
],
},
{
id: "beginner",
type: "tooltip",
target: () => document.querySelector<HTMLElement>("#option-beginner"),
title: "Welcome!",
description: "We'll start with the fundamentals to get you up and running.",
actions: [{ label: "Next", action: "next" }],
},
{
id: "basics",
type: "tooltip",
target: () => document.querySelector<HTMLElement>("#feature-basics"),
title: "Getting Started",
description: "Create your first project and invite your team.",
actions: [
{ label: "Prev", action: "prev" },
{ label: "Done", action: "dismiss" },
],
},
{
id: "advanced",
type: "tooltip",
target: () => document.querySelector<HTMLElement>("#option-advanced"),
title: "Welcome Back!",
description: "Let's skip the basics and show you what's new.",
actions: [{ label: "Next", action: "next" }],
},
{
id: "power",
type: "tooltip",
target: () => document.querySelector<HTMLElement>("#feature-power"),
title: "Power Features",
description: "Set up automations, API keys, and custom workflows.",
actions: [
{ label: "Prev", action: "prev" },
{ label: "Done", action: "dismiss" },
],
},
]

export default function Page() {
const service = useMachine(tour.machine, {
id: useId(),
steps,
})

const api = tour.connect(service, normalizeProps)

return (
<>
<main className="tour">
<div>
<button onClick={() => api.start()}>Start Tour</button>
<div className="steps__container">
<div id="option-beginner" className="tour-card">
<h4>Beginner</h4>
<p>New to the platform? Start here.</p>
</div>
<div id="option-advanced" className="tour-card">
<h4>Advanced</h4>
<p>Already familiar? Jump to power features.</p>
</div>
<div style={{ display: "flex", gap: "1rem" }}>
<div id="feature-basics" className="tour-card">
<h4>Getting Started</h4>
<p>Create your first project.</p>
</div>
<div id="feature-power" className="tour-card">
<h4>Power Features</h4>
<p>Automations and API access.</p>
</div>
</div>
</div>
</div>

{api.step && api.open && (
<Portal>
{api.step.backdrop && <div {...api.getBackdropProps()} />}
<div {...api.getSpotlightProps()} />
<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>
{api.step.arrow && (
<div {...api.getArrowProps()}>
<div {...api.getArrowTipProps()} />
</div>
)}

<p {...api.getTitleProps()}>{api.step.title}</p>
<div {...api.getDescriptionProps()}>{api.step.description}</div>
<div {...api.getProgressTextProps()}>{api.getProgressText()}</div>

{api.step.actions && (
<div className="tour button__group">
{api.step.actions.map((action) => (
<button key={action.label} {...api.getActionTriggerProps({ action })}>
{action.label}
</button>
))}
</div>
)}

<button {...api.getCloseTriggerProps()}>
<X />
</button>
</div>
</div>
</Portal>
)}
</main>

<Toolbar viz>
<StateVisualizer state={service} omit={["steps"]} />
</Toolbar>
</>
)
}
124 changes: 124 additions & 0 deletions examples/next-ts/pages/tour/step-types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Portal, normalizeProps, useMachine } from "@zag-js/react"
import * as tour from "@zag-js/tour"
import { X } from "lucide-react"
import { useId } from "react"
import { StateVisualizer } from "../../components/state-visualizer"
import { Toolbar } from "../../components/toolbar"

const steps: tour.StepDetails[] = [
{
id: "dialog-intro",
type: "dialog",
title: "Dialog Step",
description: "This is a centered dialog step with no target element.",
actions: [{ label: "Next", action: "next" }],
},
{
id: "tooltip-feature",
type: "tooltip",
target: () => document.querySelector<HTMLElement>("#feature-btn"),
title: "Tooltip Step",
description: "This tooltip points to a specific element on the page.",
placement: "bottom",
actions: [
{ label: "Prev", action: "prev" },
{ label: "Next", action: "next" },
],
},
{
id: "wait-action",
type: "wait",
effect({ next }) {
const btn = document.querySelector<HTMLElement>("#continue-btn")
if (!btn) return
const handler = () => next()
btn.addEventListener("click", handler, { once: true })
return () => btn.removeEventListener("click", handler)
},
},
{
id: "tooltip-after-wait",
type: "tooltip",
target: () => document.querySelector<HTMLElement>("#continue-btn"),
title: "Back to Tooltip",
description: "After the wait step, we're back to a tooltip. Placement styles should be clean.",
placement: "top",
actions: [{ label: "Next", action: "next" }],
},
{
id: "dialog-after-tooltip",
type: "dialog",
title: "Dialog After Tooltip",
description: "Switching back to dialog. Old tooltip positioning styles should be fully cleaned up.",
actions: [{ label: "Next", action: "next" }],
},
{
id: "floating-end",
type: "floating",
title: "Floating Step",
description: "A floating step with no backdrop or arrow.",
actions: [{ label: "Done", action: "dismiss" }],
},
]

export default function Page() {
const service = useMachine(tour.machine, {
id: useId(),
steps,
})

const api = tour.connect(service, normalizeProps)

return (
<>
<main className="tour">
<div>
<button onClick={() => api.start()}>Start Tour</button>
<div className="steps__container">
<button id="feature-btn">Feature Button</button>
<button id="continue-btn">Continue</button>
{api.step?.type === "wait" && <p>Click "Continue" to proceed...</p>}
</div>
</div>

{api.step && api.open && (
<Portal>
{api.step.backdrop && <div {...api.getBackdropProps()} />}
<div {...api.getSpotlightProps()} />
<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>
{api.step.arrow && (
<div {...api.getArrowProps()}>
<div {...api.getArrowTipProps()} />
</div>
)}

<p {...api.getTitleProps()}>{api.step.title}</p>
<div {...api.getDescriptionProps()}>{api.step.description}</div>
<div {...api.getProgressTextProps()}>{api.getProgressText()}</div>

{api.step.actions && (
<div className="tour button__group">
{api.step.actions.map((action) => (
<button key={action.label} {...api.getActionTriggerProps({ action })}>
{action.label}
</button>
))}
</div>
)}

<button {...api.getCloseTriggerProps()}>
<X />
</button>
</div>
</div>
</Portal>
)}
</main>

<Toolbar viz>
<StateVisualizer state={service} omit={["steps"]} />
</Toolbar>
</>
)
}
Loading
Loading