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
| Technology | Version | Purpose |
|---|---|---|
| React | 18 | UI framework |
| TypeScript | 5.x | Type safety |
| Tailwind CSS | 4.x | Styling |
| Vite | 6.x | Build tool |
| shadcn/ui | Latest | Component 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 functionsEntry 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
- Create a new directory under
src/client/(e.g.,help-dialog/) - Add
index.htmlandindex.jsx - Update
vite.config.tsto 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",
},
];- Update
src/server/ui.tsto 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.tsxAdd more components as needed:
npx shadcn@latest add card
npx shadcn@latest add inputButton 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} />;
}