Skip to content

GraphiQL v6#4228

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

GraphiQL v6#4228
trevor-scheer wants to merge 55 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: a5fbe57

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

This PR includes changesets to release 7 packages
Name Type
@graphiql/react Minor
graphiql Major
@graphiql/plugin-doc-explorer Major
@graphiql/plugin-history Major
@graphiql/toolkit Patch
@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

A new `useGraphiQLSettings()` hook in `@graphiql/react` owns the theme,
density, and font-size preferences. Values persist to `localStorage`
under the `graphiql:settings` key and apply automatically to the
GraphiQL container via `data-theme` / `data-density` / `data-font-size`
attributes, so the CSS cascade picks them up. Theme `auto` resolves
through `prefers-color-scheme` and tracks live system theme changes;
explicit `light` / `dark` pin the resolved value. The hook is wired into
the top-level `<GraphiQL>` provider so the attributes are present on
first paint.

## Test plan

- [x] On the deploy preview, open DevTools and run
`localStorage.setItem('graphiql:settings',
JSON.stringify({theme:'dark',density:'comfortable',fontSize:'default'}))`,
then reload; the `.graphiql-container` element should carry
`data-theme="dark"`.
- [x] Clear the key, reload with the OS in dark mode; `data-theme`
should resolve to `dark` via `auto`.
- [x] Toggle the OS appearance while the app is open; the container
attribute should follow without a reload.
- [x] Inspect the rendered HTML and confirm `data-density` and
`data-font-size` are present even on a fresh install (defaults
`comfortable` / `default`).

Refs: #4219
## Summary

`@graphiql/toolkit` adds `createTransport`, which performs the GraphQL
request and returns a `TransportResponse` carrying the real HTTP wire
metadata (status, headers, timing, size). `<GraphiQL>` accepts a new
`transport` prop, mutually exclusive with `fetcher` at the type level,
that lets the response pane surface those values directly from the
underlying `Response`. The existing `Fetcher` API,
`createGraphiQLFetcher`, and the `fetcher` prop are deprecated but
continue to work unchanged; consumers staying on them see a one-time
dismissible banner in the response pane pointing at the migration guide
rather than fabricated values. Subscriptions require an explicit
`subscriptionClient`: pass your own `graphql-ws` or `graphql-sse` client
(both are signature-compatible). The toolkit does not build one for you.

Alternative to #4323. Refs #4019.

## Test plan

- [ ] Run `yarn workspace graphiql dev`, execute a query, and confirm
the response pane header shows the real HTTP status code (200), a real
elapsed-time badge in ms, and a real response-size badge.
- [ ] `cy.intercept` to a 500 and confirm the status pill flips to the
error state.
- [ ] Migrate an existing project from `createGraphiQLFetcher` to
`createTransport` and from `<GraphiQL fetcher={...}>` to `<GraphiQL
transport={...}>`; queries, mutations, subscriptions, and incremental
delivery (`@defer`/`@stream`) all still work.
- [ ] Pass `graphql-sse`'s `createClient({url})` as `subscriptionClient`
to `createTransport` and confirm subscriptions stream over SSE.
- [ ] Render `<GraphiQL fetcher={someFetcher}>` and confirm the in-pane
upgrade banner appears, dismisses on click, stays dismissed across
reloads, and the response pane header is fully unmounted after dismiss
in the `fetcher` path.
- [ ] Confirm passing both `fetcher` and `transport` to `<GraphiQL>` is
a TypeScript compile error.
- [ ] Confirm dispatching a subscription on a `Transport` configured
without `subscriptionClient` throws with a pointer at the migration
guide.
Node 24 ships with a `jsdom` version that defines `window` but leaves
`localStorage` undefined, which causes `StorageAPI` to throw on
construction in unit tests. This adds a minimal in-memory `localStorage`
polyfill to the vitest setup files for `@graphiql/react` and
`@graphiql/toolkit`, gated on `typeof localStorage === 'undefined'` so
it has no effect in environments where the real API is present. The
toolkit package previously had no setup file, so this also wires one
into its `vitest.config.mts`.
## 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
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