Skip to content

Login Button Widget

The @flowsta/login-button package provides beautiful, pre-styled "Sign in with Flowsta" buttons for all major frameworks.

Installation

bash
npm install @flowsta/login-button

Supported Frameworks

FrameworkImport PathStatus
React@flowsta/login-button/react✅ React 16.8+
Vue 3@flowsta/login-button/vue✅ Composition API
Qwik@flowsta/login-button/qwik✅ Resumable
Vanilla JS@flowsta/login-button/vanilla✅ Framework-agnostic

Button Variants

The widget comes with 6 pre-designed variants:

VariantDescriptionBest For
dark-pillDark background, pill shapeLight backgrounds
dark-rectangleDark background, rectangleLight backgrounds, formal UI
light-pillWhite background, pill shapeDark backgrounds
light-rectangleWhite background, rectangleDark backgrounds, formal UI
neutral-pillGray background, pill shapeAny background
neutral-rectangleGray background, rectangleAny background

Visual Preview

Button Variants

React

Basic Usage

tsx
import { FlowstaLoginButton } from '@flowsta/login-button/react';

function App() {
  return (
    <FlowstaLoginButton
      clientId="your_client_id"
      redirectUri="https://yourapp.com/auth/callback"
      scopes={['openid', 'email', 'display_name']}
      variant="dark-pill"
      onSuccess={(data) => console.log('Code:', data.code)}
      onError={(error) => console.error('Error:', error)}
    />
  );
}

Props

PropTypeRequiredDescription
clientIdstringYour Flowsta app's client ID
redirectUristringWhere to redirect after login
scopesFlowstaScope[]Scopes array (default: ['openid', 'email', 'display_name'])
variantButtonVariantButton style (default: 'dark-pill')
onSuccess(data) => voidCallback on successful authorization
onError(error) => voidCallback on error
onClick() => voidCallback when button is clicked (before redirect)
loginUrlstringFlowsta login URL (default: 'https://login.flowsta.com')
classNamestringCustom CSS class
disabledbooleanDisable the button

Full Example

tsx
import { useState } from 'react';
import { FlowstaLoginButton } from '@flowsta/login-button/react';

function LoginPage() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleSuccess = (data: { code: string; state: string }) => {
    setLoading(true);
    
    // Send to your backend
    fetch('/api/auth/callback', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ code: data.code, state: data.state }),
    })
      .then(res => res.json())
      .then(user => {
        // Redirect to dashboard
        window.location.href = '/dashboard';
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  };

  const handleError = (error: { error: string; errorDescription?: string }) => {
    setError(error.errorDescription || error.error);
  };

  return (
    <div className="login-page">
      <h1>Welcome to My App</h1>

      {error && (
        <div className="error-message">
          {error}
        </div>
      )}

      {loading ? (
        <div>Loading...</div>
      ) : (
        <FlowstaLoginButton
          clientId={import.meta.env.VITE_FLOWSTA_CLIENT_ID}
          redirectUri={`${window.location.origin}/auth/callback`}
          scopes={['openid', 'email', 'display_name']}
          variant="dark-pill"
          onSuccess={handleSuccess}
          onError={handleError}
        />
      )}
    </div>
  );
}

Vue 3

Basic Usage

vue
<script setup lang="ts">
import { FlowstaLoginButton } from '@flowsta/login-button/vue';

const handleSuccess = (data: { code: string; state: string }) => {
  console.log('Authorization code:', data.code);
};

const handleError = (error: { error: string; errorDescription?: string }) => {
  console.error('Login error:', error);
};
</script>

<template>
  <FlowstaLoginButton
    client-id="your_client_id"
    redirect-uri="https://yourapp.com/auth/callback"
    :scopes="['openid', 'email', 'display_name']"
    variant="dark-pill"
    @success="handleSuccess"
    @error="handleError"
  />
</template>

Props

PropTypeRequiredDescription
client-idstringYour Flowsta app's client ID
redirect-uristringWhere to redirect after login
:scopesFlowstaScope[]Scopes array (default: ['openid', 'email', 'display_name'])
variantstringButton style (default: 'dark-pill')
login-urlstringFlowsta login URL (default: 'https://login.flowsta.com')
class-namestringCustom CSS class
:disabledbooleanDisable the button

Events

EventPayloadDescription
@success{ code: string, state: string }Emitted on successful auth
@error{ error: string, errorDescription?: string }Emitted on error
@click-Emitted when button is clicked (before redirect)

Full Example

vue
<script setup lang="ts">
import { ref } from 'vue';
import { FlowstaLoginButton } from '@flowsta/login-button/vue';

const loading = ref(false);
const error = ref<string | null>(null);

const handleSuccess = async (data: { code: string; state: string }) => {
  loading.value = true;
  error.value = null;

  try {
    const response = await fetch('/api/auth/callback', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ code: data.code, state: data.state }),
    });

    if (!response.ok) throw new Error('Authentication failed');

    const user = await response.json();
    window.location.href = '/dashboard';
  } catch (err) {
    error.value = err instanceof Error ? err.message : 'Unknown error';
    loading.value = false;
  }
};

