Repository: GitbookIO/gitbook
Pull request #3090: Improve OpenAPI codesample
Author: login: nolannbiron
Created: 2025-04-04T10:23:36Z | Merged: 2025-04-08T07:35:22Z



Diff:
diff --git a/.changeset/pink-students-grow.md b/.changeset/pink-students-grow.md
new file mode 100644
index 0000000000..b15a3f18b7
--- /dev/null
+++ b/.changeset/pink-students-grow.md
@@ -0,0 +1,6 @@
+---
+'@gitbook/react-openapi': patch
+'gitbook': patch
+---
+
+Improve OpenAPI codesample (add OpenAPISelect component)
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index e0b5488e68..1cc7c5f10e 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -90,7 +90,7 @@
 
 /* Method Tags */
 .openapi-method {
-    @apply rounded uppercase font-mono font-bold text-xs px-1 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex ring-1 ring-inset ring-tint-12/1 dark:ring-tint-1/1 whitespace-nowrap;
+    @apply rounded uppercase font-mono shrink-0 font-bold text-xs px-1 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex ring-1 ring-inset ring-tint-12/1 dark:ring-tint-1/1 whitespace-nowrap;
 }
 
 .openapi-method-get {
@@ -419,8 +419,20 @@
     @apply flex flex-row items-center;
 }
 
+.openapi-codesample-header .openapi-select > button {
+    @apply border-none;
+}
+
 .openapi-codesample-header-content {
-    @apply flex flex-row items-center h-fit;
+    @apply flex flex-row items-center justify-between h-fit p-2.5;
+}
+
+.openapi-codesample-header-content .openapi-path {
+    @apply flex items-center font-mono *:text-[0.813rem] gap-2 h-fit *:truncate overflow-x-auto min-w-0 max-w-full font-normal text-tint-strong;
+}
+
+.openapi-codesample-header-content .openapi-path .openapi-path-variable {
+    @apply text-[0.813rem];
 }
 
 .openapi-codesample-footer {
@@ -433,7 +445,7 @@
 
 /* Path */
 .openapi-path {
-    @apply flex items-start text-sm gap-2 h-fit overflow-x-auto min-w-0 max-w-full;
+    @apply flex items-center text-sm gap-2 h-fit overflow-x-auto min-w-0 max-w-full;
     scrollbar-width: none;
     -ms-overflow-style: none;
 }
@@ -447,12 +459,12 @@
 }
 
 .openapi-path .openapi-method {
-    @apply text-[0.813rem] m-0 mt-0.5 items-center flex px-2;
+    @apply text-[0.813rem] m-0 mt-0.5 items-center flex px-1;
 }
 
 .openapi-path-title {
     @apply flex-1 relative font-normal text-left font-mono text-tint-strong/10;
-    @apply py-0.5 px-1 rounded hover:bg-tint cursor-pointer transition-colors;
+    @apply py-0.5 px-1 rounded hover:bg-tint transition-colors;
     @apply whitespace-nowrap md:whitespace-normal;
 }
 
@@ -464,14 +476,6 @@
     display: none;
 }
 
-/* .openapi-path-copy {
-    @apply absolute opacity-0 h-fit right-0 top-1/2 -translate-y-1/2 bg-light dark:bg-dark border rounded-md border-tint-subtle px-1.5 py-0;
-}
-
-.openapi-path-title:hover .openapi-path-copy {
-    @apply opacity-11;
-} */
-
 .openapi-path-title em {
     @apply not-italic text-primary font-medium;
 }
@@ -498,7 +502,8 @@
     @apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
 }
 
