Skip to content

GraphiQL v6#4228

Draft
trevor-scheer wants to merge 60 commits into
mainfrom
graphiql-6
Draft

GraphiQL v6#4228
trevor-scheer wants to merge 60 commits into
mainfrom
graphiql-6

Conversation

@trevor-scheer

@trevor-scheer trevor-scheer commented May 7, 2026

Copy link
Copy Markdown
Contributor

Tracking PR for the GraphiQL v6 redesign effort. This is the long-running integration branch that hosts work-in-progress against main.

Individual PRs target graphiql-6 and produce alpha releases via changesets pre-mode. When v6 is ready to ship, this branch will be merged into main.

See discussion #4219 for background and progress updates.

Closes #734 — the visual query builder ships in v6 as @graphiql/plugin-query-builder.

## Summary

- Swap the vestigial `graphiql-5` reference in
`.github/workflows/release.yml` for `graphiql-6` so the
changesets-action runs on pushes to the integration branch.
- Enter changesets pre-mode with the `alpha` tag so merges aggregate
into `6.0.0-alpha.N` prereleases.
- Add a changeset that seeds the alpha release line by bumping
`graphiql` to v6. No functional change — subsequent alphas accumulate
the redesign work.

## Test plan

- [ ] On merge: changesets-action opens a "Version Packages (alpha)" PR
bumping `graphiql` to `6.0.0-alpha.0`.
- [ ] Merging the version PR publishes `graphiql@6.0.0-alpha.0` to npm
with the `alpha` dist-tag.

Refs: #4219
@changeset-bot

changeset-bot Bot commented May 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d79f78f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
@graphiql/react Minor
graphiql Major
@graphiql/plugin-doc-explorer Major
@graphiql/plugin-history Major
@graphiql/toolkit Patch
@graphiql/plugin-query-builder Major
@graphiql/plugin-code-exporter Major
@graphiql/plugin-explorer Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

The latest changes of this PR are not available as canary, since there are no linked changesets for this PR.

## Summary

- Introduce a new `packages/graphiql-react/src/style/tokens.css` with
the v6 OKLCH-based design token system. Both dark and light palettes
ship together.
- Light theme activates explicitly via `data-theme="light"` or
automatically via `prefers-color-scheme: light` when no theme is pinned.
Dark remains the default.
- Existing v5 HSL variables are unchanged; nothing in `@graphiql/react`
references the new tokens yet.
- Future PRs will restyle components to consume the new tokens and shim
the v5 variables.

Refs: #4219
## Summary

Storybook gives us a fast feedback loop for iterating on the look of the
app and individual components — flipping themes/density/font-size
without spinning up the full GraphiQL shell.

- Bootstrap Storybook 10 in `@graphiql/react`. Stories colocated as
`<component>.stories.tsx`; ships one starter (`Spinner`) to validate the
pipeline.
- A global decorator wraps every story in `.graphiql-container` with
`data-theme` / `data-density` / `data-font-size` attributes, toggleable
from the Storybook toolbar.
- Move `Uri`, `KeyMod`, `KeyCode`, and `Range` out of the `utility`
barrel into direct imports from `utility/monaco-ssr`. The barrel was
bundling two unrelated concerns — lightweight UI helpers (`cn`, `pick`,
etc.) and heavy Monaco re-exports — so any story reaching for `cn`
transitively pulled Monaco's ESM bundle, which doesn't initialize
cleanly inside Storybook's preview iframe. Splitting them keeps UI
primitives lightweight.

## Run locally

From the repo root:

```
yarn storybook         # dev server on http://localhost:6006
yarn build-storybook   # static build under packages/graphiql-react/storybook-static
```

Refs: #4219
## Summary

Component a11y is covered by Storybook + axe; this is the full-app
counterpart. `cypress-axe` runs axe at four checkpoints during a normal
session (initial render, after running a query, with the docs panel
open, with the history panel open) and gates PRs against a committed
baseline.

`cypress/.a11y-baseline.json` pins today's accepted violations —
color-contrast in several spots, a couple of nested-interactive cases,
link-in-text-block in the docs panel. CI fails on net-new only.

The spec lives alongside the existing Cypress suite, so it runs as part
of the normal `yarn e2e` flow. `cypress.config.ts` gets a small
`writeBaseline` Node task so the spec can persist baseline updates from
inside the browser.

