ShipAddons

Frontend Stack

React, TypeScript, Tailwind CSS, and Vite

The add-on client is built with a modern frontend stack optimized for developer experience and bundle size.

Stack Overview

TechnologyVersionPurpose
React18UI framework
TypeScript5.xType safety
Tailwind CSS4.xStyling
Vite6.xBuild tool
shadcn/uiLatestComponent library

Project Structure

addon/src/client/
├── components/          # Reusable UI components
│   ├── ui/              # shadcn/ui components
│   ├── Header.tsx
│   ├── Footer.tsx
│   ├── Home.tsx
│   └── ...
├── context/             # React context providers
│   └── AuthContext.tsx
├── hooks/               # Custom React hooks
│   ├── useAuth.ts
│   ├── usePurchase.ts
│   └── useUser.ts
├── dialog/              # Dialog entry point
│   ├── index.html
│   └── index.jsx
├── sidebar/             # Sidebar entry point
│   ├── index.html
│   └── index.jsx
├── styles.css           # Global styles + Tailwind
├── serverFunctions.ts   # Apps Script RPC bridge
└── utils.ts             # Utility functions

Entry Points

Each UI surface (sidebar, dialog) has its own entry point:

// sidebar/index.jsx
import React from "react";
import { createRoot } from "react-dom/client";
import { AuthProvider } from "../context/AuthContext";
import Home from "../components/Home";
import "../styles.css";

const root = createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <AuthProvider>
      <Home />
    </AuthProvider>
  </React.StrictMode>
);

Adding New Entry Points

  1. Create a new directory under src/client/ (e.g., help-dialog/)
  2. Add index.html and index.jsx
  3. Update vite.config.ts to include the new entry:
// vite.config.ts
const clientEntrypoints = [
  {
    name: "CLIENT - Dialog",
    filename: "dialog",
    template: "dialog/index.html",
  },
  {
    name: "CLIENT - Sidebar",
    filename: "sidebar",
    template: "sidebar/index.html",
  },
  {
    name: "CLIENT - Help Dialog",
    filename: "help-dialog",
    template: "help-dialog/index.html",
  },
];
  1. Update src/server/ui.ts to include the new entry:
export const openHelpDialog = () => {
  const html = HtmlService.createHtmlOutputFromFile('help-dialog')
    .setWidth(1280)
    .setHeight(720);
  getActiveUiForContext().showModalDialog(html, EXTENSION_NAME);
};

...

export const onOpen = () => {
  const menu = getActiveUiForContext().createAddonMenu();

  menu.addItem('Sidebar', 'openSidebar');
  menu.addSeparator();
  menu.addItem('Dialog', 'openDialog');
  menu.addSeparator();
  menu.addItem('Help Dialog', 'openHelpDialog');
  menu.addToUi();
};

UI Components

shadcn/ui

The boilerplate includes pre-configured shadcn/ui components:

components/ui/
├── alert.tsx
├── badge.tsx
├── button.tsx
├── modal.tsx
├── select.tsx
├── sonner.tsx      # Toast notifications
├── spinner.tsx
├── tabs.tsx
└── textarea.tsx

Add more components as needed:

npx shadcn@latest add card
npx shadcn@latest add input

Button Example

import { Button } from "./ui/button";

function MyComponent() {
  return (
    <div className="flex gap-2">
      <Button variant="default">Primary</Button>
      <Button variant="outline">Secondary</Button>
      <Button variant="ghost">Tertiary</Button>
      <Button variant="destructive">Danger</Button>
    </div>
  );
}

Toast Notifications

import { toast } from "sonner";

function handleAction() {
  try {
    await doSomething();
    toast.success("Action completed");
  } catch (e) {
    toast.error("Something went wrong");
  }
}

Server Functions Bridge

The serverFunctions.ts file provides type-safe access to Apps Script functions:

// serverFunctions.ts
import { GASClient } from "gas-client";
import * as publicServerFunctions from "../server";

const { serverFunctions, scriptHostFunctions } = new GASClient<
  typeof publicServerFunctions
>({
  // this is necessary for local development but will be ignored in production
  allowedDevelopmentDomains: (origin) =>
    /https:\/\/.*\.googleusercontent\.com$/.test(origin),
});

export { serverFunctions, scriptHostFunctions };

Usage in components:

import serverFunctions from "../serverFunctions";

function MyComponent() {
  const [context, setContext] = useState("");

  useEffect(() => {
    serverFunctions.detectScriptContext().then(setContext);
  }, []);

  return <div>Running in: {context}</div>;
}

Development Workflow

For detailed setup instructions including port forwarding, live development with HMR inside Google Workspace, and build commands, see the Development Setup tutorial.

Best Practices

Keep Components Small

Google Workspace add-ons have limited screen space. Keep components focused and avoid deep nesting.

Minimize Bundle Size

Every kilobyte matters in add-ons. Tips:

  • Use tree-shakeable imports
  • Avoid large libraries when possible
  • Let Vite handle code splitting

Handle Loading States

Network calls from addon client to server side (GCP) can be slow. Always show loading indicators:

function DataComponent() {
  const { data, loading, error } = useData();

  if (loading) return <Spinner />;
  if (error) return <Alert variant="destructive">{error.message}</Alert>;

  return <DataView data={data} />;
}

On this page