Skip to content
touchcn
Esc
navigateopen⌘Jpreview
On this page

Settings

A grouped settings screen composed from touchcn components.

A grouped-list settings screen: a profile header, an appearance section wired to the live appearance, notification toggles, about rows and a destructive sign-out row that confirms through a dialog before firing its event.

Installation

npx touchcn add settings

This also adds the components the block composes:

avatar · button · dialog · list · navbar · page · switch

Usage

<tcn-settings-block name="Ada Lovelace" email="ada@example.com" (signOut)="signOut()" />
import { TcnSettingsBlock } from '@/components/blocks/settings';

<TcnSettingsBlock name="Ada Lovelace" email="ada@example.com" onSignOut={signOut} />
<script setup lang="ts">
import { TcnSettingsBlock } from '@/components/blocks/settings';
</script>

<template>
  <TcnSettingsBlock name="Ada Lovelace" email="ada@example.com" @sign-out="signOut" />
</template>

The appearance rows read and write the active theme and color scheme through TouchcnAppearance (Angular) / useTouchcnAppearance (React), so they control the whole app. The notification toggles hold local state — wire them to your own settings.

Set showBack to render a back control in the navbar’s leading slot; it emits back (Angular) / onBack (React) when tapped — wire it to your router.

Source · Angular
export * from './tcn-settings-block';
import { booleanAttribute, Component, computed, inject, input, output, signal } from '@angular/core';
import { TouchcnAppearance } from '@touchcn/angular';
import { TcnAvatar } from '@/components/ui/avatar';
import { TcnButton } from '@/components/ui/button';
import { TcnDialog } from '@/components/ui/dialog';
import { TcnList, TcnListItem } from '@/components/ui/list';
import { TcnNavbar } from '@/components/ui/navbar';
import { TcnPage } from '@/components/ui/page';
import { TcnSwitch } from '@/components/ui/switch';

/**
 * Settings screen — a grouped-list settings surface. The appearance section is
 * wired to the live theme and color scheme via the `TouchcnAppearance` service;
 * the remaining toggles hold local presentational state, and sign-out is
 * confirmed through a dialog before `signOut` fires.
 */
@Component({
  selector: 'tcn-settings-block',
  imports: [TcnPage, TcnNavbar, TcnAvatar, TcnList, TcnListItem, TcnSwitch, TcnDialog, TcnButton],
  template: `
    <tcn-page>
      <tcn-navbar title="Settings">
        @if (showBack()) {
          <button navbar-leading type="button" class="tcn-navbar-back" aria-label="Back" (click)="back.emit()">
            <svg class="if-ios" width="11" height="18" viewBox="0 0 12 20" fill="none" aria-hidden="true">
              <path d="M10 2 2 10l8 8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
            </svg>
            <svg class="if-md" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <path d="M20 12H4m0 0 6-6m-6 6 6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
            </svg>
          </button>
        }
      </tcn-navbar>
      <div class="mx-auto min-h-full max-w-[430px] px-4 pt-20 pb-12">
        <div class="mb-6 flex items-center gap-4 px-1 py-2">
          <tcn-avatar size="lg" [src]="avatarSrc()" [initials]="initials()" [alt]="name()" />
          <div class="min-w-0">
            <div class="truncate text-lg font-semibold">{{ name() }}</div>
            <div class="truncate text-sm text-[var(--color-on-surface-variant)]">{{ email() }}</div>
          </div>
        </div>

        <section class="not-first:mt-6">
          <h3 class="mb-1.5 px-1 text-xs font-semibold uppercase tracking-wide text-[var(--color-on-surface-variant)]">
            Appearance
          </h3>
          <tcn-list>
            <tcn-list-item class="cursor-pointer" (click)="appearance.setTheme('ios')">
              iOS
              @if (appearance.theme() === 'ios') {
                <span item-trailing class="text-[var(--color-primary)]">
                  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                    <path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
                  </svg>
                </span>
              }
            </tcn-list-item>
            <tcn-list-item class="cursor-pointer" (click)="appearance.setTheme('md')">
              Material
              @if (appearance.theme() === 'md') {
                <span item-trailing class="text-[var(--color-primary)]">
                  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                    <path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
                  </svg>
                </span>
              }
            </tcn-list-item>
            <tcn-list-item>
              Dark mode
              <tcn-switch
                item-trailing
                [checked]="appearance.scheme() === 'dark'"
                (checkedChange)="appearance.setScheme($event ? 'dark' : 'light')"
              />
            </tcn-list-item>
          </tcn-list>
        </section>

        <section class="not-first:mt-6">
          <h3 class="mb-1.5 px-1 text-xs font-semibold uppercase tracking-wide text-[var(--color-on-surface-variant)]">
            Notifications
          </h3>
          <tcn-list>
            <tcn-list-item>
              Push notifications
              <tcn-switch item-trailing [(checked)]="push" />
            </tcn-list-item>
            <tcn-list-item>
              Email digest
              <tcn-switch item-trailing [(checked)]="emailDigest" />
            </tcn-list-item>
          </tcn-list>
        </section>

        <section class="not-first:mt-6">
          <h3 class="mb-1.5 px-1 text-xs font-semibold uppercase tracking-wide text-[var(--color-on-surface-variant)]">
            About
          </h3>
          <tcn-list>
            <tcn-list-item chevron class="cursor-pointer">Terms of Service</tcn-list-item>
            <tcn-list-item chevron class="cursor-pointer">Privacy Policy</tcn-list-item>
            <tcn-list-item>
              Version
              <span item-trailing class="text-sm text-[var(--color-on-surface-variant)]">1.0.0</span>
            </tcn-list-item>
          </tcn-list>
        </section>

        <section class="not-first:mt-6">
          <tcn-list>
            <tcn-list-item class="cursor-pointer text-[var(--color-destructive)]" (click)="confirmOpen.set(true)">
              Sign out
            </tcn-list-item>
          </tcn-list>
        </section>
      </div>

      <tcn-dialog [(open)]="confirmOpen" heading="Sign out?" message="You will need to sign in again to access your account.">
        <button dialog-action tcnButton variant="ghost" type="button" (click)="confirmOpen.set(false)">Cancel</button>
        <button dialog-action tcnButton variant="destructive" type="button" (click)="confirmSignOut()">Sign out</button>
      </tcn-dialog>
    </tcn-page>
  `,
})
export class TcnSettingsBlock {
  protected readonly appearance = inject(TouchcnAppearance);

