Account Settings Page

Updated:

How to build an Account Settings page in Tecton: six independent settings sections covering nickname, notifications, statement delivery, card management, spending limits, and overdraft protection.

An Account Settings page groups six independent settings sections on one screen, each managing its own state. A change in one section never affects another. Sections that have no validation need no JavaScript at all; sections with structured input validate on Save and stop there. A real application would submit the validated values to an API at that point.

The section order follows UX research on settings page information architecture. Personalisation (Nickname) appears first. Related communication settings (Notifications, Statement Delivery) are grouped next. Card controls (Card Management, Spending Limits) are co-located because users adjust them together. Overdraft Protection appears last as the highest-stakes, least-frequent action.

Template

Account Nickname

Give your account a name that makes it easy to identify at a glance.

Save Nickname

Notification Preferences

Choose which alerts you want to receive for this account.

Statement Delivery

Choose how you would like to receive your monthly statements.

Save Statement Delivery

Card Management

Temporarily freeze your debit card to prevent new transactions, or report it lost or stolen.

Spending Limits

Set daily limits for ATM withdrawals and purchases on this account.

Save Spending Limits

Overdraft Protection

Link a backup account to cover transactions when your balance is low.

Overdraft transfers draw from the linked account immediately. Review your linked account before saving.
Family Checking
Money Market
Emergency Savings
Business Checking
Home Equity Line
Save Overdraft Protection
<q2-section label="Basic Settings">
  <!-- Account Nickname -->
  <q2-grid columns="1" md-columns="5" gap="comfortable">
    <q2-grid-area column-span="2">
      <h3>Account Nickname</h3>
      <p>Give your account a name that makes it easy to identify at a glance.</p>
    </q2-grid-area>
    <q2-grid-area column-span="3">
      <q2-input label="Account Nickname" value="My Checking Account"></q2-input>
      <q2-action-group>
        <q2-btn id="nickname-save" intent="workflow-primary">Save Nickname</q2-btn>
      </q2-action-group>
    </q2-grid-area>
  </q2-grid>

  <!-- Notification Preferences -->
  <q2-grid columns="1" md-columns="5" gap="comfortable" style="margin-top: var(--app-scale-12x);">
    <q2-grid-area column-span="2">
      <h3>Notification Preferences</h3>
      <p>Choose which alerts you want to receive for this account.</p>
    </q2-grid-area>
    <q2-grid-area column-span="3">
      <q2-checkbox id="notif-low-balance" label="Low Balance Alert" description="Notify me when my balance falls below $100." type="toggle" alignment="right">
      </q2-checkbox>
      <q2-checkbox id="notif-large-transaction" label="Large Transaction Alert" description="Notify me when a transaction exceeds $500." type="toggle" alignment="right">
      </q2-checkbox>
      <q2-checkbox id="notif-monthly-statement" label="Monthly Statement" description="Send me an email when my monthly statement is ready." type="toggle" alignment="right" checked>
      </q2-checkbox>
    </q2-grid-area>
  </q2-grid>

  <!-- Statement Delivery -->
  <q2-grid columns="1" md-columns="5" gap="comfortable" style="margin-top: var(--app-scale-12x);">
    <q2-grid-area column-span="2">
      <h3>Statement Delivery</h3>
      <p>Choose how you would like to receive your monthly statements.</p>
    </q2-grid-area>
    <q2-grid-area column-span="3">
      <q2-radio-group id="statement-delivery" label="Delivery Method" name="statement-delivery" value="electronic">
        <q2-radio label="Electronic (email)" value="electronic"></q2-radio>
        <q2-radio label="Paper (mail)" value="paper"></q2-radio>
      </q2-radio-group>
      <q2-input id="statement-email" label="Email Address" type="email" value="bill.banker@my-domain.com"></q2-input>
      <q2-action-group>
        <q2-btn id="statement-save" intent="workflow-primary">Save Statement Delivery</q2-btn>
      </q2-action-group>
    </q2-grid-area>
  </q2-grid>

  <!-- Card Management -->
  <q2-grid columns="1" md-columns="5" gap="comfortable" style="margin-top: var(--app-scale-12x);">
    <q2-grid-area column-span="2">
      <h3>Card Management</h3>
      <p>Temporarily freeze your debit card to prevent new transactions, or report it lost or stolen.</p>
    </q2-grid-area>
    <q2-grid-area column-span="3">
      <q2-checkbox id="card-freeze" label="Freeze Debit Card" description="Prevent new purchases, ATM withdrawals, and transfers until unfrozen." type="toggle" alignment="right">
      </q2-checkbox>
      <q2-action-group>
        <q2-link variant="standalone" label="Report card lost or stolen" icon-type="card-unknown" href="#"></q2-link>
      </q2-action-group>
    </q2-grid-area>
  </q2-grid>
</q2-section>