const handleError = (err: { error: string; errorDescription?: string }) => {
  error.value = err.errorDescription || err.error;
};
</script>

<template>
  <div class="login-page">
    <h1>Welcome to My App</h1>

    <div v-if="error" class="error-message">
      {{ error }}
    </div>

    <div v-if="loading">
      <p>Logging you in...</p>
    </div>
    <FlowstaLoginButton
      v-else
      client-id="your_client_id"
      redirect-uri="https://yourapp.com/auth/callback"
      :scopes="['openid', 'email', 'display_name']"
      variant="dark-pill"
      @success="handleSuccess"
      @error="handleError"
    />
  </div>
</template>

Qwik

Basic Usage

tsx
import { component$, $ } from '@builder.io/qwik';
import { FlowstaLoginButton } from '@flowsta/login-button/qwik';

export default component$(() => {
  const handleSuccess = $((data: { code: string; state: string }) => {
    console.log('Authorization code:', data.code);
  });

  const handleError = $((error: { error: string; errorDescription?: string }) => {
    console.error('Login error:', error);
  });

  return (
    <FlowstaLoginButton
      clientId="your_client_id"
      redirectUri="https://yourapp.com/auth/callback"
      scopes={['openid', 'email', 'display_name']}
      variant="dark-pill"
      onSuccess$={handleSuccess}
      onError$={handleError}
    />
  );
});

Props

PropTypeRequiredDescription
clientIdstringYour Flowsta app's client ID
redirectUristringWhere to redirect after login
scopesFlowstaScope[]Scopes array (default: ['openid', 'email', 'display_name'])
variantButtonVariantButton style (default: 'dark-pill')
onSuccess$QRL<(data) => void>Success callback (QRL wrapped)
onError$QRL<(error) => void>Error callback (QRL wrapped)
onClick$QRL<() => void>Click callback (QRL wrapped, before redirect)
loginUrlstringFlowsta login URL (default: 'https://login.flowsta.com')
classstringCustom CSS class
disabledbooleanDisable the button

Qwik QRLs

Qwik requires event handlers to be wrapped with $() for lazy loading. Use onSuccess$ and onError$ (with dollar sign).

Full Example

tsx
import { component$, $, useSignal } from '@builder.io/qwik';
import { FlowstaLoginButton } from '@flowsta/login-button/qwik';

export default component$(() => {
  const loading = useSignal(false);
  const error = useSignal<string | null>(null);

  const handleSuccess = $(async (data: { code: string; state: string }) => {
    loading.value = true;
    error.value = null;

    try {
      const response = await fetch('/api/auth/callback', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ code: data.code, state: data.state }),
      });

      if (!response.ok) throw new Error('Authentication failed');

      window.location.href = '/dashboard';
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Unknown error';
      loading.value = false;
    }
  });

  const handleError = $((err: { error: string; errorDescription?: string }) => {
    error.value = err.errorDescription || err.error;
  });

  return (
    <div class="login-page">
      <h1>Welcome to My App</h1>

      {error.value && (
        <div class="error-message">
          {error.value}
        </div>
      )}

      {loading.value ? (
        <div>Logging you in...</div>
      ) : (
        <FlowstaLoginButton
          clientId={import.meta.env.VITE_FLOWSTA_CLIENT_ID}
          redirectUri={`${window.location.origin}/auth/callback`}
          scopes={['openid', 'email', 'display_name']}
          variant="dark-pill"
          onSuccess$={handleSuccess}
          onError$={handleError}
        />
      )}
    </div>
  );
});

Vanilla JavaScript

Basic Usage

html
<!DOCTYPE html>
<html>
<head>
  <title>Sign in with Flowsta</title>
</head>
<body>
  <div id="login-button"></div>

  <script type="module">
    import { initFlowstaLoginButton } from '@flowsta/login-button/vanilla';

    initFlowstaLoginButton('#login-button', {
      clientId: 'your_client_id',
      redirectUri: 'https://yourapp.com/auth/callback',
      scopes: ['openid', 'email', 'display_name'],
      variant: 'dark-pill',

      onSuccess: (data) => {
        console.log('Authorization code:', data.code);
      },

      onError: (error) => {
        console.error('Login error:', error);
      },
    });
  </script>
</body>
</html>

Options

OptionTypeRequiredDescription
clientIdstringYour Flowsta app's client ID
redirectUristringWhere to redirect after login
scopesFlowstaScope[]Scopes array (default: ['openid', 'email', 'display_name'])
variantstringButton style (default: 'dark-pill')
onSuccessfunctionSuccess callback
onErrorfunctionError callback
onClickfunctionClick callback (before redirect)
loginUrlstringFlowsta login URL (default: 'https://login.flowsta.com')
classNamestringCustom CSS class
disabledbooleanDisable the button

Functions