  readonly name = input('Ada Lovelace');
  readonly email = input('ada@example.com');
  readonly avatarSrc = input('');
  /** Renders a back control in the navbar's leading slot. */
  readonly showBack = input(false, { transform: booleanAttribute });
  /** Fires after the user confirms the sign-out dialog. */
  readonly signOut = output<void>();
  readonly back = output<void>();

  protected readonly push = signal(true);
  protected readonly emailDigest = signal(false);
  protected readonly confirmOpen = signal(false);

  protected readonly initials = computed(() =>
    this.name()
      .split(/\s+/)
      .filter(Boolean)
      .slice(0, 2)
      .map((part) => part[0]?.toUpperCase() ?? '')
      .join(''),
  );

  protected confirmSignOut(): void {
    this.confirmOpen.set(false);
    this.signOut.emit();
  }
}
Source · React
export * from './tcn-settings-block';
import { useState } from 'react';
import { useTouchcnAppearance } from '@touchcn/react';
import { TcnAvatar } from '@/components/ui/avatar';
import { TcnButton } from '@/components/ui/button';
import { TcnDialog } from '@/components/ui/dialog';
import { TcnList, TcnListItem } from '@/components/ui/list';
import { TcnNavbar } from '@/components/ui/navbar';
import { TcnPage } from '@/components/ui/page';
import { TcnSwitch } from '@/components/ui/switch';

export interface TcnSettingsBlockProps {
  name?: string;
  email?: string;
  avatarSrc?: string;
  /** Renders a back control in the navbar's leading slot. */
  showBack?: boolean;
  /** Fires after the user confirms the sign-out dialog. */
  onSignOut?(): void;
  /** Fires when the navbar back control is activated. */
  onBack?(): void;
}

const Check = () => (
  <span className="text-[var(--color-primary)]">
    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M5 13l4 4L19 7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  </span>
);

function initialsOf(name: string): string {
  return name
    .split(/\s+/)
    .filter(Boolean)
    .slice(0, 2)
    .map((part) => part[0]?.toUpperCase() ?? '')
    .join('');
}

const sectionHeading = 'mb-1.5 px-1 text-xs font-semibold uppercase tracking-wide text-[var(--color-on-surface-variant)]';

/**
 * Settings screen — a grouped-list settings surface. The appearance section is
 * wired to the live theme and scheme via `useTouchcnAppearance`; the remaining
 * toggles hold local presentational state, and sign-out is confirmed through a
 * dialog before `onSignOut` fires.
 */
