Skip to content

GraphiQL v6#4228

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

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

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 Major
@graphiql/plugin-history Major
graphiql Major
@graphiql/plugin-doc-explorer Major
@graphiql/plugin-collections Major
@graphiql/toolkit Major
@graphiql/plugin-query-builder Major
@graphiql/plugin-code-exporter 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
…nal (#4404)

## Summary

- The bottom status bar drops the plugin count, the `UTF-8` encoding
label, and the `GraphQL` language label. None of them told the user
anything useful; they just made the bar look like a code editor's
chrome.
- The connection dot reflects real state instead of just "schema loaded
or not." It's amber while introspection or a request is in flight, green
once a schema is loaded, red when the last introspection or request
failed, and gray when nothing has happened yet. Each state has a
matching text label and an accessible title on the dot.
- The types count stays as a plain readout ("142 types") once a schema
is loaded. It's deliberately not clickable, so the status bar doesn't
reach into the doc-explorer plugin.
- This doesn't cover full subscription-level status (an open
subscription's live/closed state, tracked separately from a one-shot
query). The dot derives from schema-load state plus the last request's
fetch/error state, which handles the common cases without new transport
plumbing.

## Test plan

- [x] Open GraphiQL. The status bar shows only a connection dot with a
label and a types count, with no plugin count, `UTF-8`, or `GraphQL`.
- [x] Before a schema loads, the dot is gray and reads "Idle".
- [x] While the schema is loading, the dot is amber and reads
"Connecting".
- [x] Once the schema loads, the dot turns green, reads "Connected", and
the types count appears.
- [x] Run a query successfully. The dot stays green.
- [ ] Point the endpoint at something that errors (bad URL, malformed
response). The dot turns red and reads "Connection error".
- [ ] `yarn workspace @graphiql/react test` passes.

Refs: #4219
## Summary

- The glyph inside a `KeycapHint` key (`⌘`, `⌃`, `⌥`, `⇧`, `⏎`, letters)
sat slightly above center. Centering the line box wasn't enough by
itself, since these glyphs carry more ink above the baseline than below
it.
- The glyph now renders in its own span with a small `translateY` nudge,
measured against each glyph's actual ink bounds rather than guessed. `⌃`
needed the most help; the rest were already close.

## Test plan

- [ ] Open GraphiQL, look at a keycap hint in the top bar (e.g. the `⌘`
`⏎` run shortcut), and confirm the glyph is vertically centered on the
key.
- [ ] In Storybook, view `Primitives/KeycapHint` (Single,
ChordWithModifier, RunShortcut stories) in both dark and light theme and
confirm centering holds across the different glyphs.

Refs: #4219
…4401)

## Summary

The Settings dialog's "Clear data" button was unstyled, and clicking it
turned the whole button green. This restyles it to match the segmented
controls it sits beside — same typography (`font-size-small`, weight
500, `letter-spacing-ui`), `bg-subtle` fill + `border-muted` border, and
the same height as a full segmented bar (4px vertical padding = a
segment cell's 3px plus the track's own 1px). It no longer reads as a
foreign element in that row.

On click, the label cross-fades to a green checkmark **in place** and
back after ~1.5s — rather than the check appearing beside the label. The
label stays in the DOM (at `opacity: 0`) so the button keeps a fixed
width and its accessible name, and an `sr-only` live region announces
"Data cleared" for screen-reader users. The swap respects
`prefers-reduced-motion`.

## Test plan

- [x] Open Settings. The "Clear data" button should read as a sibling of
the Density/Font size segmented controls — same font, weight, and height
— not a larger, heavier button.
- [x] Click "Clear data". The label should swap to a green checkmark in
place (no sideways shift, no reflow), then revert to "Clear data" after
~1.5s.
- [x] Confirm the stored settings (theme, density, font size, etc.) are
actually cleared after clicking.
- [x] Check both Dark and Light themes — the button and checkmark should
look right in each.
- [x] With a screen reader, clicking should announce "Data cleared".

Refs: #4219
#4399)

## Summary

Reworks the green **"Run"** button in the top bar
(`.graphiql-top-bar-run`) so it stops reading as a heavy, cluttered
element in an otherwise monochrome bar.

- Add a small play icon before the "Run" label.
- Add a faint vertical divider between the label and the keyboard
shortcut.
- Recolor the shortcut keys (`⌘` `⏎`) into translucent-white chips that
sit on the green fill, instead of the default dark-bordered keycap boxes
that looked like foreign objects on the button.
- Set the label to medium weight (500) rather than the previous heavy
bold (600).

Also removes the hardcoded `font-weight: 600` from the shared primary
button variant (`.graphiql-button-primary`), used by the collections
plugin dialogs (Import / Export / Save), so its label matches the other
buttons.

## Test plan

- [ ] Open GraphiQL and look at the top bar "Run" button: play icon +
"Run" + a faint divider + the `⌘⏎` shortcut as subtle chips. It should
read as one calm unit, not stand out as bold/busy.
- [ ] The shortcut chips should be legible white-on-green, not dark
boxes.
- [x] Open a collections dialog (e.g. Save / Import) and confirm the
green primary button's label matches a neutral button's weight.
- [x] Disabled state (e.g. GET selected with a mutation) still dims the
whole button and shows the "switch to POST" tooltip.

