You can already embed your stories within zeroheight documentation to give them increased context. This is perfect once your stories have been published and are not undergoing large amounts of development still. But what about when your components are still being worked on? Wouldn’t it be great if you could have the documentation served up next to the components directly in Storybook? That way, while they’re being created it’s easier for developers to easily refer back to the guidelines around how the component should look or be interacted with.
This brings me to an interesting use of the zeroheight API: pulling content from a page and displaying it in a locally run version of Storybook.

How do you create such a thing?
This can be achieved by writing a custom add-on for Storybook that uses minimal config to interact with the the GET page endpoint and surface documentation right where you need it.
Setting up zeroheight and Storybook
First thing you need to do is fork the example repository by using the GitHub template to get the base set up. We are then going to work on creating this add-on as a “Tab” as I found that interface was the best for displaying larger amounts of documentation.
Head over to src/constants.ts
and update the value of KEY
from my-addon
to zeroheight
. This is the name you will reference in the parameters of your stories to define what page in zeroheight should be displayed for the component. For example:
import type { Meta, StoryObj } from "@storybook/react";
import { Header } from "./Header";
const meta: Meta<typeof Header> = {
title: "Example/Header",
component: Header,
parameters: {
layout: "fullscreen",
// This is our new parameter and should be a link to the page you want to show documentation for
zeroheight: "https://zeroheight.com/0849fc5f0/v/latest/p/1141e3-header",
},
};
export default meta;
type Story = StoryObj<typeof Header>;
export const LoggedIn: Story = {
args: {
user: {
name: "Jane Doe",
},
},
};
example-component.stories.ts
As we’re going to build a Tab add-on, the rest of the changes we are going to make will be in src/components/Tab.tsx
.
In here, we will want to use this new parameter we have just set to call the zeroheight API and get the page content.
const zeroheightUrl = useParameter<string>(KEY);
async function loadContent() {
const headers = {
Accept: "application/json",
"X-API-KEY": process.env.ZH_ACCESS_TOKEN,
"X-API-CLIENT": process.env.ZH_CLIENT_ID,
};
const pageId = zeroheightUrl.split("/p/")[1].split("-")[0];
try {
const response = await fetch(
`https://zeroheight.com/open_api/v2/pages/${pageId}?format=markdown`,
{
method: "GET",
headers: headers,
},
);
if (response.ok) {
const resp = await response.json();
}
} catch (e) {
console.error(e);
}
}
src/components/Tab.tsx
We can then implement a useEffect
to call the load function when the add-on is clicked in the browser.
React.useEffect(() => {
if (zeroheightUrl) loadContent();
}, [zeroheightUrl]);
src/components/Tab.tsx
As you can see, we need an API key and Client ID to call the zeroheight API, these can be created in your organization settings. This is also where a small caveat comes in. As the environment variables will be accessible client-side, this add-on should only be used for local development rather than on a public site so that your API details won’t be leaked. More information on this can be found in this Storybook article.
Now we have page content being returned from the zeroheight API we want to show this in the Add-on. For this we can use React’s state hooks:
const [pageTitle, setPageTitle] = React.useState("");
const [pageIntro, setPageIntro] = React.useState("");
const [pageContent, setPageContent] = React.useState("");
src/components/Tab.tsx
Then after getting the response from the zeroheight API, we can store the data in each of these pieces of state:
setPageTitle(resp.data.page.name);
setPageIntro(resp.data.page.introduction);
setPageContent(resp.data.page.content);
src/components/Tab.tsx
We can then take all this information and show it to the end-user on the page. You will need to install react-markdown to render the page content nicely.
<TabWrapper>
<TabInner>
<H1>zeroheight documentation</H1>
<div>
{pageTitle && <H2>{pageTitle}</H2>}
{pageIntro && <H4>{pageIntro}</H4>}
{pageContent && (
<ContentContainer>
<ReactMarkdown>{pageContent}</ReactMarkdown>
</ContentContainer>
)}
<P>
See more in{" "}
<A
href={zeroheightUrl}
target="_blank"
rel="noopener noreferrer"
color="#f63e7c"
>
zeroheight
</A>
</P>
</div>
</TabInner>
</TabWrapper>
src/components/Tab.tsx
This can all come together with a nicer loading state and erroring handling using the full snippet below.
import React from "react";
import ReactMarkdown from "react-markdown";
import { A, H1, H2, H4, P } from "storybook/internal/components";
import { useParameter } from "storybook/internal/manager-api";
import { styled } from "storybook/internal/theming";
import { KEY } from "../constants";
interface TabProps {
active: boolean;
}
const TabWrapper = styled.div(({ theme }) => ({
background: theme.background.content,
padding: "4rem 20px",
minHeight: "100vh",
minWidth: 808,
boxSizing: "border-box",
}));
const TabInner = styled.div({
maxWidth: 768,
marginLeft: "auto",
marginRight: "auto",
});
const LoadingSpinner = styled.div`
display: block;
box-sizing: border-box;
width: 40px;
height: 40px;
margin: auto;
margin-top: 40px;
border: 5px solid white;
border-bottom-color: #f63e7c;
border-radius: 50%;
animation: rotation 1s linear infinite;
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`;
const ContentContainer = styled.div`
max-height: 800px;
overflow-y: auto;
`;
enum Status {
loading,
success,
error,
}
export const Tab: React.FC<TabProps> = ({ active }) => {
const zeroheightUrl = useParameter<string>(KEY);
const [loadingStatus, setLoadingStatus] = React.useState(Status.loading);
const [errorMessage, setErrorMessage] = React.useState("Unknown error");
const [pageTitle, setPageTitle] = React.useState("");
const [pageIntro, setPageIntro] = React.useState("");
const [pageContent, setPageContent] = React.useState("");
async function loadContent() {
if (!process.env.ZH_ACCESS_TOKEN || !process.env.ZH_CLIENT_ID) {
setErrorMessage(
"Ensure you have your zeroheight API credentials set up in your environment",
);
setLoadingStatus(Status.error);
return;
}
const headers = {
Accept: "application/json",
"X-API-KEY": process.env.ZH_ACCESS_TOKEN,
"X-API-CLIENT": process.env.ZH_CLIENT_ID,
};
const pageId = zeroheightUrl.split("/p/")[1].split("-")[0];
try {
const response = await fetch(
`https://zeroheight.com/open_api/v2/pages/${pageId}?format=markdown`,
{
method: "GET",
headers: headers,
},
);
if (response.ok) {
setLoadingStatus(Status.success);
const resp = await response.json();
setPageTitle(resp.data.page.name);
setPageIntro(resp.data.page.introduction);
setPageContent(resp.data.page.content);
} else {
if (response.status === 401) {
setErrorMessage("Unauthorized. Check the credentials you're using");
} else if (response.status === 404) {
setErrorMessage("Page not found");
} else {
setErrorMessage(`Error ${response.status}: ${response.statusText}`);
}
setLoadingStatus(Status.error);
}
} catch (e) {
console.error(e);
setErrorMessage("Unknown error. Check console for more information");
setLoadingStatus(Status.error);
}
}
React.useEffect(() => {
if (zeroheightUrl) loadContent();
}, [zeroheightUrl]);
if (!active) {
return null;
}
return (
<TabWrapper>
<TabInner>
<H1>zeroheight documentation</H1>
{loadingStatus === Status.loading && <LoadingSpinner />}
{loadingStatus === Status.success && (
<div>
{pageTitle && <H2>{pageTitle}</H2>}
{pageIntro && <H4>{pageIntro}</H4>}
{pageContent && (
<ContentContainer>
<ReactMarkdown>{pageContent}</ReactMarkdown>
</ContentContainer>
)}
<P>
See more in{" "}
<A
href={zeroheightUrl}
target="_blank"
rel="noopener noreferrer"
color="#f63e7c"
>
zeroheight
</A>
</P>
</div>
)}
{loadingStatus === Status.error && <P>{errorMessage}</P>}
</TabInner>
</TabWrapper>
);
};
src/components/Tab.tsx
You can then publish this as a private package for your team to install into your Storybook setup and use it locally to help develop components that follow the existing guidelines.
Once your stories are created and ready to be shared with others, you can then embed them into zeroheight so the documentation and stories live side by side!
The Storybook addon is now available on NPM