javascript
import { createFlowstaLoginButton, initFlowstaLoginButton } from '@flowsta/login-button/vanilla';

// Create and append to a container (recommended)
initFlowstaLoginButton('#login-button', options);
initFlowstaLoginButton(document.getElementById('login-button'), options);

// Create a button element (manual DOM management)
const button = createFlowstaLoginButton(options);
document.body.appendChild(button);

Full Example

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My App - Login</title>
  <style>
    .login-container {
      max-width: 400px;
      margin: 100px auto;
      padding: 40px;
      text-align: center;
      border: 1px solid #e5e7eb;
      border-radius: 8px;
    }
    .error-message {
      color: #dc2626;
      margin-bottom: 20px;
      padding: 12px;
      background: #fee2e2;
      border-radius: 6px;
    }
  </style>
</head>
<body>
  <div class="login-container">
    <h1>Welcome to My App</h1>
    <div id="error-message" class="error-message" style="display: none;"></div>
    <div id="login-button"></div>
  </div>

  <script type="module">
    import { initFlowstaLoginButton } from '@flowsta/login-button/vanilla';

    const errorDiv = document.getElementById('error-message');

    initFlowstaLoginButton('#login-button', {
      clientId: 'your_client_id',
      redirectUri: `${window.location.origin}/auth/callback`,
      scopes: ['openid', 'email', 'display_name'],
      variant: 'dark-pill',

      onSuccess: async (data) => {
        try {
          const response = await fetch('/api/auth/callback', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ code: data.code, state: data.state }),
          });

          if (!response.ok) throw new Error('Authentication failed');

          window.location.href = '/dashboard';
        } catch (error) {
          errorDiv.textContent = error.message;
          errorDiv.style.display = 'block';
        }
      },
      
      onError: (error) => {
        errorDiv.textContent = `Login failed: ${error.errorDescription || error.error}`;
        errorDiv.style.display = 'block';
      },
    });
  </script>
</body>
</html>

Advanced Usage

PKCE and State (Automatic)

The button widget automatically handles PKCE and CSRF state for you:

  • Generates a random code_verifier and code_challenge per login
  • Generates a random state parameter and stores it in sessionStorage
  • Returns the state in the onSuccess callback so your backend can verify it
javascript
import { FlowstaLoginButton } from '@flowsta/login-button/react';

function LoginButton() {
  return (
    <FlowstaLoginButton
      clientId="your_client_id"
      redirectUri="https://yourapp.com/auth/callback"
      scopes={['openid', 'email', 'display_name']}
      variant="dark-pill"
      onSuccess={(data) => {
        // data.code = authorization code
        // data.state = auto-generated state (verify on your backend)
        console.log('Auth code:', data.code);
      }}
    />
  );
}

Dynamic Redirect URI

Use environment variables for different environments:

javascript
const redirectUri = import.meta.env.DEV
  ? 'http://localhost:3000/auth/callback'
  : 'https://yourapp.com/auth/callback';

<FlowstaLoginButton
  clientId="your_client_id"
  redirectUri={redirectUri}
  scopes={['openid', 'email', 'display_name']}
/>

Programmatic Login

For programmatic login without a button, use the @flowsta/auth SDK instead:

javascript
import { FlowstaAuth } from '@flowsta/auth';

const auth = new FlowstaAuth({
  clientId: 'your_client_id',
  redirectUri: 'https://yourapp.com/auth/callback',
  scopes: ['openid', 'display_name'],
});

// Redirect to Flowsta login
auth.login();

Styling

The button widget uses inline styles and doesn't require external CSS. However, you can wrap it in a container for additional styling:

css
.login-button-container {
  display: flex;
  justify-content: center;
  margin: 40px 0;
}

.login-button-container button {
  cursor: pointer;
  transition: transform 0.2s;
}

.login-button-container button:hover {
  transform: translateY(-2px);
}

TypeScript Support

The package includes full TypeScript definitions:

typescript
import { FlowstaLoginButton, ButtonVariant } from '@flowsta/login-button/react';

interface Props {
  variant?: ButtonVariant; // 'dark-pill' | 'dark-rectangle' | etc.
}

function CustomLogin({ variant = 'dark-pill' }: Props) {
  const handleSuccess = (data: { code: string; state: string }) => {
    // Type-safe data access
  };

  return (
    <FlowstaLoginButton
      clientId="your_client_id"
      redirectUri="https://yourapp.com/auth/callback"
      scopes={['openid', 'email', 'display_name']}
      variant={variant}
      onSuccess={handleSuccess}
    />
  );
}

Browser Support

  • ✅ Chrome/Edge 90+
  • ✅ Firefox 88+
  • ✅ Safari 14+
  • ✅ Opera 76+
  • ❌ IE 11 (not supported)

Bundle Size

PackageMinifiedGzipped
React~8 KB~3 KB
Vue~7 KB~2.5 KB
Qwik~6 KB~2 KB
Vanilla JS~5 KB~2 KB

Next Steps

Need Help?

Documentation licensed under CC BY-SA 4.0.