## Refresh baseline

```
yarn workspace graphiql test:a11y:update
```

Refs: #4219
)

## Summary

Component-level a11y for v6. `@storybook/addon-a11y` surfaces axe
results next to each story while you're working on it;
`@storybook/addon-vitest` folds those same checks into the existing
Vitest suite so they run as part of `yarn test` in CI.

The model is per-story `parameters.a11y.test`:

- `'error'` (default) — axe violations fail the test
- `'todo'` — warn only, for stories with known issues we plan to fix
- `'off'` — skip a11y for the story

`vitest.config.mts` is split into two projects:

- `unit` — existing jsdom suite, unchanged behavior
- `storybook` — Vitest browser mode (Playwright Chromium), picks up
`.stories.*` files

The PR CI workflow gets one new step: `yarn playwright install
--with-deps chromium` ahead of `yarn test`.

## Run locally

```
yarn workspace @graphiql/react test                       # both projects
yarn workspace @graphiql/react vitest run --project=unit  # unit only
yarn workspace @graphiql/react vitest run --project=storybook
```

The Storybook a11y panel surfaces the same axe results live during `yarn
workspace @graphiql/react storybook`.

Refs: #4219
## Summary

- Migrate `Button`, `UnStyledButton`, `ToolbarButton`, and
`ExecuteButton` CSS to v6 OKLCH tokens.
- Add `variant?: 'default' | 'primary'` to `Button`; `primary` renders
the Run-button style.
- Switch `:focus` to `:focus-visible` on interactive states so the focus
ring no longer fires on mouse click. Aligns with [MDN
`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible)
and WCAG 2.4.7 (Focus Visible).
- Import `clsx` directly in `button` and `toolbar-button`, following the
leaf-module pattern from #4272. A follow-up PR will convert remaining
callers and remove the `cn` re-export.
- Add Storybook stories: `Primitives/Button` (Default, Primary, Success,
Error, Disabled) and `Primitives/ToolbarButton` (Default).

## Test plan

- [x] Open Storybook `Primitives/Button` and verify each variant matches
the design.
- [x] Tab into the buttons, then mouse-click them. Focus ring appears on
Tab only, not on click.
- [x] Open `Primitives/ToolbarButton`. Hover the icon button; a v6
tooltip appears.
- [x] Run `yarn dev:graphiql`. The Run button and toolbar buttons match
the new design.

Refs: #4219
## Summary

Adds `ResponseTreeView`, a custom collapsible tree renderer for GraphQL
response JSON. When the user switches to the Tree view in the response
pane, they now see the actual response rendered as an expandable tree
rather than a placeholder message. Top-level keys are expanded by
default; nested objects and arrays start collapsed with a summary
showing their child count. Values are colored by type to match the
editor's tokenizer palette: strings in blue, numbers in orange, booleans
in purple, and null in muted gray.

## Test plan

- [x] Open the example app, run a query, and switch the response pane to
Tree view. Verify the response renders as a tree with the top level
expanded.
- [x] Click a collapsed object or array node and confirm it expands to
show children. Click again to confirm it collapses.
- [x] Confirm `aria-expanded` updates correctly on each toggle button
(check with browser DevTools or a screen reader).
- [x] Switch back to JSON view and verify the Monaco editor reappears
with the response intact.
- [x] Run a query that returns nested objects more than two levels deep.
Confirm only depth 0 is expanded by default; click to drill in.
- [x] Check the tree renders correctly in both dark and light themes.
## Summary

- Fixes the long-standing flake in `docs.cy.ts` → "should show
deprecated arguments category title" that has been intermittently
failing v6 PR CI runs with `Expected to find content: 'Show Deprecated
Arguments' but never did`.
- Root cause is a race against the doc-explorer search debounce; the fix
removes the search dependency entirely.

## Root cause

The test navigated to the `hasArgs` field by typing into the
doc-explorer search box and clicking the first result:

```js
cy.dataCy('doc-explorer-input').type('hasArgs');
cy.dataCy('doc-explorer-option').first().children().first().click();
```

The search box debounces result computation by 200ms (`debounce(200,
...)` in `search.tsx`), and the results list is initialized to the
**empty-query** result set — which matches every type and field. So the
moment the input is focused, a full, stale list of options is already
rendered.

Cypress's `.first()` resolves as soon as *any* matching element exists.
Under CI load, Cypress would grab and click the first option of the
**stale full list** before the 200ms debounce filtered it down to
`hasArgs`. That navigated to the wrong field's docs, where no "Show
Deprecated Arguments" button exists, and the test timed out after
4000ms. Locally the debounce usually settled first, so it passed — a
classic timing-dependent flake.

The sibling "deprecated fields" test never flaked because it navigates
via the type tree and never touches search.

## Changes

- `docs.cy.ts`: navigate to `hasArgs` through the type tree (click the
query type, then the `hasArgs` field row) instead of the debounced
search box, mirroring the reliable "deprecated fields" test. The
field-row name is matched with an anchored regex (`/^hasArgs$/`) since
other fields' descriptions also mention `hasArgs`.

## Validation Steps

- `cd packages/graphiql && CI=true yarn e2e-server 'cypress run --spec
cypress/e2e/docs.cy.ts'`
- The full `docs.cy.ts` suite passes, including the previously-flaky
case. Ran 4× locally with no failures.
## Summary

Adds `ResponseTableView`, the Table view for the response pane. When the
user switches to Table, it walks the response body and renders every
list of objects as an HTML table with sticky headers, each captioned
with its path (e.g. `test.person.friends`) so sibling and aliased lists
each get their own table. Nested objects and arrays in cells are shown
as `Object {n}` / `Array [n]` shorthand to keep rows scannable.
Responses that contain no list field show a clear empty state rather
than a blank panel.

Lists nested deeper than a sibling that already matched stay as `Array
[n]` shorthand rather than getting their own table.

## Test plan

- [ ] Switch to Table view after running a list query and verify rows
and column headers appear
- [ ] Run a query with multiple or aliased sibling lists and verify each
renders as its own table captioned with its path
- [ ] Switch to Table view for a non-list response (e.g. a single object
or scalar) and verify the empty-state message is shown
- [ ] Check that nested object/array cells display `Object {n}` / `Array
[n]` shorthand
- [ ] Confirm ragged rows (objects with different keys) union columns
correctly; missing cells render a placeholder character
- [ ] Verify sticky column headers stay fixed when scrolling a long
result set
- [ ] Review Storybook stories: ListOfObjects, RaggedRows,
NestedObjects, NestedList, SiblingLists, PrimitiveArray, EmptyArray,
NoListField, NoResponse

Refs: #4219
## Summary

The variables/headers footer below the operation editor is replaced by a
`VarHeadersStrip` component with two tabs: Variables and Headers. The
existing JSON editors are reused under their respective tabs. A
right-aligned validity hint on the Variables tab shows how many
variables are defined and whether they parse as valid JSON.

## Test plan

- [ ] Switch between Variables and Headers tabs; each shows the correct
editor
- [ ] Edit variables JSON; the validity hint updates to reflect the
count and valid/invalid state
- [ ] Empty variables editor shows no hint
- [ ] Existing variables and headers content is preserved when switching
tabs
- [ ] The strip is vertically resizable using the drag handle above it
- [ ] The show/hide toggle collapses and restores the strip
- [ ] Active tab persists across page reload (stored in localStorage)

Refs: #4219
## Summary

The settings dialog exposes segmented controls for theme, density, font
size, and header persistence, backed by the `useGraphiQLSettings` hook
from #4322. Density and font-size presets fill in the `[data-density]`
and `[data-font-size]` token blocks, so changing a preset adjusts
spacing and typography; the font-size preset also drives the status bar
text, UI icon sizes, and the Monaco editor font. `SettingsDialog` lives
in `@graphiql/react` and is wired into the `graphiql` activity bar,
replacing the previous inline settings UI; the `forcedTheme` and
`showPersistHeadersSettings` props continue to work, with `forcedTheme`
hiding the theme control.

## Test plan

- [x] On the deploy preview, open the settings dialog from the
activity-bar gear; confirm Theme, Density, Font size, and Persist
headers controls render.
- [x] Change Density (Compact / Comfortable / Spacious) and confirm
spacing visibly shifts: top-bar height, activity-rail width, row
padding.
- [x] Change Font size (Compact / Default / Large / Extra Large) and
confirm the editor text, the status bar text, and the toolbar/rail icons
all scale.
- [x] Switch Theme (Auto / Light / Dark) and confirm the app re-themes.
- [x] Toggle Persist headers On, set a header, reload; confirm it is
restored only when On.

Refs: #4219
## Summary

`useGraphiQLPluginContext()` now returns a `transport` field that lets
plugins register request and response hooks via
`ctx.transport?.onBeforeSend(cb)` and `ctx.transport?.onResponse(cb)`.
Each callback returns a cleanup function for removal. The field is
absent when the host passes a `fetcher` rather than a `transport`, so
plugins can detect the supported path with a simple optional chain. The
hook registry wraps the underlying transport transparently: query,
mutation, subscription, and incremental-delivery flows all pass through
the registered callbacks.

## Test plan

- [x] In a plugin's `content` component, call
`useGraphiQLPluginContext()` and register an `onBeforeSend` hook that
adds a header; run a query and confirm the header reaches the server.
- [x] Register an `onResponse` hook and confirm it fires with the
response envelope after each query or subscription chunk.
- [x] Call the cleanup function returned by `onBeforeSend`; confirm the
hook no longer fires on subsequent requests.
- [x] Mount `<GraphiQLProvider fetcher={...}>` (no `transport`) and
confirm `ctx.transport` is `undefined`.

Refs: #4219
## Summary

- `createTransport` now has a coherent method-resolution model driven by
`supportedMethods`. The default is POST-only (`method: 'POST'`,
`supportedMethods: ['POST']`, no `setMethod`). Pass `['GET', 'POST']`
for both, or `['GET']` for GET-only.
- When both methods are supported, the active method defaults to POST
(unless `opts.method` overrides) and `setMethod` is available to switch
at runtime. GET queries encode `query`, `operationName`, and `variables`
into the URL with no request body; POST keeps the existing JSON body.
- Mutations are handled per the GraphQL-over-HTTP spec: a GET-only
transport throws a clear error (a compliant server would return 405),
and a both-methods transport with GET active transparently falls back to
POST.

## Changes

- `createTransport.ts`: method resolution from `supportedMethods` —
POST-only default with no `setMethod`; GET-only defaults active method
to GET; both-methods defaults to POST with `setMethod` exposed.
- GET request encoding: `query`, `operationName`, and `variables`
serialized into the URL query string, no body sent. POST requests still
carry `Content-Type: application/json` and the spec `Accept` header.
- Mutation handling: GET-only transport throws an error mentioning
"mutation" and "GET"; both-methods transport sends mutations as POST
even when GET is the active method.
- Construction- and runtime-time guards: `opts.method` must be a member
of `supportedMethods` (throws at construction); `setMethod` validates
its argument and throws if unsupported.
- `types.ts`: new types for `supportedMethods` / method resolution.
`create-fetcher/lib.ts`: updated to the new transport semantics. Adds a
changeset.

## Validation Steps

1. From `packages/graphiql-toolkit`, run the transport and fetcher
suites: `vitest run src/create-transport src/create-fetcher`.
2. POST-only default: construct with no `supportedMethods`; verify
`method` is `'POST'`, `supportedMethods` is `['POST']`, and `setMethod`
is `undefined`.
3. GET-only (`supportedMethods: ['GET']`): active method is `'GET'`
without passing `opts.method`; a query goes out as GET with params in
the URL and no body; a mutation throws an error mentioning "mutation"
and "GET".
4. Both methods with `method: 'GET'`: a query encodes params in the URL;
a mutation is sent as POST despite GET being active.
5. `opts.method` not in `supportedMethods` throws at construction;
`setMethod` to an unsupported method throws at runtime; a valid
`setMethod` switches the active method for subsequent requests.

Refs: #4219
Spec:
https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md
## Summary

- The response header now shows the real status, time, and size from
`lastResponse` instead of placeholder values.
- Still works when the legacy fetcher only provides partial metadata.

## Test plan

- [x] Stories render with mock response metadata.
- [x] Run a query and confirm the header shows status, time, and size.

Refs: #4219
## Summary

PR #4349 made `url`, `method`, and `supportedMethods` required on the
`Transport` type, but the transport hooks added in #4351 still build
`Transport` objects with only `send`. That left `@graphiql/react`
failing its type check on `graphiql-6`, which blocks every PR targeting
the branch.

- `wrap()` now spreads the wrapped transport, so
`url`/`method`/`supportedMethods` (and `setMethod`) carry through to the
returned transport instead of being dropped.
- The transport-hooks specs build their mock transports through a small
helper that fills in the required fields.

## Test plan

- [x] `@graphiql/react` type-checks cleanly (CI Types Check passes).
- [x] The transport-hooks unit tests pass.

Refs: #4351, #4349
The top bar rendered a gradient-filled placeholder square where the
brand mark belongs. This swaps in the official GraphQL logo, added to
`@graphiql/react`'s icon set as `GraphQLLogoIcon`.
`assertLinterMarkWithMessage` triggers Monaco's hover tooltip with one
`mousemove`, then looks for the message text. If that fires before
Monaco has the latest markers, it computes an empty hover and never
retries, so the tooltip never shows and the assertion times out
(`Expected to find content: '...' but never did`). The marker is already
checked directly via `getModelMarkers`, so only the hover step was
flaky.

Now it re-triggers the `mousemove` until the message shows, capped, with
a direct assertion at the end so real failures still surface. Fixes the
intermittent `lint.cy.ts` failure on "Marks syntax errors in variables
JSON as error", and the helper change covers the other lint tests too.

Same class of `...but never did` flake as #4348, which fixed a similar
race in `docs.cy.ts`.
…4362)