export function TcnSettingsBlock({
  name = 'Ada Lovelace',
  email = 'ada@example.com',
  avatarSrc,
  showBack = false,
  onSignOut,
  onBack,
}: TcnSettingsBlockProps) {
  const { theme, scheme, setTheme, setScheme } = useTouchcnAppearance();
  const [push, setPush] = useState(true);
  const [emailDigest, setEmailDigest] = useState(false);
  const [confirmOpen, setConfirmOpen] = useState(false);

  const confirmSignOut = () => {
    setConfirmOpen(false);
    onSignOut?.();
  };

  return (
    <TcnPage>
      <TcnNavbar
        title="Settings"
        leading={
          showBack ? (
            <button type="button" className="tcn-navbar-back" aria-label="Back" onClick={() => onBack?.()}>
              <svg className="if-ios" width="11" height="18" viewBox="0 0 12 20" fill="none" aria-hidden="true">
                <path d="M10 2 2 10l8 8" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
              <svg className="if-md" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                <path d="M20 12H4m0 0 6-6m-6 6 6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
            </button>
          ) : undefined
        }
      />
      <div className="mx-auto min-h-full max-w-[430px] px-4 pt-20 pb-12">
        <div className="mb-6 flex items-center gap-4 px-1 py-2">
          <TcnAvatar src={avatarSrc} initials={initialsOf(name)} size="lg" alt={name} />
          <div className="min-w-0">
            <div className="truncate text-lg font-semibold">{name}</div>
            <div className="truncate text-sm text-[var(--color-on-surface-variant)]">{email}</div>
          </div>
        </div>

        <section className="not-first:mt-6">
          <h3 className={sectionHeading}>Appearance</h3>
          <TcnList>
            <TcnListItem className="cursor-pointer" onClick={() => setTheme('ios')} trailing={theme === 'ios' ? <Check /> : undefined}>
              iOS
            </TcnListItem>
            <TcnListItem className="cursor-pointer" onClick={() => setTheme('md')} trailing={theme === 'md' ? <Check /> : undefined}>
              Material
            </TcnListItem>
            <TcnListItem trailing={<TcnSwitch checked={scheme === 'dark'} onCheckedChange={(next) => setScheme(next ? 'dark' : 'light')} />}>
              Dark mode
            </TcnListItem>
          </TcnList>
        </section>

        <section className="not-first:mt-6">
          <h3 className={sectionHeading}>Notifications</h3>
          <TcnList>
            <TcnListItem trailing={<TcnSwitch checked={push} onCheckedChange={setPush} />}>Push notifications</TcnListItem>
            <TcnListItem trailing={<TcnSwitch checked={emailDigest} onCheckedChange={setEmailDigest} />}>Email digest</TcnListItem>
          </TcnList>
        </section>

        <section className="not-first:mt-6">
          <h3 className={sectionHeading}>About</h3>
          <TcnList>
            <TcnListItem chevron className="cursor-pointer">Terms of Service</TcnListItem>
            <TcnListItem chevron className="cursor-pointer">Privacy Policy</TcnListItem>
            <TcnListItem trailing={<span className="text-sm text-[var(--color-on-surface-variant)]">1.0.0</span>}>Version</TcnListItem>
          </TcnList>
        </section>

        <section className="not-first:mt-6">
          <TcnList>
            <TcnListItem
              className="cursor-pointer text-[var(--color-destructive)]"
              onClick={() => setConfirmOpen(true)}
            >
              Sign out
            </TcnListItem>
          </TcnList>
        </section>
      </div>

      <TcnDialog
        open={confirmOpen}
        onOpenChange={setConfirmOpen}
        heading="Sign out?"
        message="You will need to sign in again to access your account."
        actions={
          <>
            <TcnButton variant="ghost" onClick={() => setConfirmOpen(false)}>
              Cancel
            </TcnButton>
            <TcnButton variant="destructive" onClick={confirmSignOut}>
              Sign out
            </TcnButton>
          </>
        }
      />
    </TcnPage>
  );
}
Source · Vue
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useTouchcnAppearance } from '@touchcn/vue';
import { TcnAvatar } from '@/components/ui/avatar';
import { TcnButton } from '@/components/ui/button';
import { TcnDialog } from '@/components/ui/dialog';
import { TcnList, TcnListItem } from '@/components/ui/list';
import { TcnNavbar } from '@/components/ui/navbar';
import { TcnPage } from '@/components/ui/page';
import { TcnSwitch } from '@/components/ui/switch';

/**
 * Settings screen — a grouped-list settings surface. The appearance section is
 * wired to the live appearance state via `useTouchcnAppearance`; the remaining
 * toggles hold local presentational state, and sign-out is confirmed through a
 * dialog before `signOut` fires.
 */
const props = withDefaults(
  defineProps<{
    name?: string;
    email?: string;
    avatarSrc?: string;
    /** Renders a back control in the navbar's leading slot. */
    showBack?: boolean;
  }>(),
  { name: 'Ada Lovelace', email: 'ada@example.com', showBack: false },
);

const emit = defineEmits<{
  /** Fires after the user confirms the sign-out dialog. */
  signOut: [];
  /** Fires when the navbar back control is activated. */
  back: [];
}>();

const { theme, scheme, setTheme, setScheme } = useTouchcnAppearance();
const push = ref(true);
const emailDigest = ref(false);
const confirmOpen = ref(false);

const initials = computed(() =>
  props.name
    .split(/\s+/)
    .filter(Boolean)
    .slice(0, 2)
    .map((part) => part[0]?.toUpperCase() ?? '')
    .join(''),
);

const sectionHeading = 'mb-1.5 px-1 text-xs font-semibold uppercase tracking-wide text-[var(--color-on-surface-variant)]';

const confirmSignOut = (): void => {
  confirmOpen.value = false;
  emit('signOut');
};
</script>

<template>
  <TcnPage>
    <TcnNavbar title="Settings">
      <template v-if="showBack" #leading>
        <button type="button" class="tcn-navbar-back" aria-label="Back" @click="emit('back')">
          <svg class="if-ios" width="11" height="18" viewBox="0 0 12 20" fill="none" aria-hidden="true">
            <path d="M10 2 2 10l8 8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
          </svg>
          <svg class="if-md" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
            <path d="M20 12H4m0 0 6-6m-6 6 6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
          </svg>
        </button>
      </template>
    </TcnNavbar>
    <div class="mx-auto min-h-full max-w-[430px] px-4 pt-20 pb-12">
      <div class="mb-6 flex items-center gap-4 px-1 py-2">
        <TcnAvatar :src="avatarSrc" :initials="initials" size="lg" :alt="name" />
        <div class="min-w-0">
          <div class="truncate text-lg font-semibold">{{ name }}</div>
          <div class="truncate text-sm text-[var(--color-on-surface-variant)]">{{ email }}</div>
        </div>
      </div>

      <section class="not-first:mt-6">
        <h3 :class="sectionHeading">Appearance</h3>
        <TcnList>
          <TcnListItem class="cursor-pointer" @click="setTheme('ios')">
            <template v-if="theme === 'ios'" #trailing>
              <span class="text-[var(--color-primary)]">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                  <path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
                </svg>
              </span>
            </template>
            iOS
          </TcnListItem>
          <TcnListItem class="cursor-pointer" @click="setTheme('md')">
            <template v-if="theme === 'md'" #trailing>
              <span class="text-[var(--color-primary)]">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                  <path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
                </svg>
              </span>
            </template>
            Material
          </TcnListItem>
          <TcnListItem>
            <template #trailing>
              <TcnSwitch
                :model-value="scheme === 'dark'"
                @update:model-value="(next) => setScheme(next ? 'dark' : 'light')"
              />
            </template>
            Dark mode
          </TcnListItem>
        </TcnList>
      </section>

      <section class="not-first:mt-6">
        <h3 :class="sectionHeading">Notifications</h3>
        <TcnList>
          <TcnListItem>
            <template #trailing><TcnSwitch v-model="push" /></template>
            Push notifications
          </TcnListItem>
          <TcnListItem>
            <template #trailing><TcnSwitch v-model="emailDigest" /></template>
            Email digest
          </TcnListItem>
        </TcnList>
      </section>

      <section class="not-first:mt-6">
        <h3 :class="sectionHeading">About</h3>
        <TcnList>
          <TcnListItem chevron class="cursor-pointer">Terms of Service</TcnListItem>
          <TcnListItem chevron class="cursor-pointer">Privacy Policy</TcnListItem>
          <TcnListItem>
            <template #trailing>
              <span class="text-sm text-[var(--color-on-surface-variant)]">1.0.0</span>
            </template>
            Version
          </TcnListItem>
        </TcnList>
      </section>

      <section class="not-first:mt-6">
        <TcnList>
          <TcnListItem class="cursor-pointer text-[var(--color-destructive)]" @click="confirmOpen = true">
            Sign out
          </TcnListItem>
        </TcnList>
      </section>
    </div>

    <TcnDialog
      v-model:open="confirmOpen"
      heading="Sign out?"
      message="You will need to sign in again to access your account."
    >
      <template #actions>
        <TcnButton variant="ghost" @click="confirmOpen = false">Cancel</TcnButton>
        <TcnButton variant="destructive" @click="confirmSignOut">Sign out</TcnButton>
      </template>
    </TcnDialog>
  </TcnPage>
</template>
export { default as TcnSettingsBlock } from './TcnSettingsBlock.vue';

Last updated on August 16, 2026

Was this page helpful?