Customization
Configure the ARCY widget's theme, individual elements, content, modes, and usage limits via code.
ARCY lets you tailor the embedded widget: logo and color theme, launcher position, size, and mount point, individual element styling, greeting copy, which modes are available, and per-mode usage limits. To hide the widget entirely on a given page, use the showWidget prop, see Configuration.
Configuring the widget
Pass customization directly as a prop on ARCYProvider. This is useful for applying per-tenant or per-user branding at runtime.
import { ARCYProvider } from "@arcyai/sdk"
import type { ArcyCustomization } from "@arcyai/sdk"
import logo from "./logo.svg"
const customization: ArcyCustomization = {
theme: {
logo,
primaryColor: "#2563eb",
borderRadius: 20,
glassiness: 40,
initialPosition: "bottom-right",
},
elements: {
chatInput: { className: "my-input-style" },
sendButton: { className: "my-button-style" },
},
content: {
assistantName: "Arc",
chatPlaceholder: "Ask Arc anything...",
greetingMessage: "Hi! Ask me anything about this app.",
},
modes: {
enabled: ["chat", "copilot", "autopilot"],
},
limits: {
copilot: { max: 10, windowMs: 86400000 }, // Copilot mode's limit
maxCreditsPerUser: { max: 500, windowMs: 2592000000 }, // 500 credits per 30 days, shared by all three capabilities including Chat
},
}
export default function RootLayout({ children }) {
return (
<ARCYProvider publicKey={(process.env.NEXT_PUBLIC_ARCY_PUBLISHABLE_KEY || process.env.NEXT_PUBLIC_ARCY_TEST_PUBLISHABLE_KEY)!} customization={customization}>
{children}
</ARCYProvider>
)
}Theme
customization.theme (ArcyThemeConfig) controls the widget's visual skin and mount behavior: logo, colors, border, glass effect, font, stacking order, mount point, and launcher position. It skins and positions the existing widget UI; it does not change its structure (use Elements for that).
| Field | Type | Range / format | Default |
|---|---|---|---|
logo | string | { src: string } | https:// URL, or a local bundler-resolved file import (e.g. import logo from "./logo.svg") | ARCY's own brand logo |
logoWidth | number | px | proportional to buttonSize in the launcher; 20 in the chat bar header |
logoHeight | number | px | proportional to buttonSize in the launcher; 20 in the chat bar header |
primaryColor | string | hex, e.g. "#2563eb" | widget's built-in accent color |
highlightColor | string | hex | falls back to primaryColor, then to a static default if neither is set |
borderWidth | number | px, 0–8 | 1 |
borderColor | string | hex | "#3f3f46" |
borderRadius | number | px, 0–32 | 28 |
backgroundColor | string | hex. Composes with glassiness | "#18181b" |
glassiness | number | 0–100. Drives backdrop blur and background alpha | 0 |
textColor | string | hex. Overrides the automatic contrast-derived foreground computed from backgroundColor | auto-computed contrast color |
scrollbarThumbColor | string | hex | "#3f3f46" (matches borderColor's default) |
scrollbarThumbHoverColor | string | hex. Chromium/Safari only (Firefox's scrollbar has no hover state) | slightly lighter than scrollbarThumbColor |
scrollbarTrackColor | string | hex | transparent |
scrollbarWidth | number | px, 2–16 | 4 |
fontFamily | string | any valid CSS font-family value | SDK's built-in font stack |
zIndex | number | stacking order of the launcher and panel | 9994 |
containerRef | React.RefObject<HTMLElement | null> | mounts the widget inside this element instead of fixed to the viewport | viewport-fixed |
draggable | boolean | whether the end user can drag and edge-dock the launcher | true |
initialPosition | "bottom-right" | "bottom-left" | "top-right" | "top-left" | "bottom-center" | "top-center" | "left-center" | "right-center" | screen edge/corner the launcher spawns at on first mount | "bottom-right" |
buttonSize | number | px, 40–120. Diameter of the floating launcher button. The logo inside scales proportionally | 76 |
import logo from "./logo.svg"
const customization: ArcyCustomization = {
theme: {
logo, // or a hosted URL string: "https://example.com/logo.png"
logoWidth: 32,
logoHeight: 32,
primaryColor: "#2563eb",
highlightColor: "#f59e0b",
borderWidth: 1,
borderColor: "#3f3f46",
borderRadius: 20,
backgroundColor: "#18181b",
glassiness: 40,
textColor: "#f0f0f0",
scrollbarThumbColor: "#52525b",
scrollbarThumbHoverColor: "#71717a",
scrollbarTrackColor: "transparent",
scrollbarWidth: 6,
fontFamily: "'Inter', sans-serif",
zIndex: 2147483000,
draggable: false,
initialPosition: "bottom-left",
buttonSize: 56,
},
}Logo sizing
logoWidth and logoHeight apply identically wherever the logo renders: the closed launcher button and the open chat bar header. Either can be set alone. The dimension you leave unset falls back to that render site's own default size, not to the other dimension's value, so setting only logoWidth on a non-square logo will not preserve its aspect ratio automatically. Values are not clamped: a logo larger than the launcher button or the chat bar's fixed logo badge will visually overflow.
How glassiness and backgroundColor compose?
They're not independent. glassiness drives two things at once, both derived from backgroundColor: the background's alpha (opacity) and its backdrop blur. At glassiness: 0 the widget background is fully opaque at your chosen backgroundColor. As glassiness increases toward 100, the background becomes more translucent (down to a minimum alpha, never fully transparent) and picks up backdrop blur, so content behind the widget shows through with a frosted-glass look. Changing backgroundColor alone (at any glassiness) changes the tint of that glass; changing glassiness alone changes how much glass effect is applied to that tint.
Scrollbar
The four scrollbar* fields style the vertical scrollbar in both the chat history and the settings panel's conversation history list, so the whole widget reads as one consistent surface. Leave any field unset to keep the SDK's built-in slim scrollbar (4px, transparent track, a muted thumb that matches the default border color). The styling applies in Chromium and Safari (::-webkit-scrollbar) and in Firefox (scrollbar-width / scrollbar-color); scrollbarThumbHoverColor only has an effect in Chromium/Safari, since Firefox's scrollbar-color has no hover state.
How initialPosition and draggable interact?
initialPosition only sets where the launcher spawns on first mount, and now supports the four corners (bottom-right, bottom-left, top-right, top-left) plus the four edge-center positions (bottom-center, top-center, left-center, right-center). If draggable is unset or true (the default), the end user can still drag it anywhere and it will edge-snap to the nearest corner or side. Set draggable: false to keep the launcher pinned exactly at its initialPosition spot with no drag or snap behavior at all.
Mounting inside your own layout
By default the widget is fixed to the viewport. Pass containerRef (a ref to an element in your own layout) to mount it inline instead — useful for embedding the launcher inside a specific panel or sidebar rather than floating over the whole page. initialPosition still applies, but relative to that container's bounds instead of the viewport's.
The ARCY dashboard's theme editor has no control for highlightColor. Set
it via the customization prop in code, as shown above.
Elements
customization.elements (ArcyElementsConfig) lets you restyle individual internal parts of the widget, not just the overall theme. Each key is a stable public name for one internal DOM node, unaffected by internal SDK refactors, so an override you write today keeps working across SDK upgrades even if ARCY renames its internal CSS classes.
Each entry accepts an ArcyElementOverride:
| Field | Type | Notes |
|---|---|---|
className | string | Appended to the widget's own base classes. Never replaces them, so the element keeps its built-in layout and behavior. |
style | React.CSSProperties | Inline style merged onto the element. |
Supported element keys:
| Key | Targets |
|---|---|
launcherButton | The floating launcher button |
launcherWrapper | The launcher's positioning wrapper |
panelContainer | The main panel container |
panelHeader | The panel's header bar |
panelCloseButton | The panel's close button |
chatInput | The chat message input |
sendButton | The send/submit button |
messageBubbleUser | User message bubbles |
messageBubbleAssistant | Assistant message bubbles |
historyList | The conversation history list/container |
interventionPrompt | The proactive intervention prompt |
coachmark | Copilot mode's coachmark/tooltip |
toast | Status toasts (e.g. autopilot plan progress) |
const customization: ArcyCustomization = {
elements: {
launcherButton: { className: "my-fab-style" },
panelContainer: { style: { fontFamily: "inherit" } },
chatInput: { className: "my-input-style" },
sendButton: { className: "my-button-style" },
messageBubbleUser: { className: "my-user-bubble" },
messageBubbleAssistant: { className: "my-assistant-bubble" },
},
}Content
customization.content (ArcyContentConfig) configures copy shown inside the widget.
| Field | Type | Notes | Default |
|---|---|---|---|
assistantName | string | Display name for the assistant. Used in the panel header, chat/panel aria-labels, the "is thinking" status, close-confirmation copy, and coachmark/tooltip text (e.g. "Let {name} do it"). | "ARCY" |
chatPlaceholder | string | Placeholder text for the chat input. Set this to override the name-derived default entirely (e.g. drop the "anything..." phrasing). | "Ask {assistantName} anything..." |
greetingMessage | string | Shown as the first message whenever the chat is opened with no history. Reappears any time history is empty, not just once per user. | none |
const customization: ArcyCustomization = {
content: {
assistantName: "Arc",
chatPlaceholder: "Ask Arc anything...",
greetingMessage: "Hi! Ask me anything about this app.",
},
}chatPlaceholder only needs to be set if you want placeholder copy that
doesn't follow the "Ask {assistantName} anything..." pattern. Setting
assistantName alone already updates the placeholder, the panel brand, and
every aria-label/coachmark string that mentions the assistant by name.
content is a separate, sibling prop from ARCYProvider's uiLanguage,
not part of it. uiLanguage picks a language for every piece of built-in
widget chrome and for assistantName/chatPlaceholder/greetingMessage's
defaults. content hand-writes your own copy for those same three
fields. An explicit content field always wins over the uiLanguage
default for that field, everything else still follows uiLanguage. See
uiLanguage in Configuration.
Modes
customization.modes (ArcyModesConfig) restricts which of the three AI capabilities (Chat, Copilot, Autopilot) are available to end users. There is no per-query mode picker: on every message, the backend query router infers the right capability from the query text itself, constrained to whatever is listed in enabled.
| Field | Type | Range / format | Default |
|---|---|---|---|
enabled | ArcyCapability[] | at least one entry | all three capabilities enabled |
ArcyCapability is "chat" \| "autopilot" \| "copilot".
const customization: ArcyCustomization = {
modes: {
enabled: ["chat", "copilot", "autopilot"],
},
}Limits
customization.limits (ArcyLimitsConfig) sets per-mode and account-wide usage ceilings. Each limit is a { max, windowMs } pair (ArcyLimitConfig): max requests (or credits) per rolling windowMs milliseconds.
| Field | Type | Notes | Default |
|---|---|---|---|
copilot | { max: number; windowMs: number } | Per-user Copilot mode limit | unlimited |
autopilot | { max: number; windowMs: number } | Per-user Autopilot mode limit | unlimited |
maxCreditsPerUser | { max: number; windowMs: number } | Global per-user credit ceiling across all modes combined | unlimited |
const customization: ArcyCustomization = {
limits: {
copilot: { max: 10, windowMs: 86400000 },
autopilot: { max: 5, windowMs: 86400000 },
maxCreditsPerUser: { max: 500, windowMs: 2592000000 }, // 500 credits per 30 days
},
}ArcyLimitsConfig has no chat field. Chat shares maxCreditsPerUser
(the account-wide credit ceiling across all capabilities) rather than
getting its own per-mode limit like copilot and autopilot do.
windowMs is a duration in milliseconds. Common values:
| Duration | Value |
|---|---|
| 1 hour | 3600000 |
| 24 hours | 86400000 |
| 7 days | 604800000 |
| 30 days | 2592000000 |
ArcyCustomization type reference
interface ArcyThemeConfig {
logo?: string | { src: string }
logoWidth?: number // px, applies at both render sites; unset falls back to that site's default
logoHeight?: number // px, applies at both render sites; unset falls back to that site's default
primaryColor?: string
highlightColor?: string
borderWidth?: number // px, 0-8
borderColor?: string // hex
borderRadius?: number // px, 0-32
backgroundColor?: string // hex, composes with glassiness
glassiness?: number // 0-100
textColor?: string // hex
scrollbarThumbColor?: string // hex
scrollbarThumbHoverColor?: string // hex, Chromium/Safari only
scrollbarTrackColor?: string // hex
scrollbarWidth?: number // px, 2-16
fontFamily?: string
zIndex?: number
containerRef?: React.RefObject<HTMLElement | null>
draggable?: boolean
initialPosition?:
| "bottom-right"
| "bottom-left"
| "top-right"
| "top-left"
| "bottom-center"
| "top-center"
| "left-center"
| "right-center"
buttonSize?: number // px, 40-120, defaults to 76
}
interface ArcyElementOverride {
className?: string
style?: React.CSSProperties
}
type ArcyElementKey =
| "launcherButton"
| "launcherWrapper"
| "panelContainer"
| "panelHeader"
| "panelCloseButton"
| "chatInput"
| "sendButton"
| "messageBubbleUser"
| "messageBubbleAssistant"
| "historyList"
| "interventionPrompt"
| "coachmark"
| "toast"
type ArcyElementsConfig = Partial<Record<ArcyElementKey, ArcyElementOverride>>
interface ArcyContentConfig {
greetingMessage?: string
assistantName?: string // defaults to "ARCY"
chatPlaceholder?: string // defaults to "Ask {assistantName} anything..."
}
type ArcyCapability = "chat" | "autopilot" | "copilot"
interface ArcyModesConfig {
enabled?: ArcyCapability[]
}
interface ArcyLimitConfig {
max: number
windowMs: number // duration in milliseconds
}
interface ArcyLimitsConfig {
autopilot?: ArcyLimitConfig
copilot?: ArcyLimitConfig
maxCreditsPerUser?: ArcyLimitConfig
}
interface ArcyCustomization {
theme?: ArcyThemeConfig
elements?: ArcyElementsConfig
content?: ArcyContentConfig
modes?: ArcyModesConfig
limits?: ArcyLimitsConfig
}