Profile

Let signed-in users view and edit their profile. Compose your own form field-by-field with <u-profile> + <u-field>, or drop in <u-full-profile> for a complete UI in one tag. Custom attributes, SSO navigation to connected services, and account deletion round it out.

View this guide as Markdown

Composed profile form

u-profile loads and saves the data; each u-field renders one profile field with validation, labels and the right input type (date picker, phone, country select with flags). You own the layout — group fields in any markup you like.

  • Field-by-field composition — u-field per profile attribute, your layout
  • Typed inputs — dates, phone numbers, country select with icons
  • Validation + labels — render-default-label and built-in error handling
<u-signed-in>
  <!-- u-profile loads the user's data; each u-field binds one profile field.
       partial-validation limits validation to the fields rendered here. -->
  <u-profile partial-validation="true">
    <u-field field="salutation" render-default-label="true"></u-field>
    <div class="grid gap-x-4 sm:grid-cols-2">
      <u-field field="first_name" render-default-label="true"></u-field>
      <u-field field="last_name" render-default-label="true"></u-field>
      <u-field field="date_of_birth" required="true" render-default-label="true"></u-field>
      <u-field field="phone_number" render-default-label="true"></u-field>
    </div>
    <u-field field="country_code" country-code-display-option="icon" render-default-label="true"
    ></u-field>

    <u-submit-button
      class-name="mt-4 px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg disabled:opacity-50 w-full cursor-pointer">
      Save changes
    </u-submit-button>
  </u-profile>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to edit your profile.
  </p>
</u-signed-in>
Live demo
Save changes

Sign in on the Auth page to edit your profile.

Full profile in one tag

u-full-profile renders a complete, ready-made profile editor. Use the fields attribute to pick and order fields (including custom_attributes.*), or omit it to show everything the tenant defines. The fastest path to a working account page.

  • One component — complete profile UI with zero markup
  • Field selection — fields="first_name,last_name,…" picks and orders
<u-signed-in>
  <!-- One component, complete profile UI. Limit and order fields with the
       fields attribute — or omit it to render everything the tenant defines. -->
  <u-full-profile
    fields="first_name,last_name,date_of_birth,country_code"
    country-code-display-option="icon"></u-full-profile>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to see the full profile
    component.
  </p>
</u-signed-in>
Live demo

Sign in on the Auth page to see the full profile component.

Custom attributes

Tenant-defined custom attributes behave like normal fields via u-field — or use u-raw-field to build fully custom inputs. These sport chips are plain checkboxes bound to a multi-value attribute, styled with CSS only.

  • custom_attributes.* fields — text, select, multi-select, radio groups
  • u-raw-field escape hatch — any UI you can build with native inputs
<u-signed-in>
  <u-profile partial-validation="true">
    <u-field field="custom_attributes.fav_player" render-default-label="true"></u-field>

    <p class="mb-2 mt-4 text-sm font-medium">Fan interests</p>
    <u-field field="custom_attributes.fan_interests"></u-field>

    <div class="mt-4">
      <u-field field="custom_attributes.jersey_size" render-default-label="true"></u-field>
    </div>

    <u-submit-button
      class-name="mt-4 px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg disabled:opacity-50 w-full cursor-pointer">
      Save changes
    </u-submit-button>
  </u-profile>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to edit your profile.
  </p>
</u-signed-in>

<style is:inline>
  /* Chip-style for the multi-select fan_interests field */
  u-field[field="custom_attributes.fan_interests"]::part(multi-select-group_field) {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding: 0;
    border: none;
    background: transparent;
  }
  u-field[field="custom_attributes.fan_interests"]::part(multi-select-item_label) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 90px;
    height: 2.5rem;
    padding: 0 0.875rem;
    border: 1px solid var(--color-border);
    border-radius: 9999px;
    background: var(--color-white);
    font-size: 0.875rem;
    cursor: pointer;
    transition: all var(--transition-normal);
  }
  u-field[field="custom_attributes.fan_interests"]::part(multi-select-item_label):hover {
    border-color: var(--color-primary);
    background: var(--color-primary-soft);
  }
  /* NOTE: the SDK exposes no "checked" part for multi-selects (only radio_checked
     exists), so the checkbox itself is the only visible selection indicator.
     Keep it visible and brand-colored instead of hiding it. */
  u-field[field="custom_attributes.fan_interests"]::part(multi-select-item_checkbox) {
    width: 1rem;
    height: 1rem;
    margin: 0 0.5rem 0 0;
    accent-color: var(--color-primary);
    cursor: pointer;
  }
  u-field[field="custom_attributes.fan_interests"]::part(field_label) {
    display: none;
  }
