Getting Started

Overview

h2o-library is the H2O design system: React components, charts, hooks, utilities, and types, published as one package with multiple subpath entries to keep your bundle lean.

This page covers installation, setup, and the import conventions that make it all work together.

Prerequisites

  • Node 18+
  • React 18+ (react and react-dom are peer dependencies)
  • A modern bundler that resolves package.json exports (Vite, Next.js, Webpack 5, esbuild, Rollup)

Installation

npm install h2o-library
# or
yarn add h2o-library
# or
pnpm add h2o-library

That's it. Everything the library needs at runtime, including the charting engine that powers the chart primitives, ships as a transitive dependency.

Stylesheets

The library ships pure CSS, no Tailwind required at the consumer side, but it integrates seamlessly with Tailwind. You only ever need to import one file: the design-tokens stylesheet. Component CSS is included automatically by your bundler.

// app/layout.tsx (Next.js) or src/main.tsx (Vite)
import "h2o-library/dist/styles/index.css"; // CSS custom-property tokens

dist/styles/index.css defines all design tokens as CSS custom properties (--primary, --destructive, --muted-200, …). Override them in your own :root { … } to re-theme, see Colors And Theming.

How component CSS gets picked up

Each component's source has an import "./component.css" statement that the ESM build preserves. The package sets "sideEffects": ["**/*.css"], which tells modern bundlers (Vite, Next.js 15+, Webpack 5, esbuild, Rollup) to keep those CSS imports and only ship the styles for components you actually render. No manual import per component needed.

Quick Start

import { Button, Input } from "h2o-library";

export default function App() {
  return (
    <form>
      <Input label="Name" placeholder="Jane Doe" />
      <Button label="Save" />
    </form>
  );
}

Import Paths

The package is split into five entries. Always import from the most specific entry that has what you need, bundlers tree-shake more effectively, and the type-only entry adds zero runtime cost.

EntryWhat's thereExample
h2o-libraryAll React components (Button, Modal, Tabs, Toast, …)import { Button } from "h2o-library"
h2o-library/chartsChart components (BarChart, LineChart, Chart, …)import { BarChart } from "h2o-library/charts"
h2o-library/hooksAll hooks (useToast, useDebounce, useMediaQuery, …)import { useToast } from "h2o-library/hooks"
h2o-library/utilsRuntime utilities (cn, …)import { cn } from "h2o-library/utils"
h2o-library/typesType-only exports (TabItem, IconType, TooltipPosition, …)import type { TabItem } from "h2o-library/types"

Code Splitting & Bundle Size

  • Charts are heavy. Putting them behind h2o-library/charts means apps that don't render charts pay zero cost, no chart engine in their bundle, no chart CSS.
  • Hooks come from /hooks for consistency. As the hook catalog grows, a single canonical path keeps imports uniform regardless of which hook you reach for. useToast lives here too, its matching ToastProvider stays at the root entry, but the React Context is shared at runtime so they wire up correctly.
  • Utils get their own entry because they're framework-agnostic helpers (cn and friends). Importable in Node scripts and tests without dragging React component code along.
  • Types-only entry keeps the value/type boundary explicit. Nothing from h2o-library/types ends up in your runtime bundle, it compiles away.

Enforce the Conventions with ESLint

The package only exposes hooks/utils through their dedicated entries, so importing useToast or cn from the root will fail at build time on its own. Types, however, can leak in through the root entry, import type { TabItem } from "h2o-library" will resolve silently. Add this rule to your ESLint config to catch the drift:

// eslint.config.js (flat config)
export default [
  {
    rules: {
      "no-restricted-syntax": [
        "error",
        {
          selector:
            "ImportDeclaration[importKind='type'][source.value='h2o-library']",
          message:
            "Import types from 'h2o-library/types', not the package root.",
        },
      ],
    },
  },
];
// .eslintrc.cjs (legacy config)
module.exports = {
  rules: {
    "no-restricted-syntax": [
      "error",
      {
        selector:
          "ImportDeclaration[importKind='type'][source.value='h2o-library']",
        message: "Import types from 'h2o-library/types', not the package root.",
      },
    ],
  },
};

If you also want to forbid the bare value-import shape (import { TabItem } from "h2o-library" without type), pair the rule above with @typescript-eslint/consistent-type-imports so type-only references always become import type first:

// requires @typescript-eslint
{
  rules: {
    "@typescript-eslint/consistent-type-imports": "error",
  },
}

With both rules on, any TabItem-style symbol imported from the root will either flip to import type (and then trip the first rule) or get caught directly.

TypeScript

Types are bundled with each subpath entry, no @types/h2o-library needed. Import value and type from their respective entries:

You can always refer to Types reference for the full catalog.

import { Tabs } from "h2o-library";
import type { TabItem } from "h2o-library/types";

const tabs: TabItem[] = [
  { label: "Overview", value: "overview" },
  { label: "Settings", value: "settings" },
];

Toast Setup

useToast is the only hook that needs a provider mounted higher in the tree. Wrap your app once:

// app/layout.tsx (Next.js)
import { ToastProvider } from "h2o-library";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        <ToastProvider>{children}</ToastProvider>
      </body>
    </html>
  );
}

Then call useToast() from any descendant. See the Toast docs for the full API.

Coding agents (Claude Code, Cursor)

The library ships with a curated reference skill for AI coding agents, SKILL.md plus topic files for components, tables, charts, hooks, utilities, and styling. Once installed, your agent will pull the right context automatically when you're editing files that import from h2o-library.

Claude Code

Drop the skill into your user-level skills directory so it's available across every project:

mkdir -p ~/.claude/skills && curl -fsSL https://h2o-library-documentation.vercel.app/skills/h2o-library.tar.gz | tar -xz -C ~/.claude/skills

To scope it to a single project instead, use .claude/skills/ at the project root:

mkdir -p .claude/skills && curl -fsSL https://h2o-library-documentation.vercel.app/skills/h2o-library.tar.gz | tar -xz -C .claude/skills

Claude Code reads the frontmatter in SKILL.md to decide when to load the skill, no further configuration needed.

Cursor

Cursor uses .mdc rule files in .cursor/rules/. The Cursor variant is shipped separately because the frontmatter shape differs (globs, alwaysApply). Install at the project root:

mkdir -p .cursor/rules && curl -fsSL https://h2o-library-documentation.vercel.app/skills/h2o-library-cursor.tar.gz | tar -xz -C .cursor/rules

The entry rule (h2o-library.mdc) auto-attaches when you're editing TypeScript / TSX / JavaScript / JSX files. The topic rules (components.mdc, tables.mdc, charts.mdc, hooks.mdc, utilities.mdc, styling-and-icons.mdc) are agent-requested, Cursor pulls them in based on each rule's description when relevant.

The skill source is also browseable directly at /skills/h2o-library/SKILL.md if you'd rather inspect it before installing, or hand-pick which files to copy.