Settings tabs
View on GitHubAdd a brand-new settings tab
Worked example: adding a billing tab to the Account section — a tab
id that doesn't exist anywhere in the codebase yet, unlike guide 1's
Export (which already had a component and a registry row, just
disabled).
Step 1 — Add the tab id
File: resources/js/types/Settings.ts
export type SettingsTabId =
| 'preferences'
| 'profile'
| 'notifications'
| 'security-access'
| 'billing'
| 'integrations'
| 'export'
| 'labels'
| 'statuses'
| 'priorities'
| 'templates'
| 'documents'
| 'members'
| 'roles-management';
Step 2 — Register it
File: resources/js/types/Settings.ts
export const SETTINGS_TABS: SettingsTab[] = [
// ...existing entries...
{
id: 'billing',
label: 'Billing',
icon: 'CreditCard',
section: 'account',
description: 'Manage your subscription plan and payment method.',
enabled: true,
},
// ...remaining entries...
];
export const ACCOUNT_SETTINGS_TAB_IDS = [
'preferences',
'profile',
'notifications',
'security-access',
'billing',
'export',
] as const;
section: 'account' and adding the id to ACCOUNT_SETTINGS_TAB_IDS
are both required, and independently — the first decides which
sidebar group (accountTabs/workspaceTabs in
Pages/Settings/Index.tsx) the nav item renders under; the second is
what isAccountSettingsTabId() checks to decide whether
AccountSettingsContent or WorkspaceSettingsContent handles it (see
the README's architecture section). Get the id into the
array matching the wrong section (or neither) and the tab is
selectable in the sidebar but resolves to neither content component's
if chain, so nothing renders.
Pick an icon from lucide-react (any name valid as
keyof typeof icons) that isn't already representing a different tab.
Step 3 — Create the tab component
New file: resources/js/Components/Organisms/AccountSettingsContent/AccountSettingsBillingTab.tsx
import Button from '@/Components/Atoms/Button/Button';
import SettingsPanel from '@/Components/Molecules/SettingsPanel/SettingsPanel';
import SettingsPanelRow from '@/Components/Molecules/SettingsPanelRow/SettingsPanelRow';
interface AccountSettingsBillingTabProps {
planName?: string;
}
export default function AccountSettingsBillingTab({
planName = 'Free',
}: AccountSettingsBillingTabProps) {
return (
<div className="space-y-5">
<SettingsPanel
title="Current plan"
description="See your active plan and manage your subscription."
>
<SettingsPanelRow
title={planName}
description="Your workspace's current billing plan."
action={
<Button type="button" isBox className="px-3 py-1.5">
Change plan
</Button>
}
/>
</SettingsPanel>
</div>
);
}
Every existing Account tab follows this same shape — one or more
SettingsPanels, each holding either free-form content or
SettingsPanelRows — copy whichever existing tab's layout is closest
to what the new one needs (AccountSettingsSecurityTab for a
list-of-items panel, AccountSettingsPreferencesTab for a grid of
selectable cards).
Step 4 — Wire it into the switch
File: resources/js/Components/Organisms/AccountSettingsContent/AccountSettingsContent.tsx
import AccountSettingsBillingTab from './AccountSettingsBillingTab';
// ...existing imports...
interface AccountSettingsContentProps {
tabId: AccountSettingsTabId;
userName?: string;
userAvatar?: string | null;
sessions?: Session[];
notificationSettings?: NotificationSettings;
planName?: string;
}
export default function AccountSettingsContent({
tabId,
userName,
userAvatar,
sessions = [],
notificationSettings,
planName,
}: AccountSettingsContentProps) {
if (tabId === 'preferences') {
return <AccountSettingsPreferencesTab />;
}
// ...existing tabId branches, unchanged...
if (tabId === 'billing') {
return <AccountSettingsBillingTab planName={planName} />;
}
return <AccountSettingsExportTab />;
}
This is a flat if chain, not a lookup object — add the new branch
anywhere before the final return, in whatever order reads clearly;
order has no functional effect since each if returns immediately.
Step 5 — Pass any real data down from the page
File: resources/js/Pages/Settings/Index.tsx — add planName to
SettingsIndexProps and pass it through to AccountSettingsContent,
the same way notificationSettings already flows through unchanged.
File: app/Http/Controllers/SettingsController.php — add a
'planName' => ... key to the Inertia::render(...) props array,
computed from whichever Service backs real billing data (not built
yet — this step is only the wiring pattern, following
SettingsController::index()'s existing shape of one array key per
prop, each usually gated behind whatever access check makes sense,
mirroring hasIntegrationsAccess/integrationStatuses's pattern for
a feature-gated prop).
Tests
resources/js/Components/Organisms/AccountSettingsContent/AccountSettingsBillingTab.test.tsx— new file, following the shape of any existing Account tab test (e.g.AccountSettingsPreferencesTab.test.tsx): render the component, assert the expected copy/props render.resources/js/Components/Organisms/AccountSettingsContent/AccountSettingsContent.test.tsx(if one exists) — add a case assertingtabId="billing"rendersAccountSettingsBillingTab.tests/Feature/SettingsControllerTest.php— if you wired a real backend prop in step 5, add a test asserting it appears in the Inertia response for an authenticated user.