</style>
Live demo

Fan interests

Save changes

Sign in on the Auth page to edit your profile.

Header profile icon

The classic header pattern: u-signed-in swaps a sign-in link for an avatar menu with profile and logout entries. Pure CSS hover dropdown — no JS required.

  • Signed-in/out switch — u-signed-in / u-signed-in not
  • u-logout-button anywhere — style it like any menu item
<!-- A header user-menu: avatar button with a hover dropdown.
     u-signed-in / u-signed-in not switch between menu and sign-in link. -->
<div class="flex justify-end">
  <u-signed-in>
    <div class="group relative inline-block">
      <button
        class="flex items-center justify-center rounded-full border border-border bg-white p-2 shadow-sm hover:bg-background-light"
        aria-label="User menu">
        <svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <circle cx="12" cy="8" r="4"></circle>
          <path d="M4 20c0-3.5 3.5-6 8-6s8 2.5 8 6"></path>
        </svg>
      </button>

      <!-- hover bridge so the menu doesn't close between button and panel -->
      <div class="absolute left-0 right-0 top-full h-2"></div>

      <div
        class="absolute right-0 top-full z-20 mt-2 hidden w-48 rounded-lg border border-border bg-white shadow-xl group-hover:block">
        <a href="/profile" class="block px-4 py-2 text-sm hover:bg-background-light"
          >Profile settings</a
        >
        <div class="border-t border-border"></div>
        <u-logout-button
          class-name="block w-full text-left px-4 py-2 text-sm hover:bg-background-light cursor-pointer">
          Sign out
        </u-logout-button>
      </div>
    </div>
  </u-signed-in>

  <u-signed-in not>
    <a href="/auth" class="btn btn-primary !min-h-0 !px-4 !py-2 text-sm">Sign in</a>
  </u-signed-in>
</div>
Live demo

SSO navigation & account deletion

u-jump-to-service signs the user into another OAuth app of your tenant; u-jump-to-unidy generates a one-time login token and opens Unidy's hosted pages — including the profile page where account deletion lives. no-auth skips the token for public pages.

  • Service SSO — jump into connected apps without re-login
  • Hosted Unidy pages — subscriptions, tickets, terms, profile
  • Account deletion — deep-link to /profile#logindata
<u-signed-in>
  <div class="flex flex-col gap-3">
    <!-- SSO into another OAuth app connected to your tenant -->
    <u-jump-to-service
      service-id="138"
      newtab
      class-name="px-4 py-2 bg-accent hover:bg-accent-hover text-white text-sm font-medium rounded-lg cursor-pointer disabled:opacity-50">
      Open connected service (SSO)
    </u-jump-to-service>

    <!-- One-time-token jump into Unidy's hosted pages -->
    <u-jump-to-unidy
      path="/subscriptions"
      newtab
      class-name="px-4 py-2 border border-border text-sm font-medium rounded-lg cursor-pointer hover:bg-background-light disabled:opacity-50">
      Manage subscriptions in Unidy
    </u-jump-to-unidy>

    <!-- Pages that need no authentication: add no-auth -->
    <u-jump-to-unidy
      path="/terms_of_use"
      no-auth
      newtab
      class-name="text-sm text-text-light underline cursor-pointer hover:text-text">
      Terms of use (no auth)
    </u-jump-to-unidy>

    <!-- Account deletion lives on Unidy's profile page — deep-link straight to it -->
    <u-jump-to-unidy
      path="/profile#logindata"
      newtab
      class-name="px-4 py-2 bg-danger hover:bg-danger-hover text-white text-sm font-medium rounded-lg cursor-pointer disabled:opacity-50">
      Delete my account
    </u-jump-to-unidy>
  </div>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to use SSO navigation.
  </p>
</u-signed-in>
Live demo
Open connected service (SSO) Manage subscriptions in Unidy Terms of use (no auth) Delete my account

Sign in on the Auth page to use SSO navigation.

Connected services

