Login Button Widget
The @flowsta/login-button package provides beautiful, pre-styled "Login with Flowsta" buttons for all major frameworks.
Installation
bash
npm install @flowsta/login-button1
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
tsx
import { FlowstaLoginButton } from '@flowsta/login-button/react';
function App() {
return (
<FlowstaLoginButton
clientId="your_client_id"
redirectUri="https://yourapp.com/auth/callback"
scope="profile email"
variant="dark_pill"
onSuccess={(data) => console.log('Code:', data.code)}
onError={(error) => console.error('Error:', error)}
/>
);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Props
| Prop | Type | Required | Description |
|---|---|---|---|
clientId | string | ✅ | Your Flowsta app's client ID |
redirectUri | string | ✅ | Where to redirect after login |
scope | string | ✅ | Space-separated scopes ("profile email") |
variant | ButtonVariant | ❌ | Button style (default: "dark_pill") |
state | string | ❌ | CSRF protection state parameter |
onSuccess | (data) => void | ❌ | Callback on successful authorization |
onError | (error) => void | ❌ | Callback on error |
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) => {
setError(error.message);
};
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`}
scope="profile email"
variant="dark_pill"
onSuccess={handleSuccess}
onError={handleError}
/>
)}
</div>
);
}1
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
51
52
53
54
55
56
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
51
52
53
54
55
56
Vue 3
Basic Usage
vue
<script setup lang="ts">
import { FlowstaLoginButtonVue } from '@flowsta/login-button/vue';
const handleSuccess = (data: { code: string; state: string }) => {
console.log('Authorization code:', data.code);
};
const handleError = (error: Error) => {
console.error('Login error:', error);
};
</script>
<template>
<FlowstaLoginButtonVue
client-id="your_client_id"
redirect-uri="https://yourapp.com/auth/callback"
scope="profile email"
variant="dark_pill"
@success="handleSuccess"
@error="handleError"
/>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Props
| Prop | Type | Required | Description |
|---|---|---|---|
client-id | string | ✅ | Your Flowsta app's client ID |
redirect-uri | string | ✅ | Where to redirect after login |
scope | string | ✅ | Space-separated scopes |
variant | string | ❌ | Button style (default: "dark_pill") |
state | string | ❌ | CSRF protection state |
Events
| Event | Payload | Description |
|---|---|---|
@success | { code: string, state: string } | Emitted on successful auth |
@error | Error | Emitted on error |
Full Example
vue
<script setup lang="ts">
import { ref } from 'vue';
import { FlowstaLoginButtonVue } 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) => {
error.value = err.message;
};
</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>
<FlowstaLoginButtonVue
v-else
client-id="your_client_id"
redirect-uri="https://yourapp.com/auth/callback"
scope="profile email"
variant="dark_pill"
@success="handleSuccess"
@error="handleError"
/>
</div>
</template>1
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
51
52
53
54
55
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
51
52
53
54
55
Qwik
Basic Usage
tsx
import { component$, $ } from '@builder.io/qwik';
import { FlowstaLoginButtonQwik } 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) => {
console.error('Login error:', error);
});
return (
<FlowstaLoginButtonQwik
clientId="your_client_id"
redirectUri="https://yourapp.com/auth/callback"
scope="profile email"
variant="dark_pill"
onSuccess$={handleSuccess}
onError$={handleError}
/>
);
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Props
| Prop | Type | Required | Description |
|---|---|---|---|
clientId | string | ✅ | Your Flowsta app's client ID |
redirectUri | string | ✅ | Where to redirect after login |
scope | string | ✅ | Space-separated scopes |
variant | ButtonVariant | ❌ | Button style (default: "dark_pill") |
state | string | ❌ | CSRF protection state |
onSuccess$ | QRL<(data) => void> | ❌ | Success callback (QRL wrapped) |
onError$ | QRL<(error) => void> | ❌ | Error callback (QRL wrapped) |
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 { FlowstaLoginButtonQwik } 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) => {
error.value = err.message;
});
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>
) : (
<FlowstaLoginButtonQwik
clientId={import.meta.env.VITE_FLOWSTA_CLIENT_ID}
redirectUri={`${window.location.origin}/auth/callback`}
scope="profile email"
variant="dark_pill"
onSuccess$={handleSuccess}
onError$={handleError}
/>
)}
</div>
);
});1
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
51
52
53
54
55
56
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
51
52
53
54
55
56
Vanilla JavaScript
Basic Usage
html
<!DOCTYPE html>
<html>
<head>
<title>Login with Flowsta</title>
</head>
<body>
<div id="login-button"></div>
<script type="module">
import { FlowstaLoginButton } from '@flowsta/login-button/vanilla';
const button = new FlowstaLoginButton({
clientId: 'your_client_id',
redirectUri: 'https://yourapp.com/auth/callback',
scope: 'profile email',
variant: 'dark_pill',
onSuccess: (data) => {
console.log('Authorization code:', data.code);
},
onError: (error) => {
console.error('Login error:', error);
},
});
button.mount('#login-button');
</script>
</body>
</html>1
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
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
Options
| Option | Type | Required | Description |
|---|---|---|---|
clientId | string | ✅ | Your Flowsta app's client ID |
redirectUri | string | ✅ | Where to redirect after login |
scope | string | ✅ | Space-separated scopes |
variant | string | ❌ | Button style (default: "dark_pill") |
state | string | ❌ | CSRF protection state |
onSuccess | function | ❌ | Success callback |
onError | function | ❌ | Error callback |
Methods
javascript
const button = new FlowstaLoginButton(options);
// Mount to DOM element
button.mount('#login-button');
button.mount(document.getElementById('login-button'));
// Unmount (cleanup)
button.unmount();
// Trigger login programmatically
button.login();1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
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 { FlowstaLoginButton } from '@flowsta/login-button/vanilla';
const errorDiv = document.getElementById('error-message');
const button = new FlowstaLoginButton({
clientId: 'your_client_id',
redirectUri: `${window.location.origin}/auth/callback`,
scope: 'profile email',
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.message}`;
errorDiv.style.display = 'block';
},
});
button.mount('#login-button');
</script>
</body>
</html>1
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Advanced Usage
Custom State Parameter
Generate a random state for CSRF protection:
javascript
import { FlowstaLoginButton } from '@flowsta/login-button/react';
function LoginButton() {
const [state] = useState(() =>
Math.random().toString(36).substring(2, 15)
);
// Store state in session/localStorage for verification later
useEffect(() => {
sessionStorage.setItem('oauth_state', state);
}, [state]);
return (
<FlowstaLoginButton
clientId="your_client_id"
redirectUri="https://yourapp.com/auth/callback"
scope="profile email"
state={state}
variant="dark_pill"
onSuccess={(data) => {
// Verify state matches
if (data.state !== sessionStorage.getItem('oauth_state')) {
console.error('State mismatch - possible CSRF attack');
return;
}
// Continue with authentication...
}}
/>
);
}1
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
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
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}
scope="profile email"
/>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Programmatic Login
Trigger login without button click:
javascript
import { FlowstaLoginButton } from '@flowsta/login-button/vanilla';
const button = new FlowstaLoginButton({
clientId: 'your_client_id',
redirectUri: 'https://yourapp.com/auth/callback',
scope: 'profile',
});
// Don't mount, just use the login method
button.login();1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
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);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
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"
scope="profile email"
variant={variant}
onSuccess={handleSuccess}
/>
);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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