Refs: #4219
…4402)

## Summary

Schema search was unreachable. The top-bar "Jump to schema" button had
no `onClick`, and the real shortcut (`⌘⌥K`) clicked a
`.graphiql-sidebar` button selector that stopped existing once v6
renamed that container to `.graphiql-activity-rail`.

The dead button is gone, and schema search is rebound to plain `⌘K` /
`Ctrl+K`. The handler opens the doc explorer through `setVisiblePlugin`
instead of clicking a DOM node, and it takes priority over
monaco-editor's own `Cmd+K` binding, so it works whether or not an
editor pane has focus. The shortcuts help dialog and key map are updated
to match.

## Test plan

- [x] Press `⌘K` (or `Ctrl+K`) with no editor focused. The doc explorer
opens and the search input is focused.
- [x] Click into the query editor, then press `⌘K`. The doc explorer
still opens and search is focused; Monaco should not swallow the
shortcut.
- [x] Confirm the old "Jump to schema" button in the top bar is gone.
- [x] Open the shortcuts help dialog (`?` or the help entry) and confirm
"Search in documentation" lists `⌘K` with no Alt/Option.

Refs: #4219
## Summary

- Remove the legacy vertical editor toolbar and the floating "GraphiQL"
corner logo. Branding now lives only in the top bar, and editor actions
live only in the tab strip.
- Move Merge Fragments, previously available only in the old toolbar,
into the tab-strip actions alongside Prettify, Copy, and Save, so the
action isn't lost.
- The composable `Toolbar`/`Logo` render-prop slots still work for
embedders. Only the default rendering is removed.
- Enlarge the tab-strip action buttons so they're easier to click.
- Position the action buttons at the right edge of the query editor
instead of the far right of the response pane, so they stay next to the
query and follow the editor/response divider as it's resized. The
sessions area is split into an editor column and a response column
divided by the drag bar; the tab strip lives in the editor column.

## Test plan

- [x] Open GraphiQL. There's no floating vertical button column and no
corner logo over the editor; branding appears once, in the top bar.
- [x] The tab strip shows Prettify, Merge Fragments, Copy, and Save,
each exactly once.
- [x] The action buttons sit at the right edge of the query editor (by
the editor/response divider), not over the response pane, and follow the
divider when it's dragged.
- [x] Write a query that references a fragment, click Merge Fragments,
and confirm the fragment gets inlined into the operation.
- [x] Prettify reformats the query, Copy copies it, and the single Run
button in the top bar still executes it.
- [x] The Prettify, Merge, and Copy keyboard shortcuts still work.
- [x] The tab-strip action buttons read as comfortably sized, not
cramped, and stay evenly spaced and aligned with the tab labels.

Refs: #4219
## Summary

This PR does two related things for GraphQL identifier coloring across
the first-party plugins.

**1. Align colors across plugins.** Type names, field names, and
argument names now use the same colors across the doc explorer, history,
and query builder. Doc explorer was already internally consistent
(orange types, green fields, purple arguments), so it's the reference
the other two now follow. History's saved query labels (previously plain
foreground text) are now the same green as field names. Query builder
previously colored composite return types and type-condition names blue
and left argument names uncolored; both now match doc explorer.

**2. Color type names by category.** Instead of every type name being
orange, type-name references are colored by GraphQL kind, so a field
list is no longer a monotonous wall of orange:

| Kind | Color |
|---|---|
| object / interface / union | orange |
| scalar | blue |
| enum | green |
| input object | gold |

This applies in the schema-aware plugins (doc explorer and query
builder). The Monaco query editor is unchanged — it isn't schema-aware,
so type names in query text stay orange.

### Public API

The scheme is exposed so third-party plugins can conform to it and
retheme it:

- Four CSS tokens — `--type-composite`, `--type-scalar`, `--type-enum`,
`--type-input` — are the supported retheming surface (documented in
`tokens.css`).
- `typeCategory(type)` is exported from `@graphiql/react`; it buckets
any GraphQL type into `'scalar' | 'enum' | 'input' | 'composite'`,
unwrapping list/non-null wrappers.

Class names, internal selectors, and the `data-type-kind` attribute
remain private. Light-theme `--type-input` uses a darker gold than the
dark theme so it clears WCAG AA as text (~5–6.5:1 against the
doc-explorer surfaces).

## Test plan

- [x] **Alignment:** Open the doc explorer, history, and query builder
in the same session. A field name is the same green in all three; an
argument name is the same purple in both doc explorer and query builder.
- [x] **Category colors (doc explorer):** Browse a schema with mixed
field types. Scalar return types (`String`/`Int`/`Boolean`/`ID`/custom
scalars) are blue, enum types green, object/interface/union types
orange, and input object types gold.
- [x] **Category colors (query builder):** Expand the field tree.
Scalar-returning fields are blue, enum-returning fields green,
object/interface/union fields orange.
- [x] **Arguments (doc explorer):** A field with an input-object
argument shows the input type name in gold, clearly distinct from the
purple argument name beside it.
- [x] **Type conditions (query builder):** Expand an interface/union
field's possible types. The `... on TypeName` labels are orange.
- [x] **Search (doc explorer):** Search results show type names colored
by the same category scheme.
- [x] **History:** Saved query entries show their label/operation name
in green.
- [x] **Light theme:** Switch themes and spot-check the same views. All
four category colors stay legible against the panel background — the
gold in particular should read as a clear dark gold, not washed out.

