VP Social Login - Styling the profile page (optional)

An optional recipe that gives the profile page — including the Connected accounts panel — a polished look: tabbed sections, cards and a tidy password field. It's a template override, so it lives in your template, not in the extension.

This step is entirely optional. The Connected accounts panel works on the default Joomla profile page without it. Use this only if you want the styled layout.

What it does

  • Groups the profile fieldsets into tabs with a card layout.
  • Fixes the password show/hide icon and field spacing.
  • Optionally hides sections front-end members don't need (Multi-factor Authentication, Joomla API Token, User Actions Log).
  • Sends Save and Cancel to a page of your choice, so Cancel never lands on an empty profile view.

Install the override

You can either download the file or copy the code below.

  1. Take the edit.php file (download button below, or copy the code).
  2. Place it at templates/YOUR_TEMPLATE/html/com_users/profile/edit.php — replace YOUR_TEMPLATE with your site template (for example cassiopeia). Create the folders if they don't exist.
  3. Clear the Joomla cache.

Download edit.php

Configure it

Three settings at the top of the file:

Setting What it does
$redirect Where Save and Cancel go. Defaults to the site home; point it at your account page, e.g. Uri::root() . 'account'.
$hideLabels Fieldsets to hide, by their visible label. Empty the array to show everything.
$showMfa Set to true to show the Multi-factor Authentication block.

This overrides a Joomla core layout. After major Joomla updates, check that the profile page still renders as expected.

The file

Copy the full file:

<?php

/**
 * Template override — com_users profile edit form.
 *
 * Organises the profile fieldsets into tabs, tidies up spacing and fixes the
 * password show/hide icon. Optionally hides sections that aren't relevant for
 * front-end members, and sends Save / Cancel to a page of your choice.
 *
 * Place at: templates/YOUR_TEMPLATE/html/com_users/profile/edit.php
 * (replace YOUR_TEMPLATE with your site template, e.g. cassiopeia)
 */

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;

/** @var Joomla\Component\Users\Site\View\Profile\HtmlView $this */

HTMLHelper::_('bootstrap.tooltip', '.hasTooltip');
$this->getLanguage()->load('plg_user_profile', JPATH_ADMINISTRATOR);

/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->getDocument()->getWebAssetManager();
$wa->useScript('keepalive')->useScript('form.validate')->useScript('bootstrap.tab');

// ---------------------------------------------------------------------------
// Config — tweak freely.
// ---------------------------------------------------------------------------
// Where Save and Cancel should go. Default: the site home page. Point it at
// your account/dashboard page if you have one, e.g. Uri::root() . 'account'.
// This also avoids Cancel landing on an empty page when the site has no
// "User Profile" menu item.
$redirect = Uri::root();

// Hide these fieldsets by their visible (translated) label. Empty the array
// to show everything.
$hideLabels = ['User Actions Log Options', 'Joomla API Token'];

// Show the Multi-factor Authentication block?
$showMfa = false;
// ---------------------------------------------------------------------------

$app  = Factory::getApplication();
$menu = $app->getMenu()->getActive();

// After Save, return to the chosen page instead of the bare profile view.
$app->setUserState('com_users.edit.profile.redirect', $redirect);

// Page title (menu item / page heading; never a raw language constant).
$title = $this->params->get('show_page_heading') && $this->params->get('page_heading')
    ? $this->escape($this->params->get('page_heading'))
    : ($menu ? $this->escape($menu->title) : 'Edit Profile');

// Build the list of tabs from the form fieldsets.
$tabs = [];

foreach ($this->form->getFieldsets() as $name => $fieldset) {
    $fields = $this->form->getFieldset($name);

    if (!count($fields)) {
        continue;
    }

    $label = isset($fieldset->label) ? Text::_($fieldset->label) : $name;

    if (in_array($label, $hideLabels, true)) {
        continue;
    }

    $tabs[] = ['label' => $label, 'fields' => $fields, 'mfa' => false];
}

