# Island Patterns

This document covers when and how to use islands within component directories. Islands enable client-side interactivity (state, event handlers, browser APIs) while keeping the component's public API identical to non-island components.

## When to Use an Island

Create an island when a component requires **client-side interactivity** — anything that needs JavaScript to run in the browser after the initial server render:

- State management (`useState`, `useReducer`)
- Event handlers (`onClick`, `onSubmit`, `onChange`)
- Browser APIs (`window`, `document`, DOM measurements)
- Effects (`useEffect`, `useLayoutEffect`)

**Examples in this library:** LanguageSwitcher (drawer toggle), Form (form submission), NavigationMenu and VerticalMenu (menu interactions).

If a component is purely presentational (no state, no handlers), it does **not** need an island.

## File Structure

```
ComponentName/
├── index.tsx                    # Wrapper — imports island, renders <Island module={...} />
├── index.module.scss            # Styles (may be shared with island — see Key details below)
├── ContentFields.tsx            # Content field definitions
├── StyleFields.tsx              # Style field definitions
├── types.ts                     # TypeScript type definitions
├── islands/
│   └── ComponentNameIsland.tsx  # Interactive implementation (default export)
└── stories/
    └── ComponentName.stories.tsx
```

The `islands/` subdirectory lives inside the component directory. The island file is named `ComponentNameIsland.tsx`.

## The Wrapper (`index.tsx`)

The wrapper is the component's public entry point. It imports the island, wraps it in the `<Island>` component from `@hubspot/cms-components`, and attaches field components via the standard compound component pattern.

```typescript
// @ts-expect-error -- ?island not typed
import ComponentNameIsland from './islands/ComponentNameIsland.js?island';
import { Island } from '@hubspot/cms-components';
import ContentFields from './ContentFields.js';
import StyleFields from './StyleFields.js';
import type { ComponentNameProps } from './types.js';

const ComponentNameComponent = (props: ComponentNameProps) => {
  return <Island module={ComponentNameIsland} {...props} />;
};

type ComponentNameComponentType = typeof ComponentNameComponent & {
  ContentFields: typeof ContentFields;
  StyleFields: typeof StyleFields;
};

const ComponentName = ComponentNameComponent as ComponentNameComponentType;
ComponentName.ContentFields = ContentFields;
ComponentName.StyleFields = StyleFields;

export default ComponentName;
```

**Key details:**

- The `?island` query parameter on the import tells the CMS build system to treat this module as an island entry point.
- `@ts-expect-error` is required because TypeScript doesn't understand the `?island` suffix.
- The wrapper can perform **server-side work** before passing props to the island. For example, LanguageSwitcher calls `useLanguageVariants()` here and passes the result as a prop.
- Field components (ContentFields, StyleFields) are attached to the **wrapper**, not the island.

## The Island File (`islands/ComponentNameIsland.tsx`)

The island file is a standard React component with a default export. It contains all the interactive logic.

```typescript
import styles from '../index.module.scss';
import cx from '../../utils/classname.js';
import type { ComponentNameIslandProps } from '../types.js';

const ComponentNameIsland = ({
  className = '',
  style = {},
  ...rest
}: ComponentNameIslandProps) => {
  // Client-side state, handlers, effects go here

  const combinedClasses = cx(styles.componentName, className);

  return (
    <div className={combinedClasses} style={style}>
      {/* Interactive UI */}
    </div>
  );
};

export default ComponentNameIsland;
```

**Key details:**

- The island can either share the parent component's stylesheet (`../index.module.scss`) or have its own stylesheet (`./index.module.scss`) inside the `islands/` directory — pick whichever fits the component.
- The island can import and compose other library components (Button, Drawer, Divider, etc.).
- Island props may differ from the wrapper's props. Define separate types in `types.ts` (e.g., `ComponentNameProps` for the wrapper, `ComponentNameIslandProps` for the island).

## Multiple Islands

A component can contain multiple island files, selected at render time based on props. The Form component demonstrates this:

```typescript
// @ts-expect-error -- ?island not typed
import FormIsland from './islands/FormIsland.js?island';
// @ts-expect-error -- ?island not typed
import LegacyFormIsland from './islands/LegacyFormIsland.js?island';
import { Island } from '@hubspot/cms-components';

const FormComponent = ({ formVersion, ...rest }: FormProps) => {
  const FormModule = formVersion === 'v4' ? FormIsland : LegacyFormIsland;
  return <Island module={FormModule} {...rest} />;
};
```

Each island file follows the same pattern — default export a React component.

## Key Principle

**Modules consuming an island component don't need to know it's an island.** The wrapper fully abstracts the island boundary. Modules use island components exactly like any other component:

```tsx
// Module usage — identical whether the component uses an island or not
<ComponentName someProp="value" />

// Fields usage — same compound component pattern
<ComponentName.ContentFields />
<ComponentName.StyleFields />
```

This means converting a presentational component to an island component (or vice versa) doesn't require changes to any consuming modules.