Refs: #4219
## Summary

Audited keyboard navigation across the current layout: top bar, activity
rail, side panel, editor, response pane, variables/headers strip. Tab
order is predictable end to end, and Shift+Tab retraces it exactly, so
no reordering was needed there. Two real bugs turned up:

- The Settings dialog trapped focus correctly and closed on Escape, but
every close path (Escape, the X button, clicking outside) silently
dropped focus to the page instead of sending it back to the gear button
that opened the dialog. Radix restores focus to whatever had it when the
dialog opened, but that snapshot kept getting overwritten by the
dialog's own re-renders while it was open. `Dialog` now takes an
explicit `restoreFocusRef` instead of relying on Radix's implicit
tracking.
- In the History panel, pressing Escape while renaming a saved query did
nothing. The handler was checking for the key name `'Esc'`, which no
browser has sent since Internet Explorer; it should be `'Escape'`. Fixed
the check, and made both Escape and Save return focus to the row's edit
button, since the rename input disappears on close and focus had nowhere
else to go but the page.

Added a Cypress spec that walks Tab and Shift+Tab through the whole app
and exercises both fixes, along with the settings dialog's focus trap
and the execute button's operation dropdown (already working fine,
thanks to Radix).

## Test plan

- [x] Load the app fresh, click anywhere non-interactive, then Tab.
Focus should land on the top bar's method chip, then move through: Run
button, activity rail icons, session tab strip, logo, operation editor,
editor toolbar buttons, variables/headers tabs, response view picker,
copy button, result window. Shift+Tab from the end should retrace the
same path in reverse.
- [x] Tab to the operation editor (it's a single stop) and press Enter.
Focus should move into the actual Monaco text area so you can start
typing.
- [x] Open Settings with the gear button, then press Escape. The dialog
should close and focus should land back on the gear (Tab afterward
should move to the next rail item, not restart from the top of the
page).
- [x] Reopen Settings and close it by clicking the X, then again by
clicking outside the dialog. Focus should return to the gear both times.
- [x] While Settings is open, Tab repeatedly. Focus should stay inside
the dialog the whole time and never escape to the page behind it.
- [x] Run a query, open History, click the pencil icon on an entry to
rename it, then press Escape. The rename input should disappear without
saving, and focus should land on that row's edit button.
- [ ] Write a query with two named operations, click the Run button's
dropdown arrow, use arrow keys to move between them, then press Escape.
The menu should close and focus should return to the Run button.
- [ ] `yarn workspace graphiql cypress run --spec
cypress/e2e/keyboard.cy.ts` passes.

Refs: #4219
## What

Consolidate the pending changesets on `graphiql-6` so they describe the
finished v6 release rather than the path we took to build it.

`graphiql-6` is unreleased and in changesets pre-mode with `pre.json`
`changesets: []` (**no alpha has consumed any changeset yet**), so this
pending set becomes the `6.0.0` changelog verbatim. It had grown to **62
entries** written as if each incremental step would be felt by a user
("add `TopBar` with a placeholder URL" → later "wire the real URL"; a
dozen separate `restyle-*` bullets). None of those intermediate states
ship.

This collapses the set to **15 changesets** that read as the actual
changelog.

## Approach

| Bucket | Changesets |
| --- | --- |
| **Redesign** (one entry, self-explanatory) | `v6-redesign`: tokens,
light/dark theming, layout chrome (top bar, activity rail, side panel,
status bar, flattened editor, vars/headers strip), restyled components,
Monaco themes, doc explorer + history rebuilds, unified syntax colors,
browserslist |
| **One per feature** | `transport-api`, `response-pane`, `settings`,
`collections`, `query-builder`, `operation-name-follows-cursor` |
| **New `@graphiql/react` primitives** (new public API, one per concern)
| `keycap-hint`, `method-pill`, `segmented-control`, `panel-header` |
| **Breaking changes** (each stands alone) | `remove-deprecated-hooks`,
`drop-legacy-client-alias`, `explorer-deprecation` |
| **Unrelated package, untouched** | `graphql-value-string-highlighting`
(`vscode-graphql-syntax`) |

Feature changesets are written as end state. E.g. the response pane
header simply shows real status/time/size, with no mention of the
placeholders it briefly had.

## Editorial calls worth a look

- Dropped `v6-alpha-line` ("Initial v6 alpha"); its `#4219` reference
and migration-guide pointer fold into `v6-redesign`, which is now the
`graphiql` major headline.
- `history-method-pill` folds into the redesign's history bullet; its
small additive `QueryStoreItem.operation` field on `@graphiql/toolkit`
is no longer called out separately (toolkit is majored by
`drop-legacy-client-alias` regardless, and the redesign narrative
shouldn't land in the toolkit changelog).
- `query-builder` left at `plugin-query-builder: patch`, preserving
existing semver intent, not editing it. Flag if you want it minor.

## Validation

`changeset status` yields an **identical** set of package bumps before
and after, verified against `origin/graphiql-6`:

- **major:** `graphiql`, `@graphiql/react`, `@graphiql/toolkit`,
`@graphiql/plugin-doc-explorer`, `@graphiql/plugin-history`,
`@graphiql/plugin-explorer`
- **minor:** `@graphiql/plugin-collections`
- **patch:** `@graphiql/plugin-query-builder`, `vscode-graphql-syntax`,
`@graphiql/plugin-code-exporter` (transitive)

No version resolves differently; this is purely changelog-narrative
editing.
> Stacks on #4409 (keyboard navigation audit). Review that one first;
this branch is based on it, so the diff here is only the
labels-and-focus-ring work.

## Summary

- Adds a single global focus ring. Any control focused with the keyboard
now gets a 2px blue outline (`--accent-blue`) with a small offset, so
nothing slips through without a visible focus indicator. The color
clears WCAG's non-text contrast threshold against the canvas in both
themes (about 9.7:1 in dark, 5.6:1 in light). Components that already
drew their own ring keep it, and the global rule backstops the rest.
- The doc explorer search box was the one focusable control with no
visible focus state. The input cleared its own outline and nothing
replaced it. It now shows the same ring while it has focus.
- Decorative icons that sit next to a text label in the doc explorer
(section headers, the FIELDS and arguments toggles, the two search
magnifiers) are marked `aria-hidden`. A screen reader now reads just the
label instead of also announcing "chevron down icon" or "magnifying
glass icon" next to it.
- The cancel button on a history label edit was icon-only with no name.
It now has an accessible label, so it reads as "Cancel" instead of an
unlabeled button.

## Test plan

- [ ] Tab through the app. Every control you land on shows a clear blue
focus ring, in both light and dark themes.
- [ ] Open the doc explorer and Tab (or click) into the search box. The
box shows a focus ring while it has focus.
- [ ] With a screen reader on, move through the doc explorer. Section
headers, the FIELDS toggle, and the argument toggles read their label
once, without a trailing icon name.
- [ ] In History, start editing a saved query's label and Tab to the
cancel (X) button. It announces as "Cancel".
- [ ] axe baseline holds: no new violation ids vs. the recorded
baseline.

Refs: #4219
…vider) (#4414)

## Summary

Three small fixes to the response pane, grouped since they all touch the
same surface:

- **The loading spinner no longer shifts the layout.** It was rendered
as a flex sibling above the response, so every query pushed the view
picker and results down, then snapped back when the response arrived.
It's now an absolutely-positioned overlay centered over the response
body, so nothing reflows. It's also smaller and uses the accent color to
match the header.
- **Consistent inset across all three response views.** The JSON result
window sat flush against the header, while Tree and Table used different
insets (12/16px and 14px). All three now use the same `var(--px-16)` as
the query and variables editors.
- **Table view rows get their divider back.** The row border referenced
`--border-subtle`, an undefined custom property, so per the CSS spec the
whole declaration was dropped and rows rendered with no separator. It
now uses `--border-muted`.

## Test plan

- [x] Run a slow query. The spinner appears centered over the response
area, and the JSON/Tree/Table view picker above it does not jump or
reflow when the spinner appears or clears.
- [x] With a result showing, confirm the JSON has an even 16px inset on
all sides (matching the query editor), not flush to the header. Switch
to Tree and Table views; the inset is the same in all three.
- [x] Run a query returning a list and switch to Table view. Each row
has a subtle bottom divider, consistent with the header separator.
- [ ] `yarn workspace @graphiql/react test` and `yarn workspace graphiql
test` pass.

Refs: #4219
…iql/plugin-explorer` (#4416)

## Summary

Collapses the two parallel color systems v6 shipped with — the legacy v5
`--color-*` HSL variables and the v6 OKLCH tokens — down to one, and
removes the last consumer of the old set.

- Migrates every first-party reader of `--color-*` to the v6 OKLCH
tokens in `tokens.css` (core library, `plugin-code-exporter`,
`plugin-collections`, and the `graphiql-webpack` example), following the
idioms already used by the restyled components.
- **Deprecates** `--color-*` rather than deleting it: the v5 README
documented the whole `root.css` variable set as public retheming API,
and the HSL→OKLCH format change rules out a transparent alias. The
variables stay frozen at their v5 values with an inline deprecation
comment; overriding them no longer re-themes GraphiQL, so consumers
should move to the OKLCH tokens.
- **Removes `@graphiql/plugin-explorer`** — the last first-party
`--color-*` reader, now superseded by the default
`@graphiql/plugin-query-builder`. It remains maintained on the v5 LTS
branch.
- Docs: the migration guide gains a `--color-*`→v6 mapping table and its
`plugin-explorer` section moves from deprecation to removal; the READMEs
point retheming at `tokens.css`.

## Test plan

- [ ] On the deploy preview, toggle Light/Dark and confirm text,
borders, icons, hover states, markdown deprecation admonitions, and the
settings dialog all render correctly in both themes.
- [ ] Confirm the default query-builder plugin still opens, and the
explorer plugin is gone from the webpack and CDN examples.
- [ ] In devtools, override `--color-primary` on `.graphiql-container`
and confirm it no longer changes the UI (deprecated); override the
matching OKLCH token (e.g. `--accent-blue`) and confirm it does.

Refs: #4219
…4410)

## Summary

Documents the v6 OKLCH design-token system in the `@graphiql/react`
README's theming section — the `--bg-*` / `--border-*` / `--fg-*` /
`--accent-*` / `--type-*` / `--btn-*` / `--radius-*` / `--shadow-*`
families, the `L% C H` triplet convention, the type-name category
tokens, and the exported `typeCategory` helper. Also marks the legacy v5
`--color-*` HSL variables deprecated and updates the migration-guide
caveat now that the README documents both sets.

Docs follow-up to #4416 (the `--color-*` deprecation and
`@graphiql/plugin-explorer` removal).

## Test plan

- [x] Render the README theming section: the OKLCH token list matches
`tokens.css`, the `typeCategory` description is accurate, and the
intra-page + `tokens.css` / migration-guide links resolve.

Refs: #4219
## Summary

The tab close button (`.graphiql-tab-close`) only changed text color on
hover: no background, no rounded corners, no transition, and no
`:focus-visible` outline. It's a keyboard-focusable button, so tabbing
to it left no visible focus indicator at all. That's an accessibility
gap, and it was the one icon button in the app that didn't match the
hover/focus pattern used everywhere else: the activity rail, toolbar
buttons, and the dialog close button. It now follows that same pattern.

## Test plan

- [x] Open several editor tabs, keyboard-Tab to a tab's close (✕)
button, and confirm it shows a clear blue focus ring.
- [x] Hover the close button and confirm a subtle background appears
behind it with rounded corners.
- [x] Click the close button and confirm the tab still closes as before.
- [x] Check both Dark and Light themes. The hover background and focus
ring should be legible in each.

Refs: #4219
## Summary

The History panel had no empty state. With no run history and no
favorites (first run, or right after "Clear"), it rendered only its
header and nothing below, just blank space. Add a "No queries run yet."
message for that case, styled to match the Collections panel's existing
empty state.

## Test plan

- [x] Open a fresh GraphiQL instance with no query history. Open the
History panel and confirm it shows a "No queries run yet." message below
the header instead of blank space.
- [x] Run a query. Reopen the History panel and confirm the empty-state
message is gone, replaced by the new history entry.
- [x] Click "Clear" to remove all history and favorites. Confirm the
empty-state message reappears.
- [x] Favorite an item, then delete the remaining regular history so
only the favorite is left. Confirm the empty-state message stays hidden
(favorites alone should not trigger it).
- [x] Check both Dark and Light themes. The empty-state text should be
legible and match Collections' muted styling in each.

Refs: #4219
## Summary

- Brings back an explicit way to pick which operation runs. When the
document has more than one operation, the Run button grows a caret.
Clicking it opens a menu of every operation by name, and picking one
sets it as the active operation and runs it, the same behavior the old
execute button had.
- Cursor tracking is still the default. Moving the cursor into a
different operation re-activates it, even after a menu pick, so nothing
gets pinned. The menu marks the currently active operation.
- An operation that can't run under the current HTTP method (a mutation
while GET is selected) shows disabled in the menu, the same block reason
already shown on the button itself.
- The Run control now has visible `:focus-visible` and `:active` states.
Previously only the dead execute-button code had them.
- Removed the unused `ExecuteButton` component now that the TopBar owns
this menu directly. Keeping both around would leave two copies of the
same picker logic.

## Test plan

- [x] Write a document with two named operations. The Run button shows a
caret sized to match the Run half (same height, one seam). No operation
name sits beside the button.
- [x] Click the caret. A menu lists both operations, with the active one
marked. Pick the one the cursor is *not* in; that operation runs and
becomes active.
- [x] Move the cursor back into the other operation. It becomes active
again (cursor still wins; the pick wasn't sticky) — confirm via the mark
in the menu.
- [x] Add an operation that's invalid for the current method (e.g. a
mutation while on GET). Its menu item is disabled.
- [x] With a single operation (or none), no caret appears. The button is
a plain Run.
- [x] Keyboard: focus the caret, open with Enter/Space, move between
operations with arrow keys, press Escape. The menu closes and focus
returns to the Run button.
- [x] Looks modern: the menu matches the app's other dropdowns (elevated
surface, rounded corners, subtle shadow, highlight-on-hover); the caret
reads as one control with the green Run pill; both halves show a focus
ring on keyboard focus and a press state. Verify in light and dark.

Refs: #4288, #4219
## Summary

Reworks how the query builder handles fragments end to end.

**Extraction is now an inline row action.** Fragment extraction used to
be a single "Create fragment from selection" button pinned to the bottom
of the panel, acting on wherever the editor's text cursor happened to
sit. It now lives on the field rows: expand a composite field, tick the
children you want, and an "Extract to fragment" action appears on that
row. Triggering it lifts the selection into a named fragment and
replaces it with a spread (`user { ...UserFields }`). The generated name
(`UserFields`, de-duped when taken) stays editable in place from the
Fragments list.

**Extracted rows stay editable.** An extracted field no longer collapses
into a locked `...FragmentName` badge. It stays a normal, expandable
composite: the spread renders as a `...FragmentName` reference row among
the field's children, and ticking more fields adds them to the **base
query** alongside the spread. A field's only tie to its fragment is the
real spread in the document — it doesn't "remember" being extracted.

**Fragments are editable in place.** Because a fragment's contents are
shared across every field that spreads it, editing them needs its own
surface. The active editing target now **follows the editor cursor**:
move the cursor into a `fragment … { … }` block and the builder switches
to editing that fragment's tree, rooted at its type condition. You can
also jump in by clicking a `...FragmentName` reference row or a name in
the Fragments list; "Back to query" returns to the operation. Fragments
are flat peers — no nesting or breadcrumbs.

**The fragment reuse suggestion buttons are gone.** The short-lived "Use
...FragmentName" reuse action (and its dead helpers) is removed — it
crowded the row, and multiple matching fragments on one field overflowed
the small row.

The row actions are plain keyboard-focusable buttons. A dedicated
shortcut and keycap hint are left for the app-wide accessibility pass.

### Implementation notes

The document mutators and readers were generalized from an
operation-only `operationName` to a `DefinitionTarget` (`operation |
fragment`), so the same field tree and handlers drive operation and
fragment editing alike. Emptying a fragment is a no-op (a fragment can't
be emptied without orphaning its spreads); promote-arg-to-variable stays
operation-only.

## Test plan

- [x] Open the query builder against a schema. Expand a composite field
(e.g. `person`), tick a scalar child (e.g. `name`), and confirm an
"Extract to fragment" action appears on that row.
- [x] Trigger it. Confirm the query reads `person { ...PersonFields }`
with a `fragment PersonFields on Person { name }` block, and the
`person` row now shows a `...PersonFields` reference among its children.
- [x] With the field still expanded, tick another child (e.g. `age`).
Confirm it lands in the **base query** — `person { ...PersonFields age
}` — and the fragment is unchanged.
- [x] Click the `...PersonFields` reference row. Confirm a focused
fragment editor opens (header `PersonFields on Person`, "Back to query"
control), rooted at `Person`.
- [x] Tick a field in the fragment editor. Confirm it edits the fragment
(`fragment PersonFields on Person { name age }`) and the operation still
just spreads it.
- [x] Move the editor cursor into the `fragment PersonFields …` block.
Confirm the builder switches to editing that fragment. Move it back into
the operation; confirm it switches back.
- [x] Click a fragment's name in the Fragments list. Confirm it focuses
that fragment and is highlighted as active.
- [x] In the Fragments list, rename `PersonFields`. Confirm the
definition and every `...PersonFields` reference update together.
- [x] Keyboard-only: Tab to the extract action / reference row /
fragment name, confirm each takes focus and activates with Enter/Space.
- [x] Unit and `query-builder.cy.ts` suites pass; Storybook a11y check
clean.

Refs: #4219
## Summary

The doc explorer's search input and results popover had their shadow
hardcoded to `0 4px 12px oklch(0% 0 0 / 0.3)`. That's the dark-theme
value of `--shadow-popover`, pasted in as a literal instead of a
reference, and it's missing the token's second, subtler shadow layer
too. Every other popover in the app (dialog, dropdown, tooltip) reads
`var(--shadow-popover)` and re-themes on its own, so in light theme this
one was casting a shadow roughly four times heavier than its siblings.
Both spots now read the token directly.

## Test plan

- [x] Switch to light theme, open the doc explorer, and focus the search
box. The active-state input and the results popover both cast a soft,
light-appropriate shadow, not a heavy dark one.
- [x] Switch to dark theme and confirm the search popover shadow still
matches other popovers (dialog, dropdown, tooltip).

Refs: #4219
…zing) (#4421)

## Summary

- `MethodPill`'s query/mutation/subscription colors were hand-typed
OKLCH values that approximated
`--accent-green`/`--accent-yellow`/`--accent-purple` but didn't
reference them, so the pills never re-themed with the rest of the app.
They now read the tokens directly. Query and subscription flip their
text color between near-black and near-white depending on theme, the
same split `.graphiql-button-success` and the query-builder's variable
toggle already use, since the token values (unlike the old hardcoded
ones) differ enough per theme to change which text color clears
contrast.
- The status bar's item gap (`--px-14`) didn't match the top bar's
(`--px-12`), its closest structural sibling, for no obvious reason. Both
now use `--px-12`. Leaving the status bar's mono/small/muted type scale
alone on purpose here, that reads as a deliberate "quiet metadata strip"
choice, not a bug.
- Icon buttons in the toolbar, activity rail, and response header each
sized themselves differently: a legacy `--toolbar-width` variable, a
hardcoded 32px, and a hardcoded 28px override. Added a shared
`--icon-button-size-sm/md/lg` scale to the density presets in
`tokens.css` and pointed all three at it.

## Test plan

- [x] Switch between light and dark themes and confirm the QRY/MUT/SUB
method pills recolor with the rest of the accent palette and their text
stays legible in both.
- [x] Confirm the status bar's item spacing now matches the top bar's.
- [x] At each density preset (Compact/Comfortable/Spacious in Settings),
confirm icon buttons in the activity rail, top-bar toolbar, and response
header size consistently with each other.
- [x] In the response header, confirm the toolbar icon buttons still fit
comfortably inside the 32px-tall header bar without touching its edges.

Refs: #4219
…ills (#4422)

## Summary

- The doc-explorer's `field-card`, `type-card`, and `fields-list` all
hardcoded `font-size` in px (11.5px, 14px, 10px, 9px, and more), so none
of that text grew or shrank with the font-size preset. They now read
`--font-size-mono`, `--font-size-small`, or `--font-size-eyebrow`
depending on which is closest to the original size.
- `response-header`, `panel-header`, and `method-pill` had the same
problem: fixed px values that ignored Settings > Font size entirely.
Migrated to the matching tokens.
- The legacy `--font-size-hint` token (`root.css`) was a frozen value
that never scaled with the preset, at ~19 call sites across
`var-headers-strip`, the collections plugin, the history plugin, and the
doc-explorer plugin. Every usage now points at `--font-size-eyebrow`
instead, and the dead `--font-size-hint` definition is gone.
- Along the way, found that `--font-size-body` itself is silently
shadowed by a frozen legacy value in `root.css` (same class of bug, but
out of scope here, so flagged separately). Avoided that token in the new
mappings and used `--font-size-mono` for the mono-family names/types
instead, since it actually rescales.

## Test plan

- [x] Open Settings, set Font Size to Large or Extra Large, then open
the Documentation Explorer. Confirm type names, field names/types,
descriptions, and the "FIELDS · N" section label all grow with it (they
previously stayed fixed).
- [x] Run a query and confirm the response header (status code, timing,
size) grows with the font-size setting.
- [x] Confirm panel headers (e.g. the "History" and "Documentation
Explorer" panel titles) grow with the setting.
- [x] Confirm method pills (QRY/MUT/SUB badges) grow with the setting.
- [x] Set the font size back to Default and confirm everything returns
to its original size with nothing shifted or clipped.
- [x] Check both Dark and Light themes at a couple of font-size presets
to confirm nothing overlaps or truncates awkwardly at the larger sizes.

Refs: #4228
## Summary

`@graphiql/react` had a `cn` helper that was just a re-export of `clsx`,
left over from before the Button/IconButton restyle. It didn't add any
behavior of its own, so this removes it and has every internal usage
import `clsx` directly instead: all the component files in
`graphiql-react` that used `cn(...)`, plus `@graphiql/plugin-history`
and the `graphiql` package, which both imported `cn` from
`@graphiql/react` rather than depending on `clsx` themselves. Both
packages now list `clsx` as a direct dependency.

Since `cn` was exported from `@graphiql/react`'s public entry point,
dropping it is a breaking change for 6.0.0. Added a changeset with a
major bump and a short note in the v6 migration guide pointing anyone
using `cn` at `clsx`.

Refs: #4228
…4424)

## Summary

List rows across the side panels had four different "this row is
highlighted" recipes doing the same job. Activity rail, dropdown menu
items, and Collections rows all used a flat `oklch(var(--bg-subtle))`
background on hover, which is the pattern the rest of the app's
interactive chrome had already settled on. Doc explorer rows instead
tinted with `--fg-default` at a few different alpha values, History rows
tinted with `--accent-blue`, and History's own action icons
(rename/favorite/delete) used yet a fourth recipe (`--fg-default` at a
different alpha again), all in the same file as the third.

Doc explorer (fields list, schema overview, and both search listboxes)
and History now hover with the same flat `--bg-subtle` background as
Collections, and History's action icons match too. `--accent-blue` is
reserved for genuine active/selected state: the active field row in doc
explorer, and the editable/rename row in History, both of which are
untouched.

Separately, only three files in the whole app had a hover `transition`,
each with its own duration (120ms, 100ms, 120ms/150ms), while nearly
everything else snapped instantly. Rather than pick a side, this PR adds
a single `--transition-fast: 120ms ease` token to `tokens.css` and
applies it to every row hover touched here, so the newly-unified hover
reads as one consistent, intentional motion instead of an arbitrary mix.
The three pre-existing transition durations elsewhere are unchanged for
now, out of scope for this pass.

## Test plan

- [x] Hover rows in the Doc Explorer (schema types, fields, search
results), History, and Collections panels; confirm all three show the
same subtle flat highlight, no blue or gray tint.
- [x] Select/activate a row (e.g. the currently-viewed field in the Doc
Explorer, or a row being renamed in History) and confirm it uses the
accent-blue treatment, clearly distinct from plain hover.
- [x] Confirm hover feels consistent across panels, including the small
action icons (rename/favorite/delete) on History rows.
- [x] Check both Dark and Light themes.

Refs: #4228
#4425)

## Summary

- Remove the composable `GraphiQL.Toolbar` and `GraphiQL.Logo` slots.
#4398 already stopped rendering their v5 defaults, leaving them in an
awkward half-fit spot with nowhere natural left to render; this finishes
the job by removing the slots themselves.
- `GraphiQL.Toolbar` is a straight removal, no deprecation period first.
Custom editor actions belong on a plugin's `sessionActions` now,
rendered into the tab strip alongside Prettify/Merge/Copy/Save.
- `GraphiQL.Logo` is removed now that `<TopBar>` takes a new `brand`
prop (any `ReactNode`), which is also exposed straight off `<GraphiQL>`.
It replaces the default hexagon icon + "GraphiQL" wordmark; leave it
unset to keep the default.
- `GraphiQL.Footer` is untouched.
- Also drops the dead
`toolbar.additionalContent`/`toolbar.additionalComponent` `TypeError`
guards, since they pointed at the component being removed here, and the
`.graphiql-toolbar`/`.graphiql-logo` CSS those slots used.
- Updates the README and the v6 migration guide with before/after
snippets, and migrates the `graphiql-vite-react-router` example off both
slots.

Builds on #4398.

## Test plan

- [x] Render `<GraphiQL>` with no extra props. The top bar shows the
default hexagon icon and "GraphiQL" wordmark.
- [x] Render `<GraphiQL brand="My Company">`. The top bar shows "My
Company" instead, and the default wordmark is gone.
- [x] Confirm the old `GraphiQL.Toolbar`/`GraphiQL.Logo` slots are gone:
referencing `<GraphiQL.Toolbar>` or `<GraphiQL.Logo>` as JSX children is
now a TypeScript error, and there's no runtime path that renders them.
- [x] Confirm toolbar actions still work from the tab strip: Prettify,
Merge Fragments, Copy, and Save each appear once and behave as before.
- [x] Register a plugin with a `sessionActions` component and confirm it
renders in the tab strip alongside the built-in actions.
- [x] Confirm `<GraphiQL.Footer>` still renders below the response,
unchanged.
- [x] Run the `graphiql-vite-react-router` example and confirm the
branding shows "API Explorer" and the share-query button still works
from the tab strip.

Refs: #4228 (builds on #4398)
## Summary

- `--font-size-body` is declared twice: as a scaling preset token in
`tokens.css` (`:root,
[data-font-size='default'|'compact'|'large'|'xl']`), and as a frozen v5
value in `root.css`'s DEPRECATED `.graphiql-container` block. Both
selectors have specificity (0,1,0) and the frozen block comes later
(after `@import 'tokens.css'`), so `calc(15rem / 16)` always won.
- Every consumer of `var(--font-size-body)` — the `.graphiql-container`
base font-size, `button`, `dialog`, `dropdown-menu`, `top-bar`, and the
collections / doc-explorer / query-builder plugins — has silently been
pinned to 15px, so Settings > Font size never touched body text even
though the code looked correct. The sibling preset tokens
(`--font-size-small`, `--font-size-mono`, `--font-size-eyebrow`) were
unaffected because they aren't redeclared in that block.
- Renaming the frozen declaration separates the two meanings and lets
the preset token win.

## Changes

- `root.css`: renamed `--font-size-body: calc(15rem / 16)` to
`--font-size-body-legacy` in the DEPRECATED `.graphiql-container` block.
- `graphiql-plugin-code-exporter/src/index.css`: pointed the
CodeMirror-based plugin at `var(--font-size-body-legacy)` so it keeps
rendering at the frozen 15px.
- Updated the DEPRECATED block comment to name the code-exporter plugin
— the explorer plugin it referenced no longer consumes this token.

## Validation Steps

1. Open the app and note the size of body text (panel headers, buttons,
top bar, doc explorer rows).
2. Open Settings > Font size and switch to **Large**, then **Extra
large**. Body text throughout the app should visibly grow at each step —
previously it stayed fixed at 15px regardless.
3. Switch to **Compact**. Body text should visibly shrink.
4. Switch back to **Default** and confirm body text settles at the
standard size (slightly smaller than before this change).
5. Open the code exporter plugin and cycle through the font-size
presets. Its exported-code panel should stay the same size at every
preset — it intentionally remains frozen.
…4428)

## Summary

`tokens.css` declared `--font-family: 'Inter'` and `--font-family-mono:
'JetBrains Mono'`, but nothing in the app ever rendered in either.
`root.css` redeclared both variables on `.graphiql-container`, pointing
them back at Roboto and Fira Code, and that selector beats `:root` in
the cascade. `packages/graphiql/src/style.css` only ever `@import`s the
Roboto and Fira Code `@font-face` files, so Inter and JetBrains Mono
weren't loaded to begin with — even if the cascade had gone the other
way, the browser would have fallen through to the generic fallbacks.

We decided in #4426 to keep Roboto and Fira Code rather than swap the
type system, so this points the canonical declaration in `tokens.css` at
the fonts that actually ship and drops the now-redundant
`.graphiql-container` override. One declaration, and it matches what you
see. No font loading changes.

## Test plan

- [x] Open the app and inspect the query editor and any body text.
Computed `font-family` resolves to Fira Code and Roboto respectively,
same as before this change.
- [x] In DevTools, confirm `--font-family` on `.graphiql-container` now
inherits from `:root` rather than being overridden locally, and that its
value is Roboto.
- [x] Switch themes and density presets and confirm no type regressions
anywhere in the app (top bar, status bar, doc explorer, history,
response pane).

Refs: #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