# Full-page apps

Build a custom Stripe Dashboard page with tabbed navigation, list-to-detail flows, and data visualization.

> Full-page apps is in private preview and your app must be added to the preview before you can use this feature. [Submit your email address](https://docs.stripe.com/stripe-apps/patterns/full-page-apps.md#stripe_apps_private_preview) to request access.

### Interested in the preview of new app components and capabilities?

Enter your email to request access.

```bash
curl https://docs.stripe.com/preview/register \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Referer: https://docs.stripe.com/stripe-apps/patterns/full-page-apps" \
  -d '{"email": "EMAIL", "preview": "stripe_apps_private_preview"}'
```

Full-page apps give you an entire page in the Stripe Dashboard. This provides room for tabbed navigation, overview dashboards, list-to-detail flows, and data visualization. This guide describes key design patterns for building a full-page app view.

## Before you begin

Before you build a full-page app, make sure your app meets these requirements:

- [Create an app](https://docs.stripe.com/stripe-apps/create-app.md) or use an existing one.
- Install `@stripe/ui-extension-sdk` version `9.2.0-alpha.x` or later.
- Install the latest version of the [Stripe Apps CLI plugin](https://docs.stripe.com/stripe-apps/reference/cli.md#upgrade-the-cli).
- Add a `stripe.dashboard.fullpage` [viewport](https://docs.stripe.com/stripe-apps/how-ui-extensions-work.md#views-and-viewports) to your app.
- Review the [routing guide](https://docs.stripe.com/stripe-apps/routing.md) for full details on `createRoutes`, `AppRouter`, `useAppRoute`, and `useNavigation`.

## Apply best practices

Keep these principles in mind when you design a full-page app:

- **Organized**: Structure your app into logical tabs that represent distinct areas of your workflow. Each tab serves a clear purpose so users always know where to find what they need.
- **Progressive**: Lead with a high-level overview and let users discover details at their own pace. Don’t overwhelm users with all information at once.
- **Consistent**: Follow the layout and navigation patterns used by native Stripe Dashboard pages.
- **Connected**: Connect your full-page app to other parts of your Stripe Apps. If your app also uses a drawer view or settings page, provide clear pathways between them.

## Set up your full-page view
![Full-page app example](https://docs.stripecdn.com/8a9e806f8630691f96a7aca4e200b859c012b6148abdc70cfea91217f3603f6e.png)

Every full-page app wraps its content in the [FullPageView](https://docs.stripe.com/stripe-apps/components/fullpageview.md) component. This renders the Dashboard page header that displays your app’s identity and actions.

The page header pulls from your `stripe-app.json`:

- **App name**: The `name` field.
- **App icon**: The `icon` field.
- **Page action**: An optional primary button. See the [FullPageView](https://docs.stripe.com/stripe-apps/components/fullpageview.md) component for the `pageAction` prop.
- **Settings**: Appears automatically to let users navigate to app settings.

Your `stripe-app.json` must include a `stripe.dashboard.fullpage` viewport:

```json
{
  "name": "Ride management",
  "icon": "./assets/icon.png",
  "ui_extension": {
    "views": [
      {
        "viewport": "stripe.dashboard.fullpage",
        "component": "FullpageView"
      }
    ]
  }
}
```

The `component` field maps to your view file in `src/views/`.

## Structure your page with tabs

Use `Tabs` and `Tab` from `@stripe/ui-extension-sdk/ui/next` to organize content into sections. Full-page apps use [routing](https://docs.stripe.com/stripe-apps/routing.md) to map URLs to views. This gives users bookmarkable URLs, browser back/forward navigation, and shareable links to specific tabs.

Start by defining your route config with a `/:tabId?` pattern so the selected tab persists in the URL:

```jsx
export const routes = createRoutes({
  home: route('/:tabId?', () => (
    <FullPageView>
      <Home />
    </FullPageView>
  )),
  customer: route('/customers/:id', ({id}) => (
    <FullPageView>
      <CustomerDetail id={id} />
    </FullPageView>
  )),
});
```

See the [routing guide](https://docs.stripe.com/stripe-apps/routing.md) for the full setup, including `AppRouter`, type-safe navigation with `RouteRegister`, path patterns, and `redirectOnNotFound`.

Then wire the selected tab to your route parameter:

```jsx
import {useAppRoute, useNavigation} from '@stripe/ui-extension-sdk/navigation';
import {Tab, Tabs} from '@stripe/ui-extension-sdk/ui/next';

import {OverviewTab} from './tabs/OverviewTab';
import {CustomersTab} from './tabs/CustomersTab';

const DEFAULT_TAB = 'overview';

export function Home() {
  const {routeParams, key} = useAppRoute();
  const {navigateToAppRoute} = useNavigation();
  const currentTab = (key === 'home' && routeParams.tabId) || DEFAULT_TAB;

  return (
    <Tabs
      selectedKey={currentTab}
      onSelectionChange={(tabId) => navigateToAppRoute('home', {tabId})}
    >
      <Tab id="overview" label="Overview"><OverviewTab /></Tab>
      <Tab id="customers" label="Customers"><CustomersTab /></Tab>
    </Tabs>
  );
}
```

This pattern connects tabs to routing in three steps:

1. **Read the current tab from the route**: `useAppRoute()` returns `routeParams` and `key` directly. Check `key === 'home'` first to narrow the type, then fall back to `DEFAULT_TAB` when no tab is specified (the `/` root URL).
2. **Control the selected tab**: Pass the resolved tab ID to `selectedKey` so the Tabs component renders the correct panel.
3. **Update the route on tab change**: Pass `navigateToAppRoute` to `onSelectionChange` so selecting a tab updates the URL from `/` to `/customers`.

This means navigating directly to a URL like `/customers` renders the correct tab automatically, and the browser’s back and forward buttons move between tabs.

## Build an overview tab
![Full-page app overview tab](https://docs.stripecdn.com/7ed23aa744ab5132515489972b7053acda5863098d7e3542b16beab7214a2aeb.png)

An overview tab gives users a high-level summary of activity across your app. Use the [OverviewPage](https://docs.stripe.com/stripe-apps/components/overviewpage.md) component to structure a two-column layout with summary metrics, charts, and sidebar modules.

```jsx
import {Box, Inline, OverviewPage, Spinner} from '@stripe/ui-extension-sdk/ui';
import {PageModule} from '@stripe/ui-extension-sdk/ui/experimental';
import {LineChart, MeterChart} from '@stripe/ui-extension-sdk/ui/next';

export function OverviewTab() {
  if (isLoading) {
    return (
      <Box css={{stack: 'y', alignX: 'center', alignY: 'center', height: 'fill', paddingTop: 'xxlarge'}}>
        <Spinner size="large" />
      </Box>
    );
  }

  return (
    <OverviewPage
      primaryColumn={
        <>
          <PageModule title="Growth">
            <Box css={{height: 182}}>
              <LineChart data={growthData} />
            </Box>
          </PageModule>
        </>
      }
      secondaryColumn={
        <>
          <PageModule title="Distribution">
            <MeterChart data={tierData} legendEnabled unitFormat={{unit: 'number', options: {}}} />
          </PageModule>
        </>
      }
    />
  );
}
```

Place the [loading state](https://docs.stripe.com/stripe-apps/patterns/loading.md) inside the tab component so the tab bar remains visible and interactive while content loads.

## Build a list view
![Full-page app list](https://docs.stripecdn.com/e23e42b8d6c8c9aa902577475890dac980fd56c98babf683b2a5560f3340dd1e.png)

Use the [DataTable](https://docs.stripe.com/stripe-apps/components/datatable.md) component to display structured data with sortable columns, status indicators, and clickable rows. Use `navigateToAppRoute` to navigate to a detail view when a user clicks a row.

```jsx
import {useNavigation} from '@stripe/ui-extension-sdk/navigation';
import {DataTable} from '@stripe/ui-extension-sdk/ui/experimental';

export function CustomersTab() {
  const {navigateToAppRoute} = useNavigation();

  return (
    <DataTable
      columns={[
        {key: 'name', label: 'Name'},
        {key: 'email', label: 'Email'},
        {key: 'status', label: 'Status', cell: {type: 'status', statusMap: {Active: 'positive', 'At risk': 'warning', Churned: 'negative'}}},
      ]}
      items={customers}
      onRowClick={(item) => navigateToAppRoute('customer', {id: item.id})}
      pagination={{pageSize: 10}}
      emptyMessage={{
        message: 'No customers yet.',
        action: {label: 'Add customer', href: 'https://dashboard.stripe.com/customers/create'},
      }}
    />
  );
}
```

Include an `emptyMessage` prop to handle [empty states](https://docs.stripe.com/stripe-apps/patterns/empty-state.md). If your list supports [filter controls](https://docs.stripe.com/stripe-apps/patterns/filter-controls.md), swap the `emptyMessage` based on whether filters are active.

## Build a detail view
![Full-page app detail view](https://docs.stripecdn.com/38feeb88bebd95b7cd3ec2b1b91f56b719777301b141171507898c250b67ffe1.png)

When a user selects an item from a list, the router renders the detail route. Use the [DetailPage](https://docs.stripe.com/stripe-apps/components/detailpage.md) component from `@stripe/ui-extension-sdk/ui/experimental` to structure the detail view with breadcrumbs, a two-column layout, and page-level actions.

```jsx
import {DetailPage, PageModule} from '@stripe/ui-extension-sdk/ui/experimental';
import {useNavigation} from '@stripe/ui-extension-sdk/navigation';

export function CustomerDetail({id}) {
  const {createAppRoute} = useNavigation();
  const customer = useCustomer(id);

  return (
    <DetailPage
      title={customer.name}
      breadcrumbs={[
        {type: 'link', label: 'Customers', route: createAppRoute('home', {tabId: 'customers'})},
        {type: 'label', label: customer.name},
      ]}
      primaryColumn={
        <PageModule title="Overview">{/* Primary content */}</PageModule>
      }
      secondaryColumn={
        <PageModule title="Details">{/* Sidebar content */}</PageModule>
      }
    />
  );
}
```

Because the detail view is its own route, the tab bar isn’t visible. The breadcrumb provides the path back to the list. See the [routing guide](https://docs.stripe.com/stripe-apps/routing.md#declarative-navigation-with-link) for more on `createAppRoute` and declarative navigation.

## Build create and edit flows

When users create a new item or edit an existing one, use a [FocusView](https://docs.stripe.com/stripe-apps/components/focusview.md) drawer. The drawer overlays the current view without interrupting the user’s place in the app.

Use the [Button](https://docs.stripe.com/stripe-apps/components/button.md) component’s `pending` prop to indicate that a save operation is in progress.

```jsx
import {useState} from 'react';
import {Button, FocusView, TextField} from '@stripe/ui-extension-sdk/ui';

function CreateItemForm({shown, setShown}) {
  const [isSaving, setIsSaving] = useState(false);

  const handleSave = async () => {
    setIsSaving(true);
    try {
      await saveItem();
      setShown(false);
    } finally {
      setIsSaving(false);
    }
  };

  return (
    <FocusView title="Create item" shown={shown} setShown={setShown}>
      <TextField label="Name" />
      <Button type="primary" onPress={handleSave} pending={isSaving}>
        Create item
      </Button>
    </FocusView>
  );
}
```

## See also

- [Routing guide](https://docs.stripe.com/stripe-apps/routing.md)
- [Loading pattern](https://docs.stripe.com/stripe-apps/patterns/loading.md)
- [Empty state pattern](https://docs.stripe.com/stripe-apps/patterns/empty-state.md)

### Interested in the preview of new app components and capabilities?

Enter your email to request access.

```bash
curl https://docs.stripe.com/preview/register \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Referer: https://docs.stripe.com/stripe-apps/patterns/full-page-apps" \
  -d '{"email": "EMAIL", "preview": "stripe_apps_private_preview"}'
```
