Meowland docs
Workbench

Component reference

RegionLayout

The parent container allocates space; a narrow window combines reading and input, and going back keeps the draft and list focus.

Preview

List, reading and input

packages/meowtail/stories/region-layout.stories.tsxline 41tsx
render: () => (
  <div style={{ height: "min(680px, calc(100dvh - 90px))", minHeight: 0 }}>
    <PageFrame
      header={
        <PageHeader
          title="Messages"
          description="Named areas and content use the same shared layout."
        />
      }
    >
      <div
        ref={(host: HTMLElement) => {
          const surface = mountRegionLayout(
            host,
            {
              areas: [
                ["list", "thread"],
                ["list", "messages"],
                ["list", "compose"],
              ],
              columns: ["compact", "fill"],
              rows: ["content", "fill", "content"],
              regions: {
                list: { group: "browse" },
                thread: { group: "detail" },
                messages: { group: "detail", scrollOwner: "content" },
                compose: { group: "detail" },
              },
            },
            {
              backLabel: "Back to list",
              regionLabels: {
                list: "Conversation list",
                thread: "Current conversation",
                compose: "Message input",
              },
            },
          );
          const draft = signal("");
          const selected = signal(-1);
          const mounts = [
            render(
              <Stack gap="compact">
                {Array.from({ length: 35 }, (_, i) => (
                  <NavItem
                    label={`Conversation ${i + 1}`}
                    active={mapSignal(selected, (value) => value === i)}
                    onClick={() => {
                      selected.value = i;
                      surface.showGroup("detail");
                    }}
                  />
                ))}
              </Stack>,
              surface.regions.list!,
            ),
            render(
              <strong>
                Continuous review work · Long conversation names wrap naturally and do not widen
                the layout
              </strong>,
              surface.regions.thread!,
            ),
            render(
              <ChatConversation ariaLabel="Message reading area">
                {Array.from({ length: 40 }, (_, i) => (
                  <p>
                    Message {i + 1}: Content stays readable; history and drafts are not lost when
                    the window changes.
                  </p>
                ))}
              </ChatConversation>,
              surface.regions.messages!,
            ),
            render(
              <Input
                aria-label="Message draft"
                value={draft}
                onInput={(event: Event) => {
                  draft.value = (event.target as HTMLInputElement).value;
                }}
              />,
              surface.regions.compose!,
            ),
          ];
          return () => {
            for (const mount of mounts) mount.unmount();
            surface.dispose();
          };
        }}
      />
    </PageFrame>
  </div>
)

Installation

bun add meowtail

Examples

The other stories that show this component.

Menus and choices in local content

Content only enters leaf regions; the shared menu, choices and focus still use real components.

packages/meowtail/stories/region-layout.stories.tsxline 147tsx
render: () => (
  <div
    style={{ height: "320px" }}
    ref={(host: HTMLElement) => {
      const surface = mountRegionLayout(
        host,
        { areas: [["content"]], regions: { content: {} } },
        { isolateContent: true },
      );
      const content = surface.regions.content!;
      const shadow = content.getRootNode() as ShadowRoot;
      const style = host.ownerDocument.createElement("style");
      style.textContent = standardTheme.cssText;
      shadow.append(style);
      const removeTheme = applyTheme(content, standardTheme);
      const selected = signal("Not selected");
      const mounted = render(
        <Stack align="start">
          <Menu
            id="isolated-menu"
            trigger={(bindings) => <Button {...bindings}>Region actions</Button>}
            items={[
              {
                id: "read",
                label: "Read",
                onSelect: () => {
                  selected.value = "Read";
                },
              },
              {
                id: "copy",
                label: "Copy",
                onSelect: () => {
                  selected.value = "Copy";
                },
              },
              {
                id: "more",
                label: "More",
                children: [
                  {
                    id: "detail",
                    label: "View details",
                    onSelect: () => {
                      selected.value = "Details";
                    },
                  },
                ],
              },
            ]}
          />
          <ChoiceSelect
            id="isolated-choice"
            options={[
              { value: "one", label: "First option" },
              { value: "two", label: "Second option" },
            ]}
          />
          <span role="status">{selected}</span>
        </Stack>,
        content,
      );
      return () => {
        mounted.unmount();
        removeTheme();
        surface.dispose();
      };
    }}
  />
)

Narrow-window navigation beside content

Two columns on desktop; a narrow window keeps navigation and content in the same group. Changing region geometry does not rebuild the input or the category.

packages/meowtail/stories/region-layout.stories.tsxline 363tsx
render: () => (
  <div style={{ height: "calc(100dvh - 90px)", minHeight: 0 }}>
    <PageFrame variant="workspace">
      <div
        ref={(host: HTMLElement) => {
          const surface = mountRegionLayout(
            host,
            {
              areas: [["navigation", "content"]],
              columns: ["compact", "fill"],
              gap: "compact",
              regions: {
                navigation: { compactRow: "content" },
                content: { compactRow: "fill" },
              },
            },
            {
              regionLabels: {
                navigation: "Category navigation",
                content: "Category body",
              },
            },
          );
          const selected = signal(0);
          const draft = signal("");
          const mounts = [
            render(
              <Grid minColumnWidth="compact" gap="compact">
                {Array.from({ length: 5 }, (_, i) => (
                  <NavItem
                    label={`Category ${i + 1}`}
                    active={mapSignal(selected, (value) => value === i)}
                    onClick={() => {
                      selected.value = i;
                    }}
                  />
                ))}
              </Grid>,
              surface.regions.navigation!,
            ),
            render(
              <Stack gap="compact">
                <strong>{mapSignal(selected, (value) => `Current category ${value + 1}`)}</strong>
                <Input
                  aria-label="Category draft"
                  value={draft}
                  onInput={(event: Event) => {
                    draft.value = (event.target as HTMLInputElement).value;
                  }}
                />
                {Array.from({ length: 20 }, (_, i) => (
                  <p>
                    Content {i + 1}: Changing the window only moves regions; the current category
                    and draft keep their identity.
                  </p>
                ))}
                <Button>End action</Button>
              </Stack>,
              surface.regions.content!,
            ),
          ];
          return () => {
            for (const mount of mounts) mount.unmount();
            surface.dispose();
          };
        }}
      />
    </PageFrame>
  </div>
)

API reference

Props

This component documents no props of its own.

DOM parts

rootbackgridregioncontent

Registry

The component is registered as region-layout in the meowtail component registry.