Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Layout

Settings Layout

Layout

Settings Layout React component for Tailwind CSS v4

SeparatorSplit View
Live props
State
defaultValue
dirty
saving
Content
title
headingAs
description
Layout
mode
nav
navWidth240px
mobileNav
saveBarPosition
Behavior
saveShortcut
confirmLeave
warnBeforeUnload
Appearance
size
tone
Code
import { SettingsLayout } from "@/components/ui/settings-layout"

<SettingsLayout />
  1. Home
  2. Components
  3. Layout
  4. Settings Layout
Pro

Settings Layout React component for Tailwind CSS v4

v1.1.0·Updated in 1.11.0

A React settings page layout like macOS and iOS: a section nav, grouped label and control rows, and a save bar that slides up on changes.

Pick a section in the sidebar (Up / Down move between items, Home / End jump, Enter opens; disabled items are skipped) and its page fades in at the top while the indicator glides over. Change anything and a dark bar slides up from the bottom: "You have unsaved changes · Discard · Save". Save with the button or mod+s (Cmd+S / Ctrl+S, bound only while there is something to save, even inside a text field): the button spins, then the bar says "Changes saved" for a moment and slides away; a failed save keeps the bar, shakes it and shows the error. Escape inside the bar does nothing, so discarding is always a deliberate click. Switching sections with unsaved changes asks first (Discard / Keep editing; in the tabs, arrow keys then only move focus and Enter or Space switches), and closing the tab warns too. In scroll mode every section sits on one page, the nav follows your scroll and a click glides to the section, below the sticky tabs when there are tabs. Below 768px (the layout's own width) the nav becomes an iOS grouped list: tap a row to push its page, then tap the back button, press Escape or swipe the page to the right to go back; or sticky scrolling pill tabs, or a select whose bottom sheet shows the same groups. On a phone the bar pins above the tab bar and the safe area, lifts over the on-screen keyboard and never covers the last row. Rows bind their label to the control, so tapping "Daily digest" flips its switch.

Settings Layout is a Pro React component in the React layout components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add settings-layout, together with Alert Dialog, Badge, Button, Kbd, Select and 11 more and the npm packages motion and lucide-react.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add settings-layout

Settings Layout is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import SettingsLayout from @/components/ui/settings-layout.

import { Bell, User } from "lucide-react"
import { useState } from "react"
import { Input } from "@/components/ui/input"
import { SettingsLayout, SettingsRow, SettingsSection } from "@/components/ui/settings-layout"
import { Switch } from "@/components/ui/switch"

export function AccountSettings() {
  const [saved, setSaved] = useState({ name: "Emma Carter", digest: true })
  const [draft, setDraft] = useState(saved)
  const dirty = draft.name !== saved.name || draft.digest !== saved.digest

  return (
    <SettingsLayout
      title="Settings"
      dirty={dirty}
      onSave={async () => {
        await fetch("/api/settings", { method: "PUT", body: JSON.stringify(draft) })
        setSaved(draft)
      }}
      onDiscard={() => setDraft(saved)}
      labels={{ unsaved: "You have unsaved changes", save: "Save changes" }}
      sections={[
        {
          id: "profile",
          label: "Profile",
          icon: <User />,
          content: (
            <SettingsSection title="Public profile">
              <SettingsRow label="Name" controlWidth="md">
                <Input value={draft.name} onValueChange={(name) => setDraft({ ...draft, name })} autoComplete="name" />
              </SettingsRow>
            </SettingsSection>
          ),
        },
        {
          id: "notifications",
          label: "Notifications",
          icon: <Bell />,
          content: (
            <SettingsSection title="Email">
              <SettingsRow label="Daily digest" description="Sales and overdue invoices, at 8 AM.">
                <Switch checked={draft.digest} onCheckedChange={(digest) => setDraft({ ...draft, digest })} />
              </SettingsRow>
            </SettingsSection>
          ),
        },
      ]}
    />
  )
}

Props

  • sections*
    SettingsNavItem[]

    The sections: { id, label, icon?, description? (the phone list subtitle), group? (eyebrow label), badge? (a number is a count), href? (a route, wins over content), disabled?, danger?, tone? (the phone icon tile), content? }.

  • defaultValuedefault "profile"
    string

    The section open at first, uncontrolled. Pair value / onValueChange to drive it from the URL. The code default is the first enabled section; an unknown id falls back to it too.

  • value
    string

    The active section id, controlled.

  • onValueChange
    (id: string) => void

    Every switch: a nav click (after the leave confirm), a phone row, the tabs or select, and the scroll spy.

  • titledefault ""
    ReactNode

    The page heading (an h1, see headingAs). On phones it becomes the large title of the grouped list.

  • headingAsdefault "h1"
    "h1" | "h2" | "h3" | "div"

    The element of the title and of the section title on phones. Keep h1 for a settings page; use h2, h3 or div when the layout sits inside a page that already has its h1.

  • descriptiondefault ""
    ReactNode

    A line under the title.

  • children
    ReactNode | ((activeId: string) => ReactNode)

    The content when sections[].content is not used: a function gets the active id (in scroll mode, each id in turn). Sections with an href always render children.

  • modedefault "pages"
    "pages" | "scroll"

    pages: only the active section shows and swaps with a fade. scroll: every section stacked on one page, the nav follows the scroll (an IntersectionObserver spy on the page's scroll container) and a click scrolls there.

  • navdefault "sidebar"
    "sidebar" | "tabs" | "none"

    Desktop navigation: a sticky left column, underline tabs above the content, or none. One section never shows a nav.

  • navWidthdefault 240
    number

    The sidebar column in px.

  • mobileNavdefault "list"
    "list" | "tabs" | "select"

    Below 768px of its own width: an iOS grouped list that pushes the section (split-view stack with swipe back), scrolling pill tabs, or a select (a bottom sheet). Scroll mode uses tabs for list.

  • dirtydefault false
    boolean

    Something changed: shows the save bar, binds saveShortcut and arms the leave guards. The demo also turns it on as soon as you edit a field.

  • savingdefault false
    boolean

    The save button's loading state, for a save you track yourself. It also loads while an onSave promise runs; turning false with dirty off flashes the saved message.

  • onSave
    (() => void | Promise<unknown>)

    Runs from the button or the shortcut. A promise keeps the button loading; resolved flashes labels.saved for 1.2s, rejected keeps the bar, shakes it and shows the error's message (else labels.saveError).

  • onDiscard
    () => void

    The Discard button, and a confirmed leave. Put the saved values back here.

  • saveShortcutdefault "mod+s"
    string

    Saves while dirty (it replaces the browser's Save Page only then). Shown as a key hint on the button, hidden on touch. "" disables.

  • confirmLeavedefault true
    boolean

    Pages mode: switching sections while dirty asks "Discard changes?" (Discard / Keep editing) and calls onDiscard when confirmed. Links (href items) are the router's to guard.

  • warnBeforeUnloaddefault true
    boolean

    The browser's leave-page warning while dirty.

  • saveBarPositiondefault "sticky"
    "sticky" | "fixed"

    sticky: floats at the bottom of the content column while you scroll; fixed: pinned to the viewport. Phones always pin it above --bottom-bar-offset and the safe area, lifted over the keyboard.

  • sizedefault "md"
    "sm" | "md"

    Nav rows 32 / 36px (44px on touch) and the row padding (48 / 56px rows).

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The nav indicator (a soft tint of the tone) and the save button. Neutral is a gray pill and an inverted button.

  • linkComponent
    (props: LinkComponentProps) => ReactNode

    Renders nav items that have an href (settings as routes). Defaults to the LinkProvider's, else a plain <a>.

  • labels
    Partial<SettingsLayoutLabels>

    Every string: nav "Settings", unsaved "You have unsaved changes", save "Save", saving "Saving…", discard "Discard", saved "Changes saved", saveError "Could not save. Try again.", leaveTitle "Discard changes?", leaveDescription "Your changes in this section will be lost.", leaveConfirm "Discard", leaveCancel "Keep editing", back "Settings", select "Section". Exported as DEFAULT_SETTINGS_LAYOUT_LABELS.

  • classNames
    Partial<Record<"frame" | "header" | "title" | "description" | "nav" | "group" | "item" | "indicator" | "content" | "mobileNav" | "mobileList" | "mobileItem" | "mobileDetail" | "saveBar", string>>

    Extra classes for the parts. The phone stack's height is the --settings-mobile-height variable (default 100dvh minus --topbar-offset).

Parts

  • SettingsSection

    component

    A group of rows: <section aria-labelledby> with an h2 (headingLevel), description, actions and a footer. variant "grouped" (an iOS inset group, the default) or "plain" (hairlines on the page); danger adds a red ring and title. Reads useDefaults("settings-section"). Props: SettingsSectionProps.

  • SettingsRow

    component

    One setting: label, description, badge ("Pro"), error and the control on the right. A single child control (an input, switch, select or any component) gets the label's htmlFor, aria-labelledby (so a segmented control or radio group is named too) and aria-describedby automatically; buttons and links keep their own name inside a group named by the label, and a <div> wrapper keeps a plain label (wire it with htmlFor or useSettingsRow). layout "auto" | "inline" | "stacked", controlWidth "auto" | "sm" (200px) | "md" (320px) | "full", align, danger, disabled. Rows go inline once their section is 560px wide. Reads useDefaults("settings-row"). Props: SettingsRowProps.

  • SettingsSaveBar

    component

    The floating save bar on its own, for any form: dirty, saving, onSave, onDiscard, saveShortcut, position (sticky | fixed), message, tone, labels, classNames. Props: SettingsSaveBarProps.

  • useSettingsLayout

    hook

    Inside the layout: the active section and a stable setter that goes through the unsaved-changes guard (a link row that jumps to another section). Signature: () => { activeId: string; setActiveId: (id: string) => void; dirty: boolean }.

  • useSettingsRow

    hook

    Inside a row: the ids to wire a custom control by hand (a radio group takes aria-labelledby={labelId}). Signature: () => { controlId: string; labelId: string; describedBy: string | undefined } | null.

  • DEFAULT_SETTINGS_LAYOUT_LABELS

    constant

    The English strings, to spread under a translation. Type: SettingsLayoutLabels.

Examples

Scroll mode

mode="scroll": every section on one page; the sidebar (pill tabs when narrow) follows the scroll and a click glides there.

Tabs on top

nav="tabs" for a short settings page (mobileNav="tabs" when narrow); switching with unsaved changes asks first.

Row layouts

Inline switches, sized fields that stack on narrow sections, a full-width textarea, a Pro badge and an error.

The save bar alone

SettingsSaveBar on any form, fixed: edit the name, then save with the button or mod+s.

On a phone

The grouped list pushes a section; swipe from the left or tap Settings to go back.

Phone with a select

mobileNav="select": one bottom-sheet picker instead of the list.

Brand tone, small

tone="brand" colors the nav indicator (pill tabs when narrow) and the save button; size="sm" tightens nav and rows.

Dependencies

motionlucide-reactalert-dialogbadgebuttonkbdselectspinnersplit-viewtabsuse-controllable-stateuse-keyboard-insetuse-scroll-stateuse-shortcutnavigationmotiontonesui-config

Files

  • components/ui/settings-layout.tsx

FAQ

Is the React Settings Layout component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Settings Layout?

Run npx wingo-ui@latest add settings-layout. It also adds the Wingo UI items it builds on (Alert Dialog, Badge, Button, Kbd, Select and 11 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/settings-layout.

How does Settings Layout behave on phones?

Below 768px (the layout's own width) the nav becomes an iOS grouped list: tap a row to push its page, then tap the back button, press Escape or swipe the page to the right to go back; or sticky scrolling pill tabs, or a select whose bottom sheet shows the same groups.

See also

Settings PageSplit ViewSidebarMembers tablePermissions MatrixForm Wizard

Used in

Settings PageUsers Admin Page

More React layout components

ToolbarSeparatorCardApp ShellAccordionCollapsible
All 12 React layout components

Articles using this component

  • React Settings Page: Section Nav, Save Bar, Danger Zone

    Build it: screens and apps · Oct 16, 2026 · 11 min read

PreviousSeparatorNext Split View

Settings

Your account and the data of Acme Software SRL

Account
Company
Team
Security

Public profile

How teammates see you in comments and approvals.

Photo

JPG or PNG, at least 256 × 256 px.

Contact details

Where you get invoices and the daily digest.

Settings

Your account and the data of Acme Software SRL

Account
Company
Team
Security

Select an item to see it here

saveShortcut="mod+s" · confirmLeave=true · warnBeforeUnload=true · dirty=false