-.openapi-panel-footer {
+.openapi-panel-footer,
+.openapi-codesample-footer {
     @apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
 }
 
@@ -513,7 +518,57 @@
 
 /* Common Elements */
 .openapi-select {
-    @apply max-w-60 rounded font-mono text-xs leading-6 px-1 py-0.5 truncate border border-tint-subtle bg-tint;
+    /* unstyled */
+}
+
+/* Prevent react-aria popover from setting overflow:auto on body */
+body:has(.openapi-select-popover) {
+    overflow: unset !important;
+}
+
+.openapi-select > button {
+    @apply flex items-center cursor-pointer gap-1.5 text-tint-strong max-w-60 rounded text-xs leading-6 px-1.5 truncate border border-tint-subtle bg-tint;
+    @apply hover:bg-tint-hover transition-all;
+}
+
+.openapi-select > button > span.react-aria-SelectValue {
+    @apply shrink truncate;
+}
+
+.openapi-select > button > .gb-icon {
+    @apply shrink-0;
+}
+
+.openapi-select > button svg {
+    @apply size-2.5;
+}
+
+.openapi-select-popover {
+    @apply min-w-32 max-w-fit w-auto max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md;
+    @apply shadow-md shadow-tint-12/1 dark:shadow-tint-1/1;
+}
+
+.openapi-select-popover[data-entering] {
+    animation: popover-enter 0.2s ease-in-out;
+}
+
+.openapi-select-popover[data-exiting] {
+    animation: popover-leave 0.2s ease-in-out;
+}
+
+.openapi-select-item {
+    @apply text-sm cursor-pointer px-1.5 py-0.5 truncate text-tint ring-0 border-none rounded !outline-none;
+    @apply hover:bg-tint-hover theme-gradient:hover:bg-tint-12/1 hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-inset contrast-more:hover:ring-current;
+}
+
+.openapi-select-item-selected {
+    @apply text-primary-subtle hover:text-primary hover:bg-primary-hover;
+    @apply theme-muted:hover:bg-primary-active theme-gradient:hover:bg-primary-active tint:font-semibold;
+    @apply contrast-more:text-primary contrast-more:hover:text-primary-strong contrast-more:font-semibold;
+}
+
+.openapi-select-listbox {
+    @apply flex flex-col gap-1 focus:ring-0 focus:outline-none;
 }
 
 .openapi-select:focus {
@@ -576,8 +631,10 @@
     @apply text-primary after:absolute after:-bottom-[calc(0.375rem_+_1px)] after:z-20 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all;
 }
 
-.openapi-tabs-panel {
+.openapi-tabs-panel,
+.openapi-codesample-panel {
     @apply flex-1 text-sm relative focus-visible:outline-none;
+    @apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
 }
 
 /* Disclosure group */
@@ -727,6 +784,32 @@
     }
 }
 
+@keyframes popover-enter {
+    0% {
+        opacity: 0;
+        transform: translateY(4px) scale(0.95);
+    }
+    100% {
+        opacity: 1;
+        transform: translateY(0) scale(1);
+    }
+}
+
+@keyframes popover-leave {
+    0% {
+        opacity: 1;
+        transform: translateY(0) scale(1);
+    }
+    100% {
+        opacity: 0;
+        transform: translateY(4px) scale(0.95);
+    }
+}
+
 .openapi-copy-button {
-    @apply hover:brightness-95;
+    @apply hover:brightness-95 cursor-pointer;
+}
+
+.openapi-copy-button[data-disabled="true"] {
+    @apply cursor-default;
 }
diff --git a/packages/react-openapi/src/InteractiveSection.tsx b/packages/react-openapi/src/InteractiveSection.tsx
index 255ff53f06..8ae60649c9 100644
--- a/packages/react-openapi/src/InteractiveSection.tsx
+++ b/packages/react-openapi/src/InteractiveSection.tsx
@@ -4,6 +4,7 @@ import clsx from 'clsx';
 import { useRef, useState } from 'react';
 import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
 import { useDisclosureState } from 'react-stately';