## Summary

GraphiQL tooltips had no height cap or `z-index`, so long content (for
example a markdown deprecation reason) could grow past the viewport and
make the whole page scroll, and sticky headers or dialogs could paint
over them. Tooltips now cap their height to the space between the
trigger and the status bar, scroll any overflow, and render above page
content.

## Changes

- Add `z-index` so tooltips sit above sticky headers and dialogs.
- Cap `max-height` to Radix's measured available height minus the status
bar, with `overflow-y: auto`.
- Use `box-sizing: border-box` so padding stays within the cap.

## Validation Steps

- [x] Hover a trigger with content taller than the viewport (e.g. a
field with a long markdown deprecation reason); confirm the tooltip caps
height, scrolls, and stops above the status bar instead of growing the
page.
- [x] Confirm the page does not become scrollable when the tooltip
opens, in both Safari and Chrome.
- [x] Hover near a sticky header or with a dialog open; confirm the
tooltip renders above it.
- [x] Confirm short tooltips look unchanged.
)

## Summary

New package `@graphiql/plugin-query-builder`, a first-party visual query
builder for GraphiQL. People have asked for one since #734.

Pick fields from the schema with checkboxes and the operation in the
editor stays in sync, both ways. Every change runs through the same
parse → mutate → reprint cycle using the `graphql` package's AST
utilities, so the builder and the editor never drift apart.