if ($showMfa && $this->mfaConfigurationUI) {
    $tabs[] = ['label' => Text::_('COM_USERS_PROFILE_MULTIFACTOR_AUTH'), 'fields' => [], 'mfa' => true];
}
?>
<style>
    .vp-profile-edit { max-width: 820px; margin: 2rem auto; }
    .vp-profile-edit__title { font-size: 1.6rem; font-weight: 800; margin-bottom: 1.25rem; }
    .vp-profile-tabs { border-bottom: 1px solid #e2e8f0; gap: .25rem; flex-wrap: wrap; margin-bottom: 0; padding-left: 0; }
    .vp-profile-tabs .nav-link { border: none; border-bottom: 2px solid transparent; border-radius: 0; color: #64748b; font-weight: 600; padding: .65rem 1.05rem; background: none; }
    .vp-profile-tabs .nav-link:hover { color: #1d4ed8; }
    .vp-profile-tabs .nav-link.active { color: #1d4ed8; border-bottom-color: #1d4ed8; background: none; }
    .vp-profile-panels { background: #fff; border: 1px solid #e2e8f0; border-top: none; border-radius: 0 0 .85rem .85rem; }
    .vp-profile-panels__body { padding: 1.5rem; }
    .vp-profile-edit .control-group { margin-bottom: 1.1rem; }
    .vp-profile-edit .control-group:last-child { margin-bottom: 0; }
    .vp-profile-edit .control-label { display: block; font-weight: 600; margin-bottom: .35rem; }
    .vp-profile-edit .control-label label { font-weight: 600; margin: 0; }
    .vp-profile-edit .controls input[type="text"],
    .vp-profile-edit .controls input[type="email"],
    .vp-profile-edit .controls input[type="password"],
    .vp-profile-edit .controls input[type="tel"],
    .vp-profile-edit .controls input[type="url"],
    .vp-profile-edit .controls select,
    .vp-profile-edit .controls textarea { width: 100%; }
    .vp-profile-edit .password-group .input-group { flex-wrap: nowrap; }
    .vp-profile-edit .password-group .input-group > .form-control { flex: 1 1 auto; width: 1%; }
    /* Password show/hide toggle — own eye icon (Joomla's icon font is unstyled here). */
    .vp-profile-edit .input-password-toggle {
        flex: 0 0 auto;
        background: #fff;
        border: 1px solid #cbd5e1;
        color: #64748b;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        padding: 0 .75rem;
    }
    .vp-profile-edit .input-password-toggle:hover { background: #f1f5f9; color: #1d4ed8; }
    .vp-profile-edit .input-password-toggle .icon-eye,
    .vp-profile-edit .input-password-toggle .icon-eye-slash {
        width: 1.15rem;
        height: 1.15rem;
        display: inline-block;
        background-color: currentColor;
        -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
        -webkit-mask-position: center; mask-position: center;
        -webkit-mask-size: contain; mask-size: contain;
    }
    .vp-profile-edit .input-password-toggle .icon-eye::before,
    .vp-profile-edit .input-password-toggle .icon-eye-slash::before { content: ""; display: none; }
    .vp-profile-edit .input-password-toggle .icon-eye {
        -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8M1.173 8a13 13 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5s3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5s-3.879-1.168-5.168-2.457A13 13 0 0 1 1.172 8z'/%3E%3Cpath d='M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0'/%3E%3C/svg%3E");
        mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8M1.173 8a13 13 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5s3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5s-3.879-1.168-5.168-2.457A13 13 0 0 1 1.172 8z'/%3E%3Cpath d='M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0'/%3E%3C/svg%3E");
    }
    .vp-profile-edit .input-password-toggle .icon-eye-slash {
        -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m10.79 12.912-1.614-1.615a3.5 3.5 0 0 1-4.474-4.474l-2.06-2.06C.938 6.278 0 8 0 8s3 5.5 8 5.5a7 7 0 0 0 2.79-.588M5.21 3.088A7 7 0 0 1 8 2.5c5 0 8 5.5 8 5.5s-.939 1.721-2.641 3.238l-2.062-2.062a3.5 3.5 0 0 0-4.474-4.474z'/%3E%3Cpath d='M5.525 7.646a2.5 2.5 0 0 0 2.829 2.829zm4.95.708-2.83-2.83a2.5 2.5 0 0 1 2.83 2.83zm3.171 6-12-12 .708-.708 12 12z'/%3E%3C/svg%3E");
        mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m10.79 12.912-1.614-1.615a3.5 3.5 0 0 1-4.474-4.474l-2.06-2.06C.938 6.278 0 8 0 8s3 5.5 8 5.5a7 7 0 0 0 2.79-.588M5.21 3.088A7 7 0 0 1 8 2.5c5 0 8 5.5 8 5.5s-.939 1.721-2.641 3.238l-2.062-2.062a3.5 3.5 0 0 0-4.474-4.474z'/%3E%3Cpath d='M5.525 7.646a2.5 2.5 0 0 0 2.829 2.829zm4.95.708-2.83-2.83a2.5 2.5 0 0 1 2.83 2.83zm3.171 6-12-12 .708-.708 12 12z'/%3E%3C/svg%3E");
    }
    .vp-profile-edit__actions { display: flex; gap: .75rem; align-items: center; margin-top: 1.5rem; }
    .vp-profile-edit__actions .btn { min-width: 120px; }
</style>

<div class="com-users-profile__edit vp-profile-edit">
    <h1 class="vp-profile-edit__title"><?php echo $title; ?></h1>

    <form id="member-profile" action="<?php echo Route::_('index.php'); ?>" method="post"
          class="com-users-profile__edit-form form-validate" enctype="multipart/form-data">

        <ul class="nav nav-tabs vp-profile-tabs" role="tablist">
            <?php foreach ($tabs as $i => $tab) : ?>
                <li class="nav-item" role="presentation">
                    <button class="nav-link<?php echo $i === 0 ? ' active' : ''; ?>"
                            id="vptab-<?php echo $i; ?>-tab" data-bs-toggle="tab"
                            data-bs-target="#vptab-<?php echo $i; ?>" type="button" role="tab"
                            aria-controls="vptab-<?php echo $i; ?>"
                            aria-selected="<?php echo $i === 0 ? 'true' : 'false'; ?>">
                        <?php echo $this->escape($tab['label']); ?>
                    </button>
                </li>
            <?php endforeach; ?>
        </ul>

        <div class="tab-content vp-profile-panels">
            <?php foreach ($tabs as $i => $tab) : ?>
                <div class="tab-pane fade<?php echo $i === 0 ? ' show active' : ''; ?>"
                     id="vptab-<?php echo $i; ?>" role="tabpanel"
                     aria-labelledby="vptab-<?php echo $i; ?>-tab" tabindex="0">
                    <div class="vp-profile-panels__body">
                        <?php if (!empty($tab['mfa'])) : ?>
                            <?php echo $this->mfaConfigurationUI; ?>
                        <?php else : ?>
                            <?php foreach ($tab['fields'] as $field) : ?>
                                <?php echo $field->renderField(); ?>
                            <?php endforeach; ?>
                        <?php endif; ?>
                    </div>
                </div>
            <?php endforeach; ?>
        </div>

        <div class="vp-profile-edit__actions">
            <button type="submit" class="btn btn-primary validate" name="task" value="profile.save">
                <?php echo Text::_('JSAVE'); ?>
            </button>
            <a class="btn btn-outline-secondary" href="/<?php echo htmlspecialchars($redirect, ENT_QUOTES, 'UTF-8'); ?>">
                <?php echo Text::_('JCANCEL'); ?>
            </a>
            <input type="hidden" name="option" value="com_users">
        </div>

        <?php echo HTMLHelper::_('form.token'); ?>
    </form>
</div>