+import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
 import { Section, SectionBody, SectionHeader, SectionHeaderContent } from './StaticSection';
 
 interface InteractiveSectionTab {
@@ -106,24 +107,25 @@ export function InteractiveSection(props: {
                         }}
                     >
                         {tabs.length > 1 ? (
-                            <select
+                            <OpenAPISelect
                                 className={clsx(
                                     'openapi-section-select',
-                                    'openapi-select',
                                     `${className}-tabs-select`
                                 )}
-                                value={selectedTab?.key ?? ''}
-                                onChange={(event) => {
-                                    setSelectedTab(event.target.value);
+                                items={tabs}
+                                selectedKey={selectedTab?.key ?? ''}
+                                onSelectionChange={(key) => {
+                                    setSelectedTab(String(key));
                                     state.expand();
                                 }}
+                                placement="bottom end"
                             >
                                 {tabs.map((tab) => (
-                                    <option key={tab.key} value={tab.key}>
+                                    <OpenAPISelectItem key={tab.key} id={tab.key} value={tab}>
                                         {tab.label}
-                                    </option>
+                                    </OpenAPISelectItem>
                                 ))}
-                            </select>
+                            </OpenAPISelect>
                         ) : null}
                     </div>
                 </SectionHeader>
diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx
index 5e36d50bdb..9a99f970bc 100644
--- a/packages/react-openapi/src/OpenAPICodeSample.tsx
+++ b/packages/react-openapi/src/OpenAPICodeSample.tsx
@@ -3,15 +3,14 @@ import {
     OpenAPIMediaTypeExamplesBody,
     OpenAPIMediaTypeExamplesSelector,
 } from './OpenAPICodeSampleInteractive';
-import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
+import { OpenAPICodeSampleBody } from './OpenAPICodeSampleSelector';
 import { ScalarApiButton } from './ScalarApiButton';
-import { StaticSection } from './StaticSection';
 import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples';
 import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
 import { stringifyOpenAPI } from './stringifyOpenAPI';
 import type { OpenAPIContext, OpenAPIOperationData } from './types';
 import { getDefaultServerURL } from './util/server';
-import { checkIsReference, createStateKey } from './utils';
+import { checkIsReference } from './utils';
 
 const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const;
 
@@ -44,13 +43,7 @@ export function OpenAPICodeSample(props: {
         return null;
     }
 
-    return (
-        <OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
-            <StaticSection header={<OpenAPITabsList />} className="openapi-codesample">
-                <OpenAPITabsPanels />
-            </StaticSection>
-        </OpenAPITabs>
-    );
+    return <OpenAPICodeSampleBody data={data} items={samples} />;
 }
 
 /**
diff --git a/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx b/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx
index 17cf16a62a..6476cc42c8 100644
--- a/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx
+++ b/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx
@@ -3,7 +3,8 @@ import clsx from 'clsx';
 import { useCallback } from 'react';
 import { useStore } from 'zustand';
 import type { MediaTypeRenderer } from './OpenAPICodeSample';
-import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
+import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
+import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
 
 type MediaTypeState = {
     mediaType: string;
@@ -15,27 +16,27 @@ function useMediaTypeState(
     defaultKey: string
 ): MediaTypeState {
     const { method, path } = data;
-    const store = useStore(getOrCreateTabStoreByKey(`media-type-${method}-${path}`, defaultKey));
-    if (typeof store.tabKey !== 'string') {
+    const store = useStore(getOrCreateStoreByKey(`media-type-${method}-${path}`, defaultKey));
+    if (typeof store.key !== 'string') {
         throw new Error('Media type key is not a string');
     }
     return {
-        mediaType: store.tabKey,
-        setMediaType: useCallback((index: string) => store.setTabKey(index), [store.setTabKey]),
+        mediaType: store.key,
+        setMediaType: useCallback((index: string) => store.setKey(index), [store.setKey]),
     };
 }
 
 function useMediaTypeSampleIndexState(data: { method: string; path: string }, mediaType: string) {
     const { method, path } = data;
     const store = useStore(
-        getOrCreateTabStoreByKey(`media-type-sample-${mediaType}-${method}-${path}`, 0)
+        getOrCreateStoreByKey(`media-type-sample-${mediaType}-${method}-${path}`, 0)
     );
-    if (typeof store.tabKey !== 'number') {
+    if (typeof store.key !== 'number') {
         throw new Error('Example key is not a number');
     }
     return {
-        index: store.tabKey,
-        setIndex: useCallback((index: number) => store.setTabKey(index), [store.setTabKey]),
+        index: store.key,
+        setIndex: useCallback((index: number) => store.setKey(index), [store.setKey]),
     };
 }
 
@@ -69,18 +70,28 @@ function MediaTypeSelector(props: {
         return null;
     }
 
+    const items = renderers.map((renderer) => ({
+        key: renderer.mediaType,
+        label: renderer.mediaType,
+    }));
+
     return (
-        <select
+        <OpenAPISelect
             className={clsx('openapi-select')}
-            value={state.mediaType}
-            onChange={(e) => state.setMediaType(e.target.value)}
+            selectedKey={state.mediaType}
+            items={renderers.map((renderer) => ({
+                key: renderer.mediaType,
+                label: renderer.mediaType,
+            }))}
+            onSelectionChange={(e) => state.setMediaType(String(e))}
+            placement="bottom start"
         >
-            {renderers.map((renderer) => (
-                <option key={renderer.mediaType} value={renderer.mediaType}>
-                    {renderer.mediaType}
-                </option>
+            {items.map((item) => (
+                <OpenAPISelectItem key={item.key} id={item.key} value={item}>
+                    {item.label}
+                </OpenAPISelectItem>
             ))}
-        </select>
+        </OpenAPISelect>
     );
 }
 
@@ -95,18 +106,24 @@ function ExamplesSelector(props: {
         return null;
     }
 
+    const items = renderer.examples.map((example, index) => ({
+        key: index,
+        label: example.example.summary || `Example ${index + 1}`,
+    }));
+
     return (
-        <select
-            className={clsx('openapi-select')}
-            value={String(state.index)}
-            onChange={(e) => state.setIndex(Number(e.target.value))}
+        <OpenAPISelect
+            items={items}
+            selectedKey={state.index}
+            onSelectionChange={(e) => state.setIndex(Number(e))}
+            placement="bottom start"
         >
-            {renderer.examples.map((example, index) => (
-                <option key={index} value={index}>
-                    {example.example.summary || `Example ${index + 1}`}
-                </option>
+            {items.map((item) => (
+                <OpenAPISelectItem key={item.key} id={item.key} value={item}>
+                    {item.label}
+                </OpenAPISelectItem>
             ))}
-        </select>
+        </OpenAPISelect>
     );
 }
 
diff --git a/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx b/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx
new file mode 100644
index 0000000000..f3f18ffc96
--- /dev/null
+++ b/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx
@@ -0,0 +1,87 @@
+'use client';
+
+import { useCallback } from 'react';
+import type { Key } from 'react-aria';
+import { useStore } from 'zustand';
+import { OpenAPIPath } from './OpenAPIPath';
+import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
+import { StaticSection } from './StaticSection';
+import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
+import type { OpenAPIOperationData } from './types';
+
+function useCodeSampleState(initialKey: Key = 'default') {
+    const store = useStore(getOrCreateStoreByKey('codesample', initialKey));
+    return {
+        key: store.key,
+        setKey: useCallback((key: Key) => store.setKey(key), [store.setKey]),
+    };
+}
+
+type CodeSampleItem = OpenAPISelectItem & {
+    body: React.ReactNode;
+    footer?: React.ReactNode;
+};
+
+export function OpenAPICodeSampleHeader(props: {
+    items: CodeSampleItem[];
+    data: OpenAPIOperationData;
+}) {
+    const { data, items } = props;
+
+    const state = useCodeSampleState(items[0]?.key ?? '');
+    const selected = items.find((item) => item.key === state.key) || items[0];
+
+    return (
+        <>
+            <OpenAPIPath canCopy={false} withServer={false} data={data} />
+            {items.length > 1 ? (
+                <OpenAPISelect
+                    selectedKey={selected?.key}
+                    onSelectionChange={(key) => {
+                        state.setKey(key);
+                    }}
+                    items={items}
+                    placement="bottom end"
+                >
+                    {items.map((item) => (
+                        <OpenAPISelectItem key={item.key} id={item.key} value={item}>
+                            {item.label}
+                        </OpenAPISelectItem>
+                    ))}
+                </OpenAPISelect>
+            ) : items[0] ? (
+                <span className="openapi-codesample-label">{items[0].label}</span>
+            ) : null}
+        </>
+    );
+}
+
+export function OpenAPICodeSampleBody(props: {
+    items: CodeSampleItem[];
+    data: OpenAPIOperationData;
+}) {
+    const { items, data } = props;
+    if (!items[0]) {
+        throw new Error('No items provided');
+    }
+
+    const state = useCodeSampleState(items[0]?.key);
+
+    const selected = items.find((item) => item.key === state.key) || items[0];
+
+    if (!selected) {
+        return null;
+    }
+
+    return (
+        <StaticSection
+            header={<OpenAPICodeSampleHeader data={data} items={items} />}
+            className="openapi-codesample"
+        >
+            <div id={selected.key as string} className="openapi-codesample-panel">
+                {selected.body ? selected.body : null}
+                {selected.footer ? selected.footer : null}
+            </div>
+        </StaticSection>
+    );
+}
diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx
index 3d7a63eb65..f58375b1cf 100644
--- a/packages/react-openapi/src/OpenAPIOperation.tsx
+++ b/packages/react-openapi/src/OpenAPIOperation.tsx
@@ -47,7 +47,7 @@ export function OpenAPIOperation(props: {
                           title: operation.summary,
                       })
                     : null}
-                <OpenAPIPath data={data} context={context} />
+                <OpenAPIPath data={data} />
             </div>
             <div className="openapi-columns">
                 <div className="openapi-column-spec">
diff --git a/packages/react-openapi/src/OpenAPIPath.tsx b/packages/react-openapi/src/OpenAPIPath.tsx
index ddc6893fe0..aa5857a5d6 100644
--- a/packages/react-openapi/src/OpenAPIPath.tsx
+++ b/packages/react-openapi/src/OpenAPIPath.tsx
@@ -1,5 +1,5 @@
 import { OpenAPICopyButton } from './OpenAPICopyButton';
-import type { OpenAPIContext, OpenAPIOperationData } from './types';
+import type { OpenAPIOperationData } from './types';
 import { getDefaultServerURL } from './util/server';
 
 /**
@@ -7,25 +7,42 @@ import { getDefaultServerURL } from './util/server';
  */
 export function OpenAPIPath(props: {
     data: OpenAPIOperationData;
-    context: OpenAPIContext;
+    /** Whether to show the server URL.
+     * @default true
+     */
+    withServer?: boolean;
+    /**
+     * Whether the path is copyable.
+     * @default true
+     */
+    canCopy?: boolean;
 }) {
-    const { data } = props;
+    const { data, withServer = true, canCopy = true } = props;
     const { method, path, operation } = data;
 
     const server = getDefaultServerURL(data.servers);
     const formattedPath = formatPath(path);
 
+    const element = (() => {
+        return (
+            <>
+                {withServer ? <span className="openapi-path-server">{server}</span> : null}
+                {formattedPath}
+            </>
+        );
+    })();
+
     return (
         <div className="openapi-path">
             <div className={`openapi-method openapi-method-${method}`}>{method}</div>
 
             <OpenAPICopyButton
-                value={server + path}
+                value={`${withServer ? server : ''}${path}`}
                 className="openapi-path-title"
                 data-deprecated={operation.deprecated}
+                isDisabled={!canCopy}
             >
-                <span className="openapi-path-server">{server}</span>
-                {formattedPath}
+                {element}
             </OpenAPICopyButton>
         </div>
     );
diff --git a/packages/react-openapi/src/OpenAPISelect.tsx b/packages/react-openapi/src/OpenAPISelect.tsx
new file mode 100644
index 0000000000..14b83b77dd
--- /dev/null
+++ b/packages/react-openapi/src/OpenAPISelect.tsx
@@ -0,0 +1,70 @@
+'use client';
+
+import clsx from 'clsx';
+import {
+    Button,
+    type Key,
+    ListBox,
+    ListBoxItem,
+    type ListBoxItemProps,
+    Popover,
+    type PopoverProps,
+    Select,
+    type SelectProps,
+    SelectValue,
+} from 'react-aria-components';
+
+export type OpenAPISelectItem = {
+    key: Key;
+    label: string;
+};
+
+interface OpenAPISelectProps<T extends OpenAPISelectItem> extends Omit<SelectProps<T>, 'children'> {
+    items: T[];
+    children: React.ReactNode | ((item: T) => React.ReactNode);
+    selectedKey?: Key;
+    onChange?: (key: string | number) => void;
+    placement?: PopoverProps['placement'];
+}
+
+export function OpenAPISelect<T extends OpenAPISelectItem>(props: OpenAPISelectProps<T>) {
+    const { items, children, className, placement } = props;
+
+    return (
+        <Select {...props} className={clsx('openapi-select', className)}>
+            <Button>
+                <SelectValue />
+                <span aria-hidden="true">
+                    <svg
+                        className="gb-icon"
+                        style={{
+                            maskImage:
+                                "url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/chevron-down.svg?v=2&token=a463935e93')",
+                            maskRepeat: 'no-repeat',
+                            maskPosition: 'center center',
+                        }}
+                    />
+                </span>
+            </Button>
+            <Popover placement={placement} className="openapi-select-popover">
+                <ListBox className="openapi-select-listbox" items={items}>
+                    {children}
+                </ListBox>
+            </Popover>
+        </Select>
+    );
+}
+
+export function OpenAPISelectItem(props: ListBoxItemProps) {
+    return (
+        <ListBoxItem
+            {...props}
+            className={({ isFocused, isSelected }) =>
+                clsx('openapi-select-item', {
+                    'openapi-select-item-focused': isFocused,
+                    'openapi-select-item-selected': isSelected,
+                })
+            }
+        />
+    );
+}
diff --git a/packages/react-openapi/src/OpenAPITabs.tsx b/packages/react-openapi/src/OpenAPITabs.tsx
index 6f8eec0c34..f3502ba1dd 100644
--- a/packages/react-openapi/src/OpenAPITabs.tsx
+++ b/packages/react-openapi/src/OpenAPITabs.tsx
@@ -3,7 +3,7 @@
 import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
 import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
 import { useEventCallback } from 'usehooks-ts';
-import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
+import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
 
 export type TabItem = {
     key: Key;
@@ -36,8 +36,8 @@ export function OpenAPITabs(
     const { children, items, stateKey } = props;
     const [tabKey, setTabKey] = useState<Key | null>(() => {
         if (stateKey && typeof window !== 'undefined') {
-            const store = getOrCreateTabStoreByKey(stateKey);
-            const tabKey = store.getState().tabKey;
+            const store = getOrCreateStoreByKey(stateKey);
+            const tabKey = store.getState().key;
             if (tabKey) {
                 return tabKey;
             }
@@ -60,10 +60,10 @@ export function OpenAPITabs(
         if (!stateKey) {
             return undefined;
         }
-        const store = getOrCreateTabStoreByKey(stateKey);
+        const store = getOrCreateStoreByKey(stateKey);
         return store.subscribe((state) => {
             cancelDeferRef.current?.();
-            cancelDeferRef.current = defer(() => selectTab(state.tabKey));
+            cancelDeferRef.current = defer(() => selectTab(state.key));
         });
     }, [stateKey, selectTab]);
     useEffect(() => {
@@ -77,8 +77,8 @@ export function OpenAPITabs(
                 onSelectionChange={(tabKey) => {
                     selectTab(tabKey);
                     if (stateKey) {
-                        const store = getOrCreateTabStoreByKey(stateKey);
-                        store.setState({ tabKey });
+                        const store = getOrCreateStoreByKey(stateKey);
+                        store.setState({ key: tabKey });
                     }
                 }}
                 selectedKey={tabKey}
diff --git a/packages/react-openapi/src/getOrCreateStoreByKey.ts b/packages/react-openapi/src/getOrCreateStoreByKey.ts
new file mode 100644
index 0000000000..06f4cc44b3
--- /dev/null
+++ b/packages/react-openapi/src/getOrCreateStoreByKey.ts
@@ -0,0 +1,35 @@
+'use client';
+
+import { createStore } from 'zustand';
+
+type Key = string | number;
+
+type State = {
+    key: Key | null;
+};
+
+type Actions = { setKey: (key: Key | null) => void };
+
+type Store = State & Actions;
+
+const createStateStore = (initial?: Key) => {
+    return createStore<Store>()((set) => ({
+        key: initial ?? null,
+        setKey: (key) => {
+            set(() => ({ key }));
+        },
+    }));
+};
+
+const defaultStores = new Map<string, ReturnType<typeof createStateStore>>();
+
+const createStateStoreFactory = (stores: typeof defaultStores) => {
+    return (storeKey: string, initialKey?: Key) => {
+        if (!stores.has(storeKey)) {
+            stores.set(storeKey, createStateStore(initialKey));
+        }
+        return stores.get(storeKey)!;
+    };
+};
+
+export const getOrCreateStoreByKey = createStateStoreFactory(defaultStores);
diff --git a/packages/react-openapi/src/useSyncedTabsGlobalState.ts b/packages/react-openapi/src/useSyncedTabsGlobalState.ts
deleted file mode 100644
index 42a6c3a09d..0000000000
--- a/packages/react-openapi/src/useSyncedTabsGlobalState.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-'use client';
-
-import { createStore } from 'zustand';
-
-type Key = string | number;
-
-type TabState = {
-    tabKey: Key | null;
-};
-
-type TabActions = { setTabKey: (tab: Key | null) => void };
-
-type TabStore = TabState & TabActions;
-
-const createTabStore = (initialTab?: Key) => {
-    return createStore<TabStore>()((set) => ({
-        tabKey: initialTab ?? null,
-        setTabKey: (tabKey) => {
-            set(() => ({ tabKey }));
-        },
-    }));
-};
-
-const defaultTabStores = new Map<string, ReturnType<typeof createTabStore>>();
-
-const createTabStoreFactory = (stores: typeof defaultTabStores) => {
-    return (storeKey: string, initialKey?: Key) => {
-        if (!stores.has(storeKey)) {
-            stores.set(storeKey, createTabStore(initialKey));
-        }
-        return stores.get(storeKey)!;
-    };
-};
-
-export const getOrCreateTabStoreByKey = createTabStoreFactory(defaultTabStores);


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

<details>
<summary>Files not reviewed (1)</summary>

* **packages/gitbook/src/components/DocumentView/OpenAPI/style.css**: Language not supported
</details>



state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T15:36:00Z
user: login: gregberge
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T16:15:50Z

Review comments:
diff_hunk: @@ -1,31 +1,48 @@
 import { OpenAPICopyButton } from './OpenAPICopyButton';
-import type { OpenAPIContext, OpenAPIOperationData } from './types';
+import type { OpenAPIOperationData } from './types';
 import { getDefaultServerURL } from './util/server';
 
 /**
  * Display the path of an operation.
  */
 export function OpenAPIPath(props: {
     data: OpenAPIOperationData;
-    context: OpenAPIContext;
+    /** Wether to show the server URL.
path: packages/react-openapi/src/OpenAPIPath.tsx
user: login: Copilot
body: Correct the spelling of 'Wether' to 'Whether' in the documentation comment.
```suggestion
    /** Whether to show the server URL.
```
created_at: 2025-04-07T15:35:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,31 +1,48 @@
 import { OpenAPICopyButton } from './OpenAPICopyButton';
-import type { OpenAPIContext, OpenAPIOperationData } from './types';
+import type { OpenAPIOperationData } from './types';
 import { getDefaultServerURL } from './util/server';
 
 /**
  * Display the path of an operation.
  */
 export function OpenAPIPath(props: {
     data: OpenAPIOperationData;
-    context: OpenAPIContext;
+    /** Wether to show the server URL.
+     * @default true
+     */
+    withServer?: boolean;
+    /**
+     * Wether the path is copyable.
path: packages/react-openapi/src/OpenAPIPath.tsx
user: login: Copilot
body: Correct the spelling of 'Wether' to 'Whether' in the documentation comment.
```suggestion
     * Whether the path is copyable.
```
created_at: 2025-04-07T15:36:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-04-04T10:23:38Z
body: <p><a href="https://linear.app/gitbook-x/issue/RND-6446/improve-code-example-block">RND-6446 Improve code example block</a></p>
author_association: NONE
user: login: changeset-bot[bot]
created_at: 2025-04-04T10:23:39Z
body: ###  🦋  Changeset detected

Latest commit: f7a35f2430cc34438f3ad7296f6e4533e06fbb73

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

<details><summary>This PR includes changesets to release 2 packages</summary>

  | Name                   | Type  |
| ---------------------- | ----- |
| @gitbook/react-openapi | Patch |
| gitbook                | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/GitbookIO/gitbook/new/nolann/rnd-6446-improve-code-example-block?filename=.changeset/curly-socks-run.md&value=---%0A%22gitbook%22%3A%20patch%0A%22%40gitbook%2Freact-openapi%22%3A%20patch%0A---%0A%0AImprove%20OpenAPI%20codesample%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-04T10:27:51Z
body: Summary of the deployments:

### Version 1 (production)

| Version | URL | Status |
| --- | --- | --- |
| Latest commit | [https://116c4812.gitbook-open.pages.dev](https://116c4812.gitbook-open.pages.dev) | ✅ |
| PR | [https://pr3090.gitbook-open.pages.dev](https://pr3090.gitbook-open.pages.dev) | ✅ |

### Version 2 (experimental)

| Version | URL | Status |
| --- | --- | --- |
| Vercel | [https://gitbook-v2-928c6gdch-gitbook.vercel.app](https://gitbook-v2-928c6gdch-gitbook.vercel.app) | ✅ |
| Cloudflare | []() | ✅ |

### Test content

| Site | v1 | v2 |
| --- | --- | --- |
| GitBook | [https://116c4812.gitbook-open.pages.dev/docs.gitbook.com](https://116c4812.gitbook-open.pages.dev/docs.gitbook.com) | [https://gitbook-v2-928c6gdch-gitbook.vercel.app/url/docs.gitbook.com](https://gitbook-v2-928c6gdch-gitbook.vercel.app/url/docs.gitbook.com) |
| E2E | [https://116c4812.gitbook-open.pages.dev/gitbook.gitbook.io/test-gitbook-open](https://116c4812.gitbook-open.pages.dev/gitbook.gitbook.io/test-gitbook-open) | [https://gitbook-v2-928c6gdch-gitbook.vercel.app/url/gitbook.gitbook.io/test-gitbook-open](https://gitbook-v2-928c6gdch-gitbook.vercel.app/url/gitbook.gitbook.io/test-gitbook-open) |
author_association: CONTRIBUTOR
user: login: argos-ci[bot]
created_at: 2025-04-04T10:29:50Z
body: **The latest updates on your projects.** Learn more about [Argos notifications ↗︎](https://argos-ci.com/docs/notifications)

| Build | Status | Details | Updated (UTC) |
| :---- | :----- | :------ | :------------ |
| **customers-v1** ([Inspect](https://app.argos-ci.com/GitbookIO/gitbook/builds/6166)) | 👍 Changes approved | 4 changed | Apr 8, 2025, 7:27 AM |
| **customers-v2** ([Inspect](https://app.argos-ci.com/GitbookIO/gitbook/builds/6167)) | 👍 Changes approved | 4 changed | Apr 8, 2025, 7:27 AM |
| **default** ([Inspect](https://app.argos-ci.com/GitbookIO/gitbook/builds/6168)) | 👍 Changes approved | 10 changed | Apr 8, 2025, 7:28 AM |
| **v2-vercel** ([Inspect](https://app.argos-ci.com/GitbookIO/gitbook/builds/6170)) | 👍 Changes approved | 4 changed | Apr 8, 2025, 7:31 AM |
author_association: NONE