Login Button Widget
The @flowsta/login-button package provides beautiful, pre-styled "Sign in with Flowsta" buttons for all major frameworks.
Installation
npm install @flowsta/login-buttonHow the Button Works
Clicking the button performs a full-page redirect to https://login.flowsta.com/login with PKCE and state parameters. The user approves the sign-in there with their Flowsta Vault, and Flowsta redirects the browser back to your redirectUri with ?code=...&state=... - a fresh page load.
That means:
- There is no success callback. The page that rendered the button is gone by the time authorization completes. Handle the authorization code on a callback page at your
redirectUri(see Handling the Callback). onClickfires just before the redirect (useful for analytics or a loading state).onErrorfires only if the authorization URL could not be built - not for authorization failures, which arrive aserrorparameters on your callback page.- The button stores the PKCE
code_verifierandstateinsessionStorageautomatically; the package exports helpers to retrieve them on your callback page.
Email Scope Caveat
email is not in the default scopes, and requesting it won't return one - Flowsta's server stores only a hash. If your app needs an email address, ask the user directly after sign-in. (New apps on dev.flowsta.com don't have the email scope enabled by default; enable it there before requesting it, or authorize calls fail with invalid_scope.)
Supported Frameworks
| Framework | Import Path | Status |
|---|---|---|
| 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:
| Variant | Description | Best For |
|---|---|---|
dark-pill | Dark background, pill shape | Light backgrounds |
dark-rectangle | Dark background, rectangle | Light backgrounds, formal UI |
light-pill | White background, pill shape | Dark backgrounds |
light-rectangle | White background, rectangle | Dark backgrounds, formal UI |
neutral-pill | Gray background, pill shape | Any background |
neutral-rectangle | Gray background, rectangle | Any background |
Visual Preview

React
Basic Usage
import { FlowstaLoginButton } from '@flowsta/login-button/react';
function App() {
return (
<FlowstaLoginButton
clientId="your_client_id"
redirectUri="https://yourapp.com/auth/callback"
scopes={['openid', 'display_name']}
variant="dark-pill"
onError={(error) => console.error('Could not start login:', error)}
/>
);
}2
3
4
5
6
7
8
9
10
11
12
13
Props
| Prop | Type | Required | Description |
|---|---|---|---|
clientId | string | ✅ | Your Flowsta app's client ID |
redirectUri | string | ✅ | Where the authorization code is delivered after login |
scopes | FlowstaScope[] | ❌ | Scopes array (default: ['openid', 'display_name']) |
variant | ButtonVariant | ❌ | Button style (default: 'dark-pill') |
onClick | () => void | ❌ | Called when the button is clicked, just before the redirect |
onError | (error) => void | ❌ | Called if the authorization URL could not be built |
loginUrl | string | ❌ | Flowsta login URL (default: 'https://login.flowsta.com') |
className | string | ❌ | Custom CSS class |
disabled | boolean | ❌ | Disable the button |
Full Example
// Login page - renders the button. The result arrives at /auth/callback.
import { useState } from 'react';
import { FlowstaLoginButton } from '@flowsta/login-button/react';
function LoginPage() {
const [error, setError] = useState<string | null>(null);
return (
<div className="login-page">
<h1>Welcome to My App</h1>
{error && (
<div className="error-message">
{error}
</div>
)}
<FlowstaLoginButton
clientId={import.meta.env.VITE_FLOWSTA_CLIENT_ID}
redirectUri={`${window.location.origin}/auth/callback`}
scopes={['openid', 'display_name']}
variant="dark-pill"
onClick={() => setError(null)}
onError={(err) => setError(err.errorDescription || err.error)}
/>
</div>
);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Callback page - route this at /auth/callback
import { useEffect, useState } from 'react';
import {
handleCallback,
validateState,
retrieveCodeVerifier,
} from '@flowsta/login-button';
function AuthCallbackPage() {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function completeSignIn() {
const { code, state, error, errorDescription } = handleCallback();
if (error) throw new Error(errorDescription || error);
if (!code) throw new Error('No authorization code received');
if (!state || !validateState(state)) throw new Error('State mismatch');
const codeVerifier = retrieveCodeVerifier(state);
if (!codeVerifier) throw new Error('Missing PKCE code verifier');
// Exchange on your backend (recommended) or directly via /oauth/token
const response = await fetch('/api/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, code_verifier: codeVerifier }),
});
if (!response.ok) throw new Error('Authentication failed');
window.location.href = '/dashboard';
}
completeSignIn().catch((err) => setError(err.message));
}, []);
if (error) return <div className="error-message">{error}</div>;
return <div>Completing sign-in...</div>;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Vue 3
Basic Usage
<script setup lang="ts">
import { FlowstaLoginButton } from '@flowsta/login-button/vue';
const handleError = (error: { error: string; errorDescription?: string }) => {
console.error('Could not start login:', error);
};
</script>
<template>
<FlowstaLoginButton
client-id="your_client_id"
redirect-uri="https://yourapp.com/auth/callback"
:scopes="['openid', 'display_name']"
variant="dark-pill"
@error="handleError"
/>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Props
| Prop | Type | Required | Description |
|---|---|---|---|
client-id | string | ✅ | Your Flowsta app's client ID |
redirect-uri | string | ✅ | Where the authorization code is delivered after login |
:scopes | FlowstaScope[] | ❌ | Scopes array (default: ['openid', 'display_name']) |
variant | string | ❌ | Button style (default: 'dark-pill') |
login-url | string | ❌ | Flowsta login URL (default: 'https://login.flowsta.com') |
class-name | string | ❌ | Custom CSS class |
:disabled | boolean | ❌ | Disable the button |
Events
| Event | Payload | Description |
|---|---|---|
@click | - | Emitted when the button is clicked, just before the redirect |
@error | { error: string, errorDescription?: string } | Emitted if the authorization URL could not be built |
Full Example
<!-- Login page - renders the button. The result arrives at /auth/callback. -->
<script setup lang="ts">
import { ref } from 'vue';
import { FlowstaLoginButton } from '@flowsta/login-button/vue';
const error = ref<string | null>(null);
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>
<FlowstaLoginButton
client-id="your_client_id"
redirect-uri="https://yourapp.com/auth/callback"
:scopes="['openid', 'display_name']"
variant="dark-pill"
@click="error = null"
@error="handleError"
/>
</div>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- Callback page - route this at /auth/callback -->
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import {
handleCallback,
validateState,
retrieveCodeVerifier,
} from '@flowsta/login-button';
const error = ref<string | null>(null);
onMounted(async () => {
try {
const { code, state, error: authError, errorDescription } = handleCallback();
if (authError) throw new Error(errorDescription || authError);
if (!code) throw new Error('No authorization code received');
if (!state || !validateState(state)) throw new Error('State mismatch');
const codeVerifier = retrieveCodeVerifier(state);
if (!codeVerifier) throw new Error('Missing PKCE code verifier');
const response = await fetch('/api/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, code_verifier: codeVerifier }),
});
if (!response.ok) throw new Error('Authentication failed');
window.location.href = '/dashboard';
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error';
}
});
</script>
<template>
<div v-if="error" class="error-message">{{ error }}</div>
<div v-else>Completing sign-in...</div>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Qwik
Basic Usage
import { component$, $ } from '@builder.io/qwik';
import { FlowstaLoginButton } from '@flowsta/login-button/qwik';
export default component$(() => {
const handleError = $((error: { error: string; errorDescription?: string }) => {
console.error('Could not start login:', error);
});
return (
<FlowstaLoginButton
clientId="your_client_id"
redirectUri="https://yourapp.com/auth/callback"
scopes={['openid', 'display_name']}
variant="dark-pill"
onError$={handleError}
/>
);
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Props
| Prop | Type | Required | Description |
|---|---|---|---|
clientId | string | ✅ | Your Flowsta app's client ID |
redirectUri | string | ✅ | Where the authorization code is delivered after login |
scopes | FlowstaScope[] | ❌ | Scopes array (default: ['openid', 'display_name']) |
variant | ButtonVariant | ❌ | Button style (default: 'dark-pill') |
onClick$ | QRL<() => void> | ❌ | Click callback (QRL wrapped, fires just before the redirect) |
onError$ | QRL<(error) => void> | ❌ | Called if the authorization URL could not be built (QRL wrapped) |
loginUrl | string | ❌ | Flowsta login URL (default: 'https://login.flowsta.com') |
class | string | ❌ | Custom CSS class |
disabled | boolean | ❌ | Disable the button |
Qwik QRLs
Qwik requires event handlers to be wrapped with $() for lazy loading. Use onClick$ and onError$ (with dollar sign).
Full Example
// Callback route - src/routes/auth/callback/index.tsx
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
import {
handleCallback,
validateState,
retrieveCodeVerifier,
} from '@flowsta/login-button';
export default component$(() => {
const error = useSignal<string | null>(null);
// eslint-disable-next-line qwik/no-use-visible-task
useVisibleTask$(async () => {
try {
const { code, state, error: authError, errorDescription } = handleCallback();
if (authError) throw new Error(errorDescription || authError);
if (!code) throw new Error('No authorization code received');
if (!state || !validateState(state)) throw new Error('State mismatch');
const codeVerifier = retrieveCodeVerifier(state);
if (!codeVerifier) throw new Error('Missing PKCE code verifier');
const response = await fetch('/api/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, code_verifier: codeVerifier }),
});
if (!response.ok) throw new Error('Authentication failed');
window.location.href = '/dashboard';
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error';
}
});
return (
<div class="login-page">
{error.value ? (
<div class="error-message">{error.value}</div>
) : (
<div>Completing sign-in...</div>
)}
</div>
);
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Vanilla JavaScript
Basic Usage
<!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.html',
scopes: ['openid', 'display_name'],
variant: 'dark-pill',
onError: (error) => {
console.error('Could not start login:', error);
},
});
</script>
</body>
</html>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Options
| Option | Type | Required | Description |
|---|---|---|---|
clientId | string | ✅ | Your Flowsta app's client ID |
redirectUri | string | ✅ | Where the authorization code is delivered after login |
scopes | FlowstaScope[] | ❌ | Scopes array (default: ['openid', 'display_name']) |
variant | string | ❌ | Button style (default: 'dark-pill') |
onClick | function | ❌ | Click callback (fires just before the redirect) |
onError | function | ❌ | Called if the authorization URL could not be built |
loginUrl | string | ❌ | Flowsta login URL (default: 'https://login.flowsta.com') |
className | string | ❌ | Custom CSS class |
disabled | boolean | ❌ | Disable the button |
Functions
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);2
3
4
5
6
7
8
9
Full Example (Callback Page)
<!-- callback.html - served at your redirectUri -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Completing sign-in...</title>
</head>
<body>
<div id="status">Completing sign-in...</div>
<div id="error-message" style="display: none; color: #dc2626;"></div>
<script type="module">
import {
handleCallback,
validateState,
retrieveCodeVerifier,
} from '@flowsta/login-button';
const errorDiv = document.getElementById('error-message');
async function completeSignIn() {
const { code, state, error, errorDescription } = handleCallback();
if (error) throw new Error(errorDescription || error);
if (!code) throw new Error('No authorization code received');
if (!state || !validateState(state)) throw new Error('State mismatch');
const codeVerifier = retrieveCodeVerifier(state);
if (!codeVerifier) throw new Error('Missing PKCE code verifier');
const response = await fetch('/api/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, code_verifier: codeVerifier }),
});
if (!response.ok) throw new Error('Authentication failed');
window.location.href = '/dashboard.html';
}
completeSignIn().catch((err) => {
document.getElementById('status').style.display = 'none';
errorDiv.textContent = `Sign-in failed: ${err.message}`;
errorDiv.style.display = 'block';
});
</script>
</body>
</html>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Handling the Callback
Whatever framework you use, the pattern is the same:
- Create a page at your
redirectUri. Flowsta delivers?code=...&state=...there on a fresh page load. - Parse and validate.
handleCallback()reads the URL parameters;validateState(state)checks the CSRF state the button stored;retrieveCodeVerifier(state)returns the PKCE verifier (and clears it fromsessionStorage). - Exchange the code. POST
code+code_verifiertohttps://auth-api.flowsta.com/oauth/token- from your backend (recommended) or directly from the browser. See the API Reference.
All three helpers are exported from the package root:
import {
handleCallback, // parse code/state/error from the callback URL
validateState, // verify the state matches what the button stored
retrieveCodeVerifier // fetch (and clear) the stored PKCE verifier
} from '@flowsta/login-button';2
3
4
5
Prefer a Full SDK?
The @flowsta/auth package wraps the whole flow - auth.login() to start (or this button widget), and auth.handleCallback() on the callback page to exchange the code and fetch the user in one call. From v2.3.1, handleCallback() completes button-initiated logins too.
Advanced Usage
PKCE and State (Automatic)
The button widget automatically handles PKCE and CSRF state for you:
- Generates a random
code_verifierandcode_challengeper login - Generates a random
stateparameter - Stores both in
sessionStorage(the verifier is keyed by state) so your callback page can retrieve them after the redirect
Dynamic Redirect URI
Use environment variables for different environments:
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', 'display_name']}
/>2
3
4
5
6
7
8
9
Programmatic Login
For programmatic login without a button, use the @flowsta/auth SDK instead:
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();
// ...and on your callback page:
const user = await auth.handleCallback();2
3
4
5
6
7
8
9
10
11
12
13
Styling
The button widget uses inline styles and doesn't require external CSS. However, you can wrap it in a container for additional styling:
.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);
}2
3
4
5
6
7
8
9
10
11
12
13
14
TypeScript Support
The package includes full TypeScript definitions:
import { FlowstaLoginButton, ButtonVariant } from '@flowsta/login-button/react';
interface Props {
variant?: ButtonVariant; // 'dark-pill' | 'dark-rectangle' | etc.
}
function CustomLogin({ variant = 'dark-pill' }: Props) {
return (
<FlowstaLoginButton
clientId="your_client_id"
redirectUri="https://yourapp.com/auth/callback"
scopes={['openid', 'display_name']}
variant={variant}
/>
);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Browser Support
- ✅ Chrome/Edge 90+
- ✅ Firefox 88+
- ✅ Safari 14+
- ✅ Opera 76+
- ❌ IE 11 (not supported)
Bundle Size
| Package | Minified | Gzipped |
|---|---|---|
| React | ~8 KB | ~3 KB |
| Vue | ~7 KB | ~2.5 KB |
| Qwik | ~6 KB | ~2 KB |
| Vanilla JS | ~5 KB | ~2 KB |
Next Steps
- API Reference - OAuth endpoint documentation
- OAuth Overview - Complete OAuth integration guide
- Security - Best practices and security guide
Need Help?
- 💬 Discord: Join our community
- 🆘 Support: Find out about Flowsta support options
- 🐙 GitHub: github.com/WeAreFlowsta