{"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"CodeWalkthrough","attributes":{"filters":{},"filesets":[{"files":[{"path":"docs/realm/customization/eject-components/eject-components-tutorial/code-walkthrough-files/tutorial.md","content":[{"start":0,"condition":{"steps":["tutorial-code-snippet"]},"children":["# Tutorial","","## Declare a variable","",{"start":5,"condition":{"steps":["add-syncId-attribute"]},"children":["{% tabs syncId=\"programming-language\" %}"]},"  {% tab label=\"JavaScript\" %}","    ```js","    const arr = [];","    ```","  {% /tab %}","  {% tab label=\"TypeScript\" %}","    ```ts","    const arr: number[] = [];","    ```","  {% /tab %}","{% /tabs %}","","## Define an async function","",{"start":22,"condition":{"steps":["add-syncId-attribute"]},"children":["{% tabs syncId=\"programming-language\" %}"]},"  {% tab label=\"JavaScript\" %}","    ```js","    async function promisify(arg) {","      return arg;","    }","    ```","  {% /tab %}","  {% tab label=\"TypeScript\" %}","    ```ts","    async function promisify<T>(arg: T): Promise<T> {","      return arg;","    }","    ```","  {% /tab %}","{% /tabs %}"]},""],"metadata":{"steps":["tutorial-code-snippet","add-syncId-attribute","add-syncId-attribute"]},"basename":"tutorial.md","language":"markdoc"},{"path":"docs/realm/customization/eject-components/eject-components-tutorial/code-walkthrough-files/useTabs.ts","content":[{"start":0,"condition":{"steps":["implement-useTabs-hook"]},"children":["import { useEffect } from 'react';","// https://www.npmjs.com/package/@redocly/theme?activeTab=code","import { useTabs as useOriginalTabs } from '@redocly/theme/core/hooks';","","type UseTabsProps = {","  initialTab: string;","  totalTabs: number;","  syncId?: string;","};","","export function useTabs({ initialTab, totalTabs, syncId }: UseTabsProps) {","  const localStorageKey = `tabState-${syncId}`;","","  // Use the original useTabs hook to handle the basic tab functionality","  const { activeTab, setActiveTab, ...rest } = useOriginalTabs({","    // Sync the tab state on initial mount","    initialTab: window.localStorage.getItem(localStorageKey) || initialTab,","    totalTabs,","  });","","  // Sync the tab state to localStorage whenever activeTab changes","  useEffect(() => {","    if (syncId && activeTab) {","      window.localStorage.setItem(localStorageKey, activeTab);","","      window.dispatchEvent(","        new StorageEvent('storage', {","          key: localStorageKey,","          newValue: activeTab,","        }),","      );","    }","  }, [syncId, activeTab]);","","  // Listen to storage events and update the active tab","  useEffect(() => {","    if (syncId) {","      const handleStorageChange = (event: StorageEvent) => {","        if (event.key === localStorageKey && event.newValue) {","          setActiveTab(event.newValue);","        }","      };","","      window.addEventListener('storage', handleStorageChange);","","      return () => {","        window.removeEventListener('storage', handleStorageChange);","      };","    }","  }, [syncId, setActiveTab]);","","  return { activeTab, setActiveTab, ...rest };","}"]},""],"metadata":{"steps":["implement-useTabs-hook"]},"basename":"useTabs.ts","language":"typescript"},{"path":"docs/realm/customization/eject-components/eject-components-tutorial/code-walkthrough-files/Tabs.tsx","content":["import React from 'react';","import styled from 'styled-components';","","import { Tab } from '@redocly/theme/markdoc/components/Tabs/Tab';",{"start":4,"condition":{"steps":["replace-useTabs-hook-import"]},"children":["import { useTabs } from './useTabs';"]},"","type Child = { props: { label: string } & TabsProps };",{"start":9,"condition":{"steps":["extend-TabsProps-interface"]},"children":["type TabsProps = React.PropsWithChildren<{","  children: Child[];","  className?: string;","  syncId?: string;","}>;"]},"",{"start":17,"condition":{"steps":["add-syncId-prop"]},"children":["export function Tabs({ children, className, syncId }: TabsProps): JSX.Element {"]},"  const childrenArray = React.Children.toArray(children) as React.ReactElement[];","","  const initialTab = childrenArray[0]?.props.label ?? '';",{"start":23,"condition":{"steps":["pass-syncId-to-useTabs-hook"]},"children":["  const { activeTab, setTabRef, onTabClick, handleKeyboard, getTabId } = useTabs({","    initialTab,","    totalTabs: childrenArray.length,","    syncId,","  });"]},"","  return (","    <TabsContainer data-component-name=\"Markdoc/Tabs/Tabs\" className={className}>","      <TabList role=\"tablist\">","        {childrenArray.map((child, index) => {","          const { label, onClick } = child.props;","          const tabId = getTabId(label, index);","          return (","            <Tab","              key={`key-${tabId}`}","              tabId={tabId}","              label={label}","              isActive={activeTab === label}","              setRef={(el: HTMLButtonElement | null) => setTabRef(el, index)}","              onKeyDown={(event) => handleKeyboard(event, index)}","              onClick={() => {","                onTabClick(label);","                onClick?.();","              }}","            />","          );","        })}","      </TabList>","      {childrenArray.map((child, index) => {","        const { label } = child.props;","        const tabId = getTabId(label, index);","        return label === activeTab ? (","          <TabContent","            key={`content-${tabId}`}","            id={`panel-${tabId}`}","            aria-labelledby={`tab-${tabId}`}","            tabIndex={0}","            role=\"tabpanel\"","          >","            {child.props.children}","          </TabContent>","        ) : null;","      })}","    </TabsContainer>","  );","}","","const TabsContainer = styled.div`","  color: var(--md-tabs-container-text-color);","  font-size: var(--md-tabs-container-font-size);","  font-family: var(--md-tabs-container-font-family);","  font-style: var(--md-tabs-container-font-style);","  font-weight: var(--md-tabs-container-font-weight);","  background-color: var(--md-tabs-container-bg-color);","  margin: var(--md-tabs-container-margin);","  padding: var(--md-tabs-container-padding);","  border: var(--md-tabs-container-border);","","  ol[class^='Tabs__TabList'] {","    margin: 0;","    padding: 0;","  }","`;","","export const TabList = styled.ul`","  && {","    padding: var(--md-tabs-padding);","    margin-block-end: 0;","    border: var(--md-tabs-border);","    border-width: var(--md-tabs-border-width);","    margin: 0;","","    & > li:first-child {","      margin-bottom: 0px;","    }","","    li[class^='Tab__TabItem'] {","      margin: var(--md-tabs-tab-margin);","    }","  }","`;","","export const TabContent = styled.div`","  color: var(--md-tabs-content-text-color);","  font-size: var(--md-tabs-content-font-size);","  font-family: var(--md-tabs-content-font-family);","  font-style: var(--md-tabs-content-font-style);","  font-weight: var(--md-tabs-content-font-weight);","  background-color: var(--md-tabs-content-bg-color);","  margin: var(--md-tabs-content-margin);","  padding: var(--md-tabs-content-padding);","  border: var(--md-tabs-content-border);","`;",""],"metadata":{"steps":["replace-useTabs-hook-import","extend-TabsProps-interface","add-syncId-prop","pass-syncId-to-useTabs-hook"]},"basename":"Tabs.tsx","language":"tsx"},{"path":"docs/realm/customization/eject-components/eject-components-tutorial/code-walkthrough-files/tabs.ts","content":["import markdoc from '@markdoc/markdoc';","","import type { MarkdocSchemaWrapper } from '@redocly/theme/markdoc/tags/types';","","import { isTag } from '@redocly/theme/core/utils';","","export const tabs: MarkdocSchemaWrapper = {","  schema: {",{"start":8,"condition":{"steps":["add-syncId-markdoc-attribute"]},"children":["    attributes: {","      syncId: { type: String },","    },"]},"    transform(node, config) {","      const attributes = node.transformAttributes(config);","      const children = node.transformChildren(config);","","      const tabsContent = children.map((child) => {","        if (isTag(child) && child?.name === 'CodeBlock') {","          const key = child.attributes['data-language'] || 'default';","          const label = child.attributes['data-label'] || key;","          return new markdoc.Tag('div', { label, key }, [child]);","        }","        return child;","      });","","      return new markdoc.Tag('Tabs', attributes, tabsContent);","    },","  },","  tagName: 'tabs',","};",""],"metadata":{"steps":["add-syncId-markdoc-attribute"]},"basename":"tabs.ts","language":"typescript"},{"path":"docs/realm/customization/eject-components/eject-components-tutorial/code-walkthrough-files/schema.ts","content":[{"start":0,"condition":{"steps":["add-custom-schema"]},"children":["import { tabs } from './tags/tabs';","","export const tags = {","  [tabs.tagName]: tabs.schema,","};"]},""],"metadata":{"steps":["add-custom-schema"]},"basename":"schema.ts","language":"typescript"}],"downloadAssociatedFiles":[]}],"steps":[{"id":"implement-useTabs-hook"},{"id":"replace-useTabs-hook-import","heading":"Replace useTabs hook import"},{"id":"extend-TabsProps-interface","heading":"Extend TabsProps interface"},{"id":"add-syncId-prop","heading":"Add syncId prop to the Tabs component"},{"id":"pass-syncId-to-useTabs-hook","heading":"Pass syncId to the useTabs hook"},{"id":"add-syncId-markdoc-attribute","heading":"Add syncId markdoc attribute"},{"id":"add-custom-schema","heading":"Fix \"invalid attribute: 'syncId'\" warning"},{"id":"tutorial-code-snippet","heading":"Create tutorial page"},{"id":"add-syncId-attribute","heading":"Add syncId attribute"}],"inputs":{},"toggles":{}},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"eject-components-tutorial","__idx":0},"children":["Eject components tutorial"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"update-tabs-component-behavior","__idx":1},"children":["Update tabs component behavior"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Let's imagine you are writing a tutorial describing how to write code to work with your product."," ","You decided to use Redocly's ",{"$$mdtype":"Tag","name":"Link","attributes":{"href":"https://redocly.com/learn/markdoc/tags/tabs"},"children":["tabs"]}," Markdoc tag to show code examples in different languages."," ","As you are working, you decide it would be nice if a user could select their preferred programming language once."," ","That selection would update all other tabs on the page to the selected programming language."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["But you notice, the Redocly ",{"$$mdtype":"Tag","name":"Link","attributes":{"href":"https://redocly.com/learn/markdoc/tags/tabs"},"children":["tabs"]}," Markdoc tag, does not have the ability to configure syncing between tabs."," ","So you decide to eject the component to update the behavior to allow configuring syncing between tabs."]},{"$$mdtype":"Tag","name":"Image","attributes":{"src":"/content-assets/tabs-without-syncing-6a521ac709d03ba9.png","alt":"Tabs without syncing","framed":true,"withLightbox":true,"height":200,"align":"center"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"eject-the-tabs-component-in-reunite","__idx":2},"children":["Eject the tabs component in Reunite"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For this tutorial we are going to eject the component using the ",{"$$mdtype":"Tag","name":"Link","attributes":{"href":"/docs/realm/customization/eject-components/eject-components-in-reunite"},"children":["Reunite editor"]},"."," ","To eject the component using the Reunite editor:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Select ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Editor"]}," on the left side navigation."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Click the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Theme components"]}," in the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Files"]}," section."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Search for ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["tabs"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Find the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Tabs.tsx"]}," component and click the eject icon."]}]},{"$$mdtype":"Tag","name":"Image","attributes":{"src":"/content-assets/eject-tabs-component-05f0e9e0002efb4b.png","alt":"Eject tabs component","framed":true,"withLightbox":true,"height":400},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["After ejecting, the component is added to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@theme"]}," folder at the path ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@theme/markdoc/components/Tabs/Tabs.tsx"]}," where we can modify it."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"implement-usetabs-hook","__idx":3},"children":["Implement ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["useTabs"]}," hook"]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"implement-useTabs-hook"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create a new ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["useTabs.ts"]}," file adjacent to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Tabs.tsx"]}," file and copy the code from the example."," ","You can also download all examples and drag and drop them to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@theme/markdoc/components/Tabs"]}," folder."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This custom hook reuses the original ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["useTabs"]}," hook and adds syncing functionality between tabs with the same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["syncId"]},"."," ","It stores the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["activeTab"]}," state in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["localStorage"]}," after an update was triggered."," ","All other tabs are subscribed to the storage events to update their state."," ","Also, for initial mount, it gets the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["activeTab"]}," from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["localStorage"]}," to preserve the tabs state even after a page is reloaded."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info","name":"Prerequisites"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This tutorial does not provide step-by-step instructions for every customization."," ","Each case is unique and requires a different approach."," ","React, Markdoc and TypeScript knowledge is required to customize it."]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"replace-the-usetabs-hook-inside-the-tabs-component","__idx":4},"children":["Replace the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["useTabs"]}," hook inside the Tabs component"]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"replace-useTabs-hook-import","heading":"Replace useTabs hook import"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Replace the original ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["useTabs"]}," hook import with the custom hook."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"extend-TabsProps-interface","heading":"Extend TabsProps interface"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Extend the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["TabsProps"]}," interface to include the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["syncId"]}," prop."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"add-syncId-prop","heading":"Add syncId prop to the Tabs component"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Add the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["syncId"]}," prop to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Tabs"]}," component."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"pass-syncId-to-useTabs-hook","heading":"Pass syncId to the useTabs hook"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["And finally, pass the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["syncId"]}," prop to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["useTabs"]}," hook."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"extend-the-tabs-markdoc-schema","__idx":5},"children":["Extend the tabs Markdoc schema"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We also need to update the tabs interface to be able to pass the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["syncId"]}," prop to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["tabs"]}," Markdoc component."," ","That component passes the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["syncId"]}," to our modified ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Tabs"]}," React component."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"eject-the-tags-markdoc-schema","__idx":6},"children":["Eject the tags Markdoc schema"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Select ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Editor"]}," on the left side navigation."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Click the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Theme components"]}," in the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Files"]}," section."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Search for ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["tabs"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Find the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["tabs.ts"]}," tag and click the eject icon."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"add-syncId-markdoc-attribute","heading":"Add syncId markdoc attribute"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Add the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["syncId"]}," Markdoc attribute to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["tabs"]}," tag."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"add-custom-schema","heading":"Fix \"invalid attribute: 'syncId'\" warning"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To fix a warning, let's redefine our Markdoc schema."," ","Create a new ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["schema.ts"]}," inside the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@theme/markdoc"]}," folder and copy the code from the example."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"create-a-tutorial-page","__idx":7},"children":["Create a tutorial page"]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"tutorial-code-snippet","heading":"Create tutorial page"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To test our changes, let's create a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["tutorial.md"]}," page in the root of the project and copy the code from the example."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Your file structure should look like this:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"treeview","data-title":"Example of ejected components and a tutorial page","header":{"title":"Example of ejected components and a tutorial page","controls":{"copy":{}}},"source":"├── @theme/\n│   └── markdoc/\n│       ├── components/\n│       │   └── Tabs/\n│       │       ├── Tabs.tsx\n│       │       └── useTabs.ts\n│       ├── tags/\n│       │   └── tabs.ts\n│       └── schema.ts\n└── tutorial.md\n","lang":"treeview"},"children":[]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"add-syncId-attribute","heading":"Add syncId attribute"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Since we added ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["syncId"]}," attributes to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["tabs"]}," tags, we can sync the tabs state between those tabs with the same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["syncId"]},"."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Test it out."," ","Do the tabs sync?"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"-congratulations","__idx":8},"children":["🎉 Congratulations"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You have successfully ejected the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Tabs"]}," component and added custom functionality to it."," ","Don't forget, with great power comes great responsibility, now you are responsible for maintaining the component 😉."]}]}]},"frontmatter":{"products":["Revel","Realm"],"plans":["Pro","Enterprise","Enterprise+"]},"tagList":["admonition","code-walkthrough","img","step"],"title":"Eject components tutorial","lastModified":"2026-09-17T13:26:51.000Z"}