What it covers:

- **Schema tree.** Root types render as collapsible sections. Scalar
fields have checkboxes; object, interface, and union fields are
expand-only, since a composite isn't a valid selection without
subfields. Checking a nested scalar adds it and any parents; unchecking
removes it and prunes anything left empty (including the operation
itself when nothing is left).
- **Operation-aware.** The builder reads and edits the operation the
editor cursor sits in. In a multi-operation document only that
operation's root type is enabled; the others collapse. Moving the cursor
onto a field expands the tree down to it and highlights it.
- **Argument inputs.** Checked fields show inputs matched to each
argument's type: scalars (Int/Float as number inputs, Boolean as a
checkbox, String/ID as text), enums as a dropdown, lists with an
add/remove UI, and input objects as recursive nested editors, including
the list-of-input-objects case.
- **Variables.** "Use as variable" moves an argument's value into the
variables editor and references it as `$name` in the operation; an
"Inline argument" toggle reverses it. Suggested names are unique across
the whole document, and removing a field cleans up any variable it
orphaned.
- **Unions and interfaces.** Each concrete type shows as a `... on
TypeName` selector; checking a field inside one adds it within that
inline fragment. Interface fields shared by every type can also be
selected directly, without a type condition. Named fragments already in
the document are listed.

The plugin is default-installed in the `graphiql` package, so the
rail icon is present with no extra configuration. Omit it from your
`plugins` list to opt out.

This also changes `@graphiql/react`: the active `operationName` now
follows the editor cursor, so the Run button, the operation dropdown,
and operation-aware plugins reflect the operation you are editing. See
the `graphiql@6` migration guide for details.

Refs: #734, #4219
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Visual query builder from Graphql schema

1 participant