# Alchemist: Embedded Agent — Floating Widget
Add a persistent, floating chat widget that users can toggle on any page.
---
The **floating widget** is a persistent, toggleable chat interface that floats over your application content. It appears as a button in the bottom-right corner (or custom position) and opens a chat panel when clicked.
## When to use the floating widget
Use the floating widget when:
- The agent is **supplementary** to your main application (support, help, search assistant)
- You want a **low-intrusion** interface that users can hide when not needed
- You want the agent available on **every page** with minimal code
- You want **persistent state** across page navigation
For a prominent agent interface that is part of your main navigation, use the [inline panel](/docs/guides/alchemist-embedded-chipp-agent-inline-panel/) instead.
## Basic setup
### 1. Install the feature pack
Follow the [installation guide](/docs/guides/alchemist-embedded-chipp-agent-install/) first. Once installed, your project has a `FloatingWidget` component ready to import.
### 2. Import the component
In your root layout or main app file:
```tsx
import { FloatingWidget } from '@/components/embedded-agent/FloatingWidget';
```
### 3. Render the widget
Place the component once in your root layout (it renders globally and floats over all content):
```tsx
export default function RootLayout({ children }) {
return (
<>
{/* Your app header */}
{children}
>
);
}
```
The widget is now available on every page. Users see a chat button in the bottom-right corner.
## Component props
```typescript
interface FloatingWidgetProps {
// Required
projectId: string; // Your Alchemist project ID
sessionEndpoint: string; // Your backend session endpoint
// Optional: Identity
externalUserId?: string; // Authenticated user ID (email)
metadata?: Record; // Additional user context
// Optional: Appearance
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
buttonLabel?: string; // Label on the toggle button (default: "Chat")
buttonIcon?: React.ReactNode; // Custom icon for the button
buttonColor?: string; // CSS color for button background
theme?: 'light' | 'dark'; // Default: inherits from page
// Optional: Behavior
initiallyOpen?: boolean; // Open by default (default: false)
hideOnMobile?: boolean; // Don't show on mobile devices (default: false)
allowMinimize?: boolean; // Show minimize button (default: true)
persistState?: boolean; // Remember open/closed state (default: true)
// Optional: Callbacks
onOpen?: () => void; // Fired when widget opens
onClose?: () => void; // Fired when widget closes
onError?: (error: EmbedError) => void; // Fired on chat errors
}
```
## Positioning
### Custom position
Place the widget in a different corner:
```tsx
```
### Custom offset
Adjust the distance from the edge via CSS:
```tsx
```
```css
.custom-widget-wrapper :global(.embed-widget-button) {
bottom: 2rem; /* Default: 1rem */
right: 2rem; /* Default: 1rem */
}
```
## Authentication & identity
### Authenticated users
Pass the logged-in user's email:
```tsx
const user = useAuthContext();
```
The user's conversations are retained across sessions and pages.
### Anonymous/visitor mode
For public pages or unauthenticated visitors:
```tsx
```
### Upgrading sessions
When a visitor logs in, upgrade their anonymous session to authenticated:
```tsx
// In your login handler
const newSessionId = await fetch('/api/chat/session/upgrade', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
currentSessionId: anonSessionId,
externalUserId: user.email,
}),
});
```
Their previous conversation is merged into the authenticated session.
## Styling & customization
### Custom button appearance
```tsx
```
### Custom icon
```tsx
import { HelpCircle } from 'lucide-react';
}
/>
```
### Dark mode override
```tsx
```
### Mobile behavior
Hide the widget on mobile devices:
```tsx
```
Or show a different component instead:
```tsx
{isMobile ? (
) : (
)}
```
## State persistence
By default, the widget remembers whether the user opened or closed it (stored in localStorage). On each page load, it restores that state.
### Disable persistence
```tsx
```
### Reset state
Clear the persisted state programmatically:
```tsx
localStorage.removeItem('chipp-embed-widget-state');
```
## Opening/closing programmatically
You can control the widget state via a ref:
```tsx
import { useRef } from 'react';
const widgetRef = useRef(null);
const handleOpenChat = () => {
widgetRef.current?.open();
};
const handleCloseChat = () => {
widgetRef.current?.close();
};
<>
>
```
## Callbacks & events
### Widget lifecycle
```tsx
{
console.log('User opened chat');
analytics.track('chat_opened');
}}
onClose={() => {
console.log('User closed chat');
analytics.track('chat_closed');
}}
/>
```
### Error handling
```tsx
const handleChatError = (error: EmbedError) => {
if (error.code === 'AUTH_FAILED') {
// Prompt to re-authenticate
showModal('Login Required', 'Please sign in to use chat.');
} else {
showToast(`Chat unavailable: ${error.message}`);
}
};
```
## Responsive layout
The widget is responsive by default. On narrow screens:
- The chat panel shrinks to fit the available width
- The button position adjusts to avoid keyboard on mobile (if the user taps the input)
### Disable on specific breakpoints
```tsx
const isMobile = useMediaQuery('(max-width: 768px)');
const isSmallScreen = useMediaQuery('(max-height: 600px)');
return !isMobile && !isSmallScreen ? (
) : null;
```
## Z-index & layering
The widget has a default z-index of `999` (below modals, above most content). Adjust if needed:
```css
:global(.embed-widget-container) {
z-index: 50 !important; /* Higher or lower than default */
}
```
## Conversation history
The widget maintains the current conversation across page navigations. Reloading the page or starting a new session (different `externalUserId`) creates a new conversation.
### Clearing the conversation
Users can clear the conversation by clicking the **Clear** button in the widget header. You can also programmatically clear it:
```tsx
localStorage.removeItem('chipp-embed-widget-messages');
```
## Troubleshooting
### "Widget button not visible"
- Verify the component is rendered at the root level (not conditionally hidden).
- Check z-index: other elements may be covering it. Use browser DevTools to inspect z-index values.
- Ensure CSS is loaded (check for styling overrides).
### "Widget opens but chat doesn't load"
- Verify `projectId` and `sessionEndpoint` are correct.
- Check browser console for network errors (401, 404, 5xx).
- Ensure your backend session endpoint is responding correctly.
### "Session expires quickly or chat resets"
- Verify token TTL is reasonable (default: 8 hours for authenticated, 1 hour for anonymous).
- If using anonymous mode, clearing the browser's cookies will reset the session.
- For authenticated users, check that `externalUserId` is consistent across page loads.
### "Widget not persisting state across page reloads"
- Ensure `persistState={true}` (default).
- Check if browser localStorage is available and not full (quota exceeded).
- Inspect localStorage: `localStorage.getItem('chipp-embed-widget-state')` should return JSON.
## Next steps
- **[Inline panel](/docs/guides/alchemist-embedded-chipp-agent-inline-panel/)** — Embed a full panel as part of your layout
- **[Backend client](/docs/guides/alchemist-embedded-chipp-agent-backend/)** — Integrate agent calls in your server routes