Show which third-party services a user has connected, and let them connect or disconnect in place. The connect flow runs on the /partner page (which holds the u-oauth-provider); this card checks the live connection state via the service_connections endpoint.

  • Connect / disconnect — visual state card with one-click actions
  • service_connections API — live connection state, no local guessing
  • u-signed-in gating — only shown to authenticated users
{clientId ? (
  <>
    <u-signed-in>
      <div id="pfProvider" class="space-y-3" data-client-id={clientId}>

        <!-- Disconnected state -->
        <div id="pfLocked" class="flex items-center justify-between p-4 rounded-xl border border-border bg-background-light">
          <div class="flex items-center gap-3">
            <div class="w-9 h-9 rounded-lg bg-[#20272f] flex items-center justify-center flex-shrink-0">
              <img src="/images/rbsalzburg-logo.png" alt="" class="w-7 h-7" />
            </div>
            <div>
              <p class="font-semibold text-sm">Red Bull Salzburg Fan Portal</p>
              <p class="text-xs text-text-muted">Get personalized matchday content</p>
            </div>
          </div>
          <a
            href="/partner"
            class="text-xs font-medium px-3 py-1.5 rounded-lg bg-primary text-white hover:bg-primary-dark transition-colors cursor-pointer no-underline"
          >
            Connect
          </a>
        </div>

        <!-- Connected state -->
        <div id="pfConnected" class="flex items-center justify-between p-4 rounded-xl border border-green-200 bg-green-50">
          <div class="flex items-center gap-3">
            <div class="w-9 h-9 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0">
              <svg class="w-4 h-4 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/>
              </svg>
            </div>
            <div>
              <p class="font-semibold text-sm text-green-900">Red Bull Salzburg Fan Portal</p>
              <p class="text-xs text-green-700">Connected</p>
            </div>
          </div>
          <button
            id="pfDisconnectBtn"
            class="text-xs font-medium cursor-pointer px-3 py-1.5 rounded-lg border border-error-border text-danger hover:bg-error-bg transition-colors disabled:opacity-50"
          >
            Disconnect
          </button>
        </div>

      </div>
    </u-signed-in>

    <u-signed-in not>
      <p class="text-text-muted text-sm">Sign in to manage connected services.</p>
    </u-signed-in>
  </>
) : (
  <p class="text-text-muted text-sm">No partner service configured for this demo.</p>
)}

<style>
  #pfConnected { display: none; }
  #pfProvider.pf-connected #pfLocked    { display: none; }
  #pfProvider.pf-connected #pfConnected { display: flex; }
</style>

<script type="module">
  import { getUnidyClient, onAuthChange } from "https://cdn.jsdelivr.net/npm/@unidy.io/sdk@1.9.1/dist/sdk/index.esm.js";

  const provider = document.getElementById("pfProvider");
  const disconnectBtn = document.getElementById("pfDisconnectBtn");
  const PARTNER_CLIENT_ID = provider?.dataset.clientId;

  function showConnected()    { provider?.classList.add("pf-connected"); }
  function showDisconnected() { provider?.classList.remove("pf-connected"); }

  async function getServiceHeaders() {
    const client = getUnidyClient();
    const idToken = await client.oauth.getIdToken();
    return {
      "Authorization": "Token " + client.apiClient.api_key,
      "X-Id-Token": idToken,
    };
  }

  async function checkConnection() {
    if (!PARTNER_CLIENT_ID) return;
    try {
      const client = getUnidyClient();
      const headers = await getServiceHeaders();
      const resp = await fetch(client.apiClient.baseUrl + "/api/sdk/v1/service_connections", { headers });
      if (!resp.ok) { showDisconnected(); return; }
      const services = await resp.json();
      const connected = Array.isArray(services) && services.some((s) => s.id === PARTNER_CLIENT_ID);
      connected ? showConnected() : showDisconnected();
    } catch {
      showDisconnected();
    }
  }

  disconnectBtn?.addEventListener("click", async () => {
    if (!PARTNER_CLIENT_ID) return;
    disconnectBtn.disabled = true;
    try {
      const client = getUnidyClient();
      const headers = await getServiceHeaders();
      const resp = await fetch(
        client.apiClient.baseUrl + "/api/sdk/v1/service_connections/" + encodeURIComponent(PARTNER_CLIENT_ID),
        { method: "DELETE", headers }
      );
      if (!resp.ok) throw new Error("HTTP " + resp.status);
      showDisconnected();
    } catch {
      /* keep connected state on failure */
    } finally {
      disconnectBtn.disabled = false;
    }
  });

  // Poll until u-config is initialized (waitForConfig is not exported by the SDK)
  await new Promise(resolve => {
    const iv = setInterval(() => {
      try { getUnidyClient(); clearInterval(iv); resolve(); } catch {}
    }, 50);
  });
  checkConnection();
  onAuthChange(checkConnection);
</script>
Live demo

Red Bull Salzburg Fan Portal

Get personalized matchday content

Connect

Red Bull Salzburg Fan Portal

Connected

Sign in to manage connected services.