This guide is intended to help you get started with contributing to the project. By following these steps — which should take no more than 30 minutes —, you will understand the development process and workflow.
- Cloning the repository
- Installing Node.js and npm
- Installing dependencies
- Starting the development server
- Creating a component
- Creating the default example
- Styling the example
- Testing the example
- Writing another example
- Importing styles from other examples
- Promoting the component
- Updating the examples to import from
@ariakit/react
- Writing the component documentation
- Writing documentation for other examples
- Submitting a pull request
This guide covers more advanced topics. Pick the topics based on your needs.
To start contributing to the project, you have to fork this repository and clone it to your local machine:
git clone https://github.com/YOUR_USERNAME/ariakit.git
If you are already part of the organization on GitHub, clone the repository directly:
git clone https://github.com/ariakit/ariakit.git
Alternatively, you can open the project in Gitpod and skip to Creating a component.
This repository uses npm workspaces to manage multiple ESM projects. You need to install npm v7 or higher and Node.js v18 or higher.
You can run the following commands in your terminal to check your local Node.js and npm versions:
node -v
npm -v
If the versions are not correct or you don't have Node.js or npm installed, download them from https://nodejs.org.
Alternatively, you can use nvm to install the project's Node.js and npm versions. Once in the project's root directory, run the following command in your terminal:
nvm use
If you haven't installed the specific Node.js version yet,
nvm
will ask you to runnvm install
to install it. Follow the instructions in your terminal.
Once in the project's root directory, run the following command to install the project's dependencies:
npm install
After installing the project's dependencies, run the following command to start the development server:
npm run dev
Now open http://localhost:3000 in your browser to see the project's site.
To make a new component, create a file with the following contents:
packages/ariakit-react-core/src/my-component/my-component.tsx
import type { ElementType } from "react";
import { createElement, createHook, forwardRef } from "../utils/system.tsx";
import type { Options, Props } from "../utils/types.ts";
const TagName = "div" satisfies ElementType;
type TagName = typeof TagName;
/**
* Description for my component hook.
* @see https://ariakit.org/components/my-component
* @example
* ```jsx
* const props = useMyComponent();
* <Role {...props} />
* ```
*/
export const useMyComponent = createHook<TagName, MyComponentOptions>(
function useMyComponent({ customProp = "My component", ...props }) {
props = { children: customProp, ...props };
return props;
}
);
/**
* Description for my component.
* @see https://ariakit.org/components/my-component
* @example
* ```jsx
* <MyComponent />
* ```
*/
export const MyComponent = forwardRef(function MyComponent(
props: MyComponentProps,
) {
const htmlProps = useMyComponent(props);
return createElement(TagName, htmlProps);
});
export interface MyComponentOptions<_T extends ElementType = TagName>
extends Options {
/**
* Description for custom prop.
*/
customProp?: string;
};
export type MyComponentProps<T extends ElementType = TagName> = Props<
T,
MyComponentOptions<T>
>;
That's the basic structure for all components in the project. This will guarantee that the component will support all the library's core features. You can take a look at other components to see more complex examples.
The development workflow on this project is entirely based on examples. You can think of an example as a use case of a component. This will be used not only for development purposes, but also to show the component and its usage in the documentation.
Every component has a default example that receives the name of the component. This default example should be a common use case, but also simple enough so it requires as few custom props as possible.
Let's create a default example for our component:
examples/my-component/index.tsx
import { MyComponent } from "@ariakit/react-core/my-component/my-component";
export default function Example() {
return <MyComponent />;
}
Now open http://localhost:3000/examples/my-component to see the example in action.
When necessary, you can apply styles to the example. We're using Tailwind to keep the styles consistent throughout the project. You will find the theme configuration in the tailwind.config.cjs
file.
To use Tailwind in a CSS file rather than applying classes directly to the HTML elements, we're using the
@apply
directive.Make sure you also take dark mode into account.
examples/my-component/style.css
.my-component {
@apply
bg-red-600
text-white
dark:bg-red-800
}
Now we need to import the CSS file on the example's index.tsx
file and add the class name to the respective elements:
examples/my-component/index.tsx
import "./style.css";
import { MyComponent } from "@ariakit/react-core/my-component/my-component";
export default function Example() {
return <MyComponent className="my-component" />;
}
Now open http://localhost:3000/examples/my-component to see the example with the styles applied.
You'll notice that the transpiled CSS file has been also added to editor's files so people can easily edit it directly in the browser. You can also use it to see the output CSS while applying Tailwind classes.
One of the goals of having use cases written like that is so we can write automated tests for them. Instead of testing the Ariakit components directly, we're testing the examples that represent the way people use Ariakit components.
We use
@ariakit/test
, which is a wrapper around React Testing Library with some additional features to ensure that events like clicks and key presses work similarly to actual user events.
Let's create a test for our example:
examples/my-component/test.ts
import { q } from "@ariakit/test";
test("my component", () => {
expect(q.text("My component")).toBeInTheDocument();
});
Now run the following command in your terminal to watch the test results:
npm test watch my-component
A component may have multiple examples besides the default one. This is useful when you want to show a component in different contexts and props.
Conventionally, the example names are prefixed with the component name and a short suffix. For example:
my-component-custom-prop
.
Let's create another example for our component:
examples/my-component-custom-prop/index.tsx
import "./style.css";
import { MyComponent } from "@ariakit/react-core/my-component/my-component";
export default function Example() {
return <MyComponent className="my-component" customProp="Hello world" />;
}
We can @import
CSS files from other examples. You'll usually import the styles from the default example into the other examples so you don't need to repeat the same base styles.
examples/my-component-custom-prop/style.css
@import url("../my-component/style.css");
.my-component {
@apply
p-4
}
Now open http://localhost:3000/examples/my-component-custom-prop to see the example with the custom prop applied.
So far, we've been working with the @ariakit/react-core
package. This is where all the components — including experimental stuff — are developed. This package doesn't follow semver, so we can introduce breaking changes on patch and minor updates. Once a component is stable enough, it's promoted to the @ariakit/react
package.
To promote our component to the @ariakit/react
package, we need to create a file and re-export the component from the @ariakit/react-core
package:
packages/ariakit-react/src/my-component.ts
export { MyComponent } from "@ariakit/react-core/my-component/my-component";
export type {
MyComponentProps,
MyComponentOptions,
} from "@ariakit/react-core/my-component/my-component";
Finally, we must update the index.ts
file to export the component:
packages/ariakit-react/src/index.ts
// ...
export * from "./my-component.ts";
// ...
Now that we've promoted our component to the @ariakit/react
package, we need to update the import declarations on the examples:
examples/my-component/index.tsx
import "./style.css";
import * as Ariakit from "@ariakit/react";
export default function Example() {
return <Ariakit.MyComponent className="my-component" />;
}
examples/my-component-custom-prop/index.tsx
import "./style.css";
import * as Ariakit from "@ariakit/react";
export default function Example() {
return (
<Ariakit.MyComponent className="my-component" customProp="Hello world" />
);
}
Now that the component is part of the main library, we should write documentation for it.
We can create a markdown file in the components folder and render an anchor element pointing to the example's index file with a data-playground
attribute. This will turn the link into a playground.
components/my-component.md
# My component
<div data-description>
This is my component.
</div>
<a href="../examples/my-component/index.tsx" data-playground>Example</a>
Now open http://localhost:3000/components/my-component to see the component documentation.
Unlike default examples, other examples will be primarily accessed through their own URLs (for example: http://localhost:3000/examples/my-component-custom-prop). To write documentation for them, we can create a readme.md
file in the example's directory and follow the same convention as for the component's markdown file.
examples/my-component-custom-prop/readme.md
# My component with `customProp`
<div data-description>
This is my component using `customProp`.
</div>
<a href="./index.tsx" data-playground>Example</a>
Note that we're passing the `customProp` prop to the component:
```tsx
<MyComponent className="my-component" customProp="Hello world" />
```
Now open http://localhost:3000/examples/my-component-custom-prop to see the example documentation.
When you're ready to submit a pull request, you can follow these naming conventions:
- Pull request titles use the Imperative Mood (e.g.,
Add something
,Fix something
). - Changesets use past tense verbs (e.g.,
Added something
,Fixed something
).
When you submit a pull request, GitHub will automatically lint, build, and test your changes. If you see an ❌, it's most likely a bug in your code. Please, inspect the logs through the GitHub UI to find the cause.
When adding new features or fixing bugs, we'll need to bump the package versions. We use Changesets to do this.
The action of adding a new example doesn't require a version bump. Only changes to the codebase that affect the public API or existing behavior (e.g., bugs) do.
Let's craft a fresh changeset file for our component. You can use any name for the file, but it should begin with the pull request number, followed by a dash:
.changeset/1271-my-component.md
---
"@ariakit/react": minor
"@ariakit/react-core": patch
---
Added `MyComponent` component.
Once your pull request is merged into the main
branch, the Publish
PR will be automatically created/updated with the new changes. Once we merge this PR, the affected packages will be automatically published to npm and the changelog will be updated.
Ariakit supports both React 17 and React 18. If you want to see if your example works with React 17, you can run the following commands.
npm run test-react17
This command will automatically re-install React 18 at the end of the process. If, for some reason, this doesn't happen automatically, you should run npm i
in your terminal.
Most of the time, we'll write unit and integration tests as described on Testing the example. Those tests simulate real user interactions, but they don't run in the browser. They use JSDOM, which implements JavaScript DOM APIs in a Node.js environment.
Combined with the @ariakit/test
package, this is more than enough for most cases. However, sometimes we need a real browser to test specific interactions with our examples that aren't supported in JSDOM. For those cases, we use Playwright.
Let's create an end-to-end test for our example:
examples/my-component/test-chrome.ts
import { expect, test } from "@playwright/test";
test.beforeEach(async ({ page }) => {
await page.goto("/previews/my-component");
});
test("my component", async ({ page }) => {
const element = await page.getByText("My component");
await expect(element).toBeVisible();
});
Now run the following command in your terminal to see the test results (make sure to replace my-component
with the name of your example, or omit it to run all the tests):
Note: The development server must be running in another terminal instance.
npm run test-browser my-component
You can also run the tests in headed mode:
npm run test-browser-headed my-component
Or in debug mode:
npm run test-browser-debug my-component