Frontend SDK: UI Components
While Optare uses OAuth for authentication (meaning users sign in on id.optare.one), you can build custom UI components in your application to initiate the sign-in flow and display user information.
Sign-In Button Component
A simple button that redirects users to Optare for authentication:
import { signIn } from "@optare/optareid-react";
export function SignInButton() {
const handleSignIn = async () => {
await signIn.social({
provider: "optare",
callbackURL: "/dashboard",
});
};
return (
<button
onClick={handleSignIn}
className="px-6 py-3 bg-black text-white rounded-lg hover:bg-gray-800 transition-colors"
>
Sign in with Optare
</button>
);
}Sign-In Page
A full sign-in page with branding:
import { signIn } from "@optare/optareid-react";
import Link from "next/link"; // or @remix-run/react
export default function SignInPage() {
const handleSignIn = async () => {
await signIn.social({
provider: "optare",
callbackURL: "/dashboard",
});
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900">Welcome back</h1>
<p className="mt-2 text-gray-600">
Sign in to your account to continue
</p>
</div>
<div className="bg-white py-8 px-6 shadow rounded-lg">
<button
onClick={handleSignIn}
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-black hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
>
Sign in with Optare
</button>
<p className="mt-6 text-center text-sm text-gray-600">
Don't have an account?{" "}
<Link href="/sign-up" className="font-medium text-black hover:text-gray-800">
Sign up
</Link>
</p>
</div>
<p className="text-center text-xs text-gray-500">
By signing in, you agree to our{" "}
<Link href="/terms" className="underline">Terms of Service</Link>
{" "}and{" "}
<Link href="/privacy" className="underline">Privacy Policy</Link>
</p>
</div>
</div>
);
}User Menu Dropdown
A dropdown menu showing user info with sign-out option:
import { useSession, signOut } from "@optare/optareid-react";
import { useState, useRef, useEffect } from "react";
import { ChevronDown, LogOut, User, Building } from "lucide-react";
export function UserMenu() {
const { data: session } = useSession();
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
if (!session) {
return null;
}
const handleSignOut = async () => {
await signOut({ callbackURL: "/" });
};
return (
<div className="relative" ref={menuRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center space-x-3 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors"
>
<div className="flex items-center space-x-2">
{session.user.image ? (
<img
src={session.user.image}
alt={session.user.name}
className="w-8 h-8 rounded-full"
/>
) : (
<div className="w-8 h-8 rounded-full bg-gray-900 text-white flex items-center justify-center text-sm font-medium">
{session.user.name[0].toUpperCase()}
</div>
)}
<span className="text-sm font-medium text-gray-700">
{session.user.name}
</span>
</div>
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${isOpen ? "rotate-180" : ""}`} />
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
{/* User Info */}
<div className="px-4 py-3 border-b border-gray-100">
<p className="text-sm font-medium text-gray-900">{session.user.name}</p>
<p className="text-xs text-gray-500">{session.user.email}</p>
</div>
{/* Organization Info */}
{session.organization && (
<div className="px-4 py-3 border-b border-gray-100">
<div className="flex items-center space-x-2 text-sm">
<Building className="w-4 h-4 text-gray-400" />
<div>
<p className="font-medium text-gray-900">{session.organization.name}</p>
<p className="text-xs text-gray-500 capitalize">{session.role}</p>
</div>
</div>
</div>
)}
{/* Actions */}
<div className="py-1">
<button
onClick={handleSignOut}
className="flex items-center space-x-2 w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
<LogOut className="w-4 h-4" />
<span>Sign out</span>
</button>
</div>
</div>
)}
</div>
);
}User Profile Card
Display user and organization information in a card:
import { useSession } from "@optare/optareid-react";
import { Mail, Building, Shield } from "lucide-react";
export function UserProfileCard() {
const { data: session } = useSession();
if (!session) {
return null;
}
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center space-x-4 mb-6">
{session.user.image ? (
<img
src={session.user.image}
alt={session.user.name}
className="w-16 h-16 rounded-full"
/>
) : (
<div className="w-16 h-16 rounded-full bg-gray-900 text-white flex items-center justify-center text-2xl font-bold">
{session.user.name[0].toUpperCase()}
</div>
)}
<div>
<h2 className="text-xl font-bold text-gray-900">{session.user.name}</h2>
{session.user.emailVerified && (
<span className="inline-flex items-center px-2 py-1 text-xs font-medium text-green-700 bg-green-100 rounded">
✓ Verified
</span>
)}
</div>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-3 text-gray-600">
<Mail className="w-5 h-5" />
<span>{session.user.email}</span>
</div>
{session.organization && (
<>
<div className="flex items-center space-x-3 text-gray-600">
<Building className="w-5 h-5" />
<span>{session.organization.name}</span>
</div>
{session.role && (
<div className="flex items-center space-x-3 text-gray-600">
<Shield className="w-5 h-5" />
<span className="capitalize">{session.role}</span>
</div>
)}
</>
)}
</div>
</div>
);
}Organization Switcher
Allow users to switch between multiple organizations:
import { useSession } from "@optare/optareid-react";
import { useState } from "react";
import { Check, ChevronDown } from "lucide-react";
interface Organization {
id: string;
name: string;
slug: string;
}
interface OrganizationSwitcherProps {
organizations: Organization[];
onSwitch: (orgId: string) => void;
}
export function OrganizationSwitcher({ organizations, onSwitch }: OrganizationSwitcherProps) {
const { data: session } = useSession();
const [isOpen, setIsOpen] = useState(false);
if (!session?.organization) {
return null;
}
const currentOrg = session.organization;
return (
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-between w-full px-4 py-2 text-sm bg-white border border-gray-300 rounded-lg hover:bg-gray-50"
>
<span className="font-medium">{currentOrg.name}</span>
<ChevronDown className="w-4 h-4 text-gray-500" />
</button>
{isOpen && (
<div className="absolute z-10 w-full mt-2 bg-white border border-gray-200 rounded-lg shadow-lg">
{organizations.map((org) => (
<button
key={org.id}
onClick={() => {
onSwitch(org.id);
setIsOpen(false);
}}
className="flex items-center justify-between w-full px-4 py-2 text-sm hover:bg-gray-50 first:rounded-t-lg last:rounded-b-lg"
>
<span>{org.name}</span>
{org.id === currentOrg.id && (
<Check className="w-4 h-4 text-green-600" />
)}
</button>
))}
</div>
)}
</div>
);
}Protected Content Wrapper
Show content only to specific roles:
import { useSession } from "@optare/optareid-react";
import { ReactNode } from "react";
interface ProtectedContentProps {
children: ReactNode;
allowedRoles?: Array<"owner" | "admin" | "member">;
fallback?: ReactNode;
}
export function ProtectedContent({
children,
allowedRoles = ["owner", "admin", "member"],
fallback = null
}: ProtectedContentProps) {
const { data: session } = useSession();
if (!session) {
return <>{fallback}</>;
}
if (!allowedRoles.includes(session.role || "member")) {
return <>{fallback}</>;
}
return <>{children}</>;
}Usage:
export function SettingsPage() {
return (
<div>
<h1>Settings</h1>
{/* Everyone can see */}
<div>
<h2>Profile Settings</h2>
<ProfileForm />
</div>
{/* Only admins and owners */}
<ProtectedContent allowedRoles={["owner", "admin"]}>
<div>
<h2>Team Settings</h2>
<TeamForm />
</div>
</ProtectedContent>
{/* Only owners */}
<ProtectedContent
allowedRoles={["owner"]}
fallback={<p className="text-gray-500">Only organization owners can access this section.</p>}
>
<div>
<h2>Billing</h2>
<BillingForm />
</div>
</ProtectedContent>
</div>
);
}Loading Skeleton
Show a skeleton while session loads:
export function SessionLoadingSkeleton() {
return (
<div className="animate-pulse">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-gray-300 rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-300 rounded w-3/4"></div>
<div className="h-3 bg-gray-300 rounded w-1/2"></div>
</div>
</div>
</div>
);
}Usage:
import { useSession } from "@optare/optareid-react";
import { SessionLoadingSkeleton } from "~/components/SessionLoadingSkeleton";
export function Header() {
const { data: session, isPending } = useSession();
return (
<header>
{isPending ? (
<SessionLoadingSkeleton />
) : session ? (
<UserMenu />
) : (
<SignInButton />
)}
</header>
);
}Styling Guidelines
All examples use Tailwind CSS for styling. You can customize these components to match your design system:
- Replace Tailwind classes with your own CSS modules or styled-components
- Adjust colors, spacing, and typography to match your brand
- Use your own icon library instead of Lucide React
Best Practices
1. Always Handle Loading States
Show skeletons or loading indicators while session data loads:
const { data: session, isPending } = useSession();
if (isPending) {
return <LoadingSkeleton />;
}2. Provide Fallbacks
For protected content, show helpful messages instead of hiding elements:
<ProtectedContent
allowedRoles={["owner"]}
fallback={<p>Only owners can access this feature.</p>}
>
{/* Protected content */}
</ProtectedContent>3. Use Semantic HTML
Ensure your components are accessible:
<button
onClick={handleSignIn}
aria-label="Sign in with Optare"
>
Sign in
</button>Next Steps
- React Hooks - Learn about useSession and other hooks
- Backend SDK - Protect your API routes
- Organizations - Multi-tenancy concepts