<!-- Advanced Settings: Spending Limits + Overdraft Protection -->
<q2-section label="Advanced Settings" collapsible>

  <!-- Spending Limits -->
  <q2-grid columns="1" md-columns="5" gap="comfortable">
    <q2-grid-area column-span="2">
      <h3>Spending Limits</h3>
      <p>Set daily limits for ATM withdrawals and purchases on this account.</p>
    </q2-grid-area>
    <q2-grid-area column-span="3">
      <q2-form>
      <q2-input id="atm-limit" label="Daily ATM Withdrawal Limit" type="currency" value="500"></q2-input>
      <q2-input id="purchase-limit" label="Daily Purchase Limit" type="currency" value="2500"></q2-input>
        </q2-form>
      <q2-action-group>
        <q2-btn id="limits-save" intent="workflow-primary">Save Spending Limits</q2-btn>
      </q2-action-group>
    </q2-grid-area>
  </q2-grid>


  <q2-card elevation="2" bar="warning" style="margin-top: var(--app-scale-12x);">
    <q2-grid columns="1" md-columns="5" gap="comfortable">
      <q2-grid-area column-span="2">
        <h3>Overdraft Protection</h3>
        <p>Link a backup account to cover transactions when your balance is low.</p>
      </q2-grid-area>
      <q2-grid-area column-span="3">
        <q2-message type="warning" style="margin-bottom: var(--app-scale-3x)">
          Overdraft transfers draw from the linked account immediately. Review your linked account before saving.
        </q2-message>
        <q2-select id="overdraft-select" label="Linked Account" value="3" optional multiline-options>
          <q2-option value="1" display="Family Checking">
            <q2-item>
              <q2-avatar slot="decorator" icon="people-group"></q2-avatar>
              <div slot="header">Family Checking</div>
              <q2-detail label="Account Number" description="xxxx-xxx-9538" slot="body"></q2-detail>
              <q2-detail label="Account Type" description="Checking" slot="body"></q2-detail>
            </q2-item>
          </q2-option>
          <q2-option value="2" display="Money Market">
            <q2-item>
              <q2-avatar slot="decorator" icon="linechart-trend"></q2-avatar>
              <div slot="header">Money Market</div>
              <q2-detail label="Account Number" description="xxxx-xxx-9424" slot="body"></q2-detail>
              <q2-detail label="Account Type" description="Investment" slot="body"></q2-detail>
            </q2-item>
          </q2-option>
          <q2-option value="3" display="Emergency Savings">
            <q2-item>
              <q2-avatar slot="decorator" icon="piggy-bank"></q2-avatar>
              <div slot="header">Emergency Savings</div>
              <q2-detail label="Account Number" description="xxxx-xxx-0745" slot="body"></q2-detail>
              <q2-detail label="Account Type" description="Savings" slot="body"></q2-detail>
            </q2-item>
          </q2-option>
          <q2-option value="4" display="Business Checking">
            <q2-item>
              <q2-avatar slot="decorator" icon="buildings"></q2-avatar>
              <div slot="header">Business Checking</div>
              <q2-detail label="Account Number" description="xxxx-xxx-9402" slot="body"></q2-detail>
              <q2-detail label="Account Type" description="Checking" slot="body"></q2-detail>
            </q2-item>
          </q2-option>
          <q2-option value="5" display="Home Equity Line">
            <q2-item>
              <q2-avatar slot="decorator" icon="home-chimney"></q2-avatar>
              <div slot="header">Home Equity Line</div>
              <q2-detail label="Account Number" description="xxxx-xxx-1094" slot="body"></q2-detail>
              <q2-detail label="Account Type" description="Loan" slot="body"></q2-detail>
            </q2-item>
          </q2-option>
        </q2-select>
        <q2-action-group>
          <q2-btn id="overdraft-save" intent="workflow-primary">Save Overdraft Protection</q2-btn>
        </q2-action-group>
      </q2-grid-area>
    </q2-grid>
  </q2-card>
</q2-section>

<script>
// Statement Delivery: show the email field only when electronic delivery is selected.
const statementDelivery = document.querySelector('#statement-delivery');
const statementEmail = document.querySelector('#statement-email');
const statementSave = document.querySelector('#statement-save');

const statementState = {
    method: 'electronic',
    email: 'bill.banker@my-domain.com',
};

// Toggles the email field based on the selected delivery method.
function updateStatementForm() {
    statementEmail.hidden = statementState.method !== 'electronic';
}

statementDelivery.addEventListener('tctChange', event => {
    statementState.method = event?.detail?.value ?? 'electronic';
    updateStatementForm();
});

statementEmail.addEventListener('tctInput', event => {
    statementState.email = event?.detail?.value ?? '';
    if (statementEmail.errors?.length) statementEmail.errors = [];
});

// Save validates only. A real application would submit the delivery preferences here.
statementSave.addEventListener('tctClick', () => {
    if (statementState.method === 'electronic' && !statementState.email.trim()) {
        statementEmail.errors = ['Email address is required for electronic delivery.'];
        return;
    }
});

// Initial render: sync email field visibility with the default radio selection.
updateStatementForm();

// Spending Limits: validate both amounts are greater than 0.
const atmLimit = document.querySelector('#atm-limit');
const purchaseLimit = document.querySelector('#purchase-limit');
const limitsSave = document.querySelector('#limits-save');

const limitsState = {
    atm: '500',
    purchase: '2500',
};

// Validates both limit fields and returns true when both pass.
function validateLimits() {
    let valid = true;

    const atmValue = parseFloat(limitsState.atm);
    if (!limitsState.atm || isNaN(atmValue) || atmValue <= 0) {
        atmLimit.errors = ['Enter a limit greater than $0.'];
        valid = false;
    }

    const purchaseValue = parseFloat(limitsState.purchase);
    if (!limitsState.purchase || isNaN(purchaseValue) || purchaseValue <= 0) {
        purchaseLimit.errors = ['Enter a limit greater than $0.'];
        valid = false;
    }

    return valid;
}

atmLimit.addEventListener('tctInput', event => {
    limitsState.atm = event?.detail?.value ?? '';
    if (atmLimit.errors?.length) atmLimit.errors = [];
});

purchaseLimit.addEventListener('tctInput', event => {
    limitsState.purchase = event?.detail?.value ?? '';
    if (purchaseLimit.errors?.length) purchaseLimit.errors = [];
});

// Save validates only. A real application would submit the spending limits here.
limitsSave.addEventListener('tctClick', () => {
    if (!validateLimits()) return;
});
</script>

How it works

The page holds six independent settings sections. Each section owns its state and responds to user input without affecting the others. Only two of them need JavaScript in this example:

  • Validated on save (Statement Delivery, Spending Limits): the user makes changes and clicks Save, and validation runs once on click. The example stops once the input is valid, which is the point at which a real application would send the data to an API.
  • No validation (Account Nickname, Notification Preferences, Card Management, Overdraft Protection): toggles, a plain input, and an optional select with nothing to check. These need no JavaScript here; a real application would submit each change, or each Save, directly.

Account Nickname

The nickname input has a Save button but no validation rule. In a real application, the Save handler would read the input's current value and submit it to an API. Nothing here requires JavaScript.

Notification Preferences

Each toggle is an independent q2-checkbox with type="toggle". Toggling one has no effect on the others. In a real application, each tctChange handler would submit the new value immediately. No Save button is needed.

Statement Delivery: conditional field

The Statement Delivery section shows the email address field only when the user selects electronic delivery. updateStatementForm() toggles the hidden attribute on the email input and runs on initial render so the field matches the default radio selection from the start.

function updateStatementForm() {
    statementEmail.hidden = statementState.method !== 'electronic';
}

statementDelivery.addEventListener('tctChange', event => {
    statementState.method = event?.detail?.value ?? 'electronic';
    updateStatementForm();
});

Validation runs only when the user clicks Save and only applies the email-required rule when electronic delivery is active.

statementSave.addEventListener('tctClick', () => {
    if (statementState.method === 'electronic' && !statementState.email.trim()) {
        statementEmail.errors = ['Email address is required for electronic delivery.'];
        return;
    }
});

Card Management

The freeze toggle is a plain q2-checkbox. The "Report card lost or stolen" link leads to a separate flow. Neither control has validation, so no JavaScript is needed for this section.

Spending Limits: validation

Both currency inputs use type="currency", which formats values as USD as the user types. The raw numeric value is available on event.detail.value from tctInput. validateLimits() checks that each value is non-empty, parseable as a number, and greater than zero.

function validateLimits() {
    let valid = true;

    const atmValue = parseFloat(limitsState.atm);
    if (!limitsState.atm || isNaN(atmValue) || atmValue <= 0) {
        atmLimit.errors = ['Enter a limit greater than $0.'];
        valid = false;
    }

    const purchaseValue = parseFloat(limitsState.purchase);
    if (!limitsState.purchase || isNaN(purchaseValue) || purchaseValue <= 0) {
        purchaseLimit.errors = ['Enter a limit greater than $0.'];
        valid = false;
    }

    return valid;
}

Setting errors to a non-empty array activates a field's error state. Setting it back to [] clears it. Each field checks errors?.length before clearing, so the property is only written when an error is actually present.

// Save validates only. A real application would submit the spending limits here.
limitsSave.addEventListener('tctClick', () => {
    if (!validateLimits()) return;
});

Overdraft Protection

Overdraft Protection appears last as the highest-stakes, least-frequent action on the page. The q2-select uses multiline-options, so each option renders a full q2-item with an avatar, masked account number, and account type. The field is optional, so leaving it empty is always valid and requires no validation before Save.

The static q2-message with type="warning" provides context about how overdraft transfers work. It is always visible and requires no interaction.

Related