Logo

Customization

Style and customize the appearance and behavior of chat components

LlamaIndex Chat UI is built with customization in mind. You can style components, override default behaviors, and create custom themes to match your application's design system.

Styling System

The library uses Tailwind CSS for styling and provides several customization approaches:

CSS Variables

The library exposes CSS variables for easy theming:

/* Custom theme variables */
:root {
  --chat-primary: #3b82f6;
  --chat-secondary: #6b7280;
  --chat-background: #ffffff;
  --chat-surface: #f9fafb;
  --chat-border: #e5e7eb;
  --chat-text: #1f2937;
  --chat-text-secondary: #6b7280;
  --chat-success: #10b981;
  --chat-warning: #f59e0b;
  --chat-error: #ef4444;
}

/* Dark theme */
[data-theme="dark"] {
  --chat-primary: #60a5fa;
  --chat-secondary: #9ca3af;
  --chat-background: #111827;
  --chat-surface: #1f2937;
  --chat-border: #374151;
  --chat-text: #f9fafb;
  --chat-text-secondary: #d1d5db;
}

Component-Level Styling

Override component styles using className props:

import { ChatSection, ChatMessages, ChatInput } from '@llamaindex/chat-ui'

function CustomStyledChat() {
  return (
    <ChatSection 
      handler={handler}
      className="bg-gradient-to-b from-blue-50 to-white"
    >
      <ChatMessages className="bg-white/80 backdrop-blur rounded-lg shadow-lg">
        <ChatMessages.List className="space-y-6 p-6">
          {/* Custom message rendering */}
        </ChatMessages.List>
      </ChatMessages>
      
      <ChatInput className="bg-white border-2 border-blue-200 rounded-xl p-4">
        <ChatInput.Form className="flex items-end gap-3">
          <ChatInput.Field 
            className="flex-1 border-none bg-gray-50 rounded-lg px-4 py-2"
            placeholder="Ask me anything..."
          />
          <ChatInput.Submit className="bg-blue-600 hover:bg-blue-700 text-white rounded-lg px-4 py-2">
            Send
          </ChatInput.Submit>
        </ChatInput.Form>
      </ChatInput>
    </ChatSection>
  )
}

Message Customization

Custom Message Layout

import { ChatMessage, useChatMessage } from '@llamaindex/chat-ui'

function CustomMessageLayout() {
  const { message, isLast } = useChatMessage()
  
  return (
    <ChatMessage 
      message={message} 
      isLast={isLast}
      className={`
        ${message.role === 'user' ? 'ml-8' : 'mr-8'}
        transition-all duration-200 hover:shadow-md
      `}
    >
      <div className={`
        flex gap-3 p-4 rounded-2xl
        ${message.role === 'user' 
          ? 'bg-blue-600 text-white ml-auto' 
          : 'bg-gray-100 text-gray-900'
        }
      `}>
        <ChatMessage.Avatar>
          <CustomAvatar role={message.role} />
        </ChatMessage.Avatar>
        
        <div className="flex-1">
          <ChatMessage.Content>
            <ChatMessage.Content.Markdown />
            <ChatMessage.Content.Image />
            <ChatMessage.Content.Source />
          </ChatMessage.Content>
          
          <ChatMessage.Actions>
            <CustomMessageActions />
          </ChatMessage.Actions>
        </div>
      </div>
    </ChatMessage>
  )
}

function CustomAvatar({ role }: { role: string }) {
  const avatarClass = role === 'user' 
    ? 'bg-blue-500 text-white' 
    : 'bg-gray-300 text-gray-700'
    
  return (
    <div className={`
      h-10 w-10 rounded-full flex items-center justify-center 
      text-sm font-semibold ${avatarClass}
    `}>
      {role === 'user' ? '👤' : '🤖'}
    </div>
  )
}

Message Role Styling

function RoleBasedMessage() {
  const { message } = useChatMessage()
  
  const roleStyles = {
    user: {
      container: 'justify-end',
      bubble: 'bg-blue-600 text-white rounded-l-2xl rounded-tr-2xl',
      maxWidth: 'max-w-[80%]'
    },
    assistant: {
      container: 'justify-start',
      bubble: 'bg-white border shadow-sm rounded-r-2xl rounded-tl-2xl',
      maxWidth: 'max-w-[85%]'
    },
    system: {
      container: 'justify-center',
      bubble: 'bg-yellow-50 border border-yellow-200 rounded-lg text-yellow-800',
      maxWidth: 'max-w-[70%]'
    }
  }
  
  const styles = roleStyles[message.role] || roleStyles.assistant
  
  return (
    <div className={`flex ${styles.container} mb-4`}>
      <div className={`${styles.bubble} ${styles.maxWidth} p-4`}>
        <ChatMessage.Content>
          <ChatMessage.Content.Markdown />
        </ChatMessage.Content>
      </div>
    </div>
  )
}

Input Customization

Custom Input Design

import { ChatInput, useChatInput } from '@llamaindex/chat-ui'

function CustomChatInput() {
  const { input, setInput, handleSubmit, isLoading } = useChatInput()
  
  return (
    <div className="relative border-t bg-white p-4">
      <div className="mx-auto max-w-4xl">
        <ChatInput>
          <ChatInput.Form className="relative flex items-end gap-3">
            {/* Custom input field with enhanced styling */}
            <div className="relative flex-1">
              <ChatInput.Field
                className="
                  w-full resize-none rounded-2xl border border-gray-300 
                  bg-white px-4 py-3 pr-12 text-sm
                  focus:border-blue-500 focus:outline-none focus:ring-2 
                  focus:ring-blue-500/20 disabled:opacity-50
                  max-h-32 min-h-[44px]
                "
                placeholder="Type your message..."
                disabled={isLoading}
              />
              
              {/* Character counter */}
              <div className="absolute bottom-1 right-12 text-xs text-gray-400">
                {input.length}/2000
              </div>
            </div>
            
            {/* Upload button */}
            <ChatInput.Upload>
              <button
                type="button"
                className="
                  flex h-11 w-11 items-center justify-center rounded-xl
                  border border-gray-300 bg-white hover:bg-gray-50
                  transition-colors disabled:opacity-50
                "
                disabled={isLoading}
              >
                <PaperclipIcon className="h-5 w-5 text-gray-600" />
              </button>
            </ChatInput.Upload>
            
            {/* Submit button */}
            <ChatInput.Submit>
              <button
                type="submit"
                disabled={isLoading || !input.trim()}
                className="
                  flex h-11 w-11 items-center justify-center rounded-xl
                  bg-blue-600 text-white hover:bg-blue-700
                  transition-colors disabled:opacity-50 disabled:cursor-not-allowed
                "
              >
                {isLoading ? (
                  <LoadingSpinner className="h-5 w-5" />
                ) : (
                  <SendIcon className="h-5 w-5" />
                )}
              </button>
            </ChatInput.Submit>
          </ChatInput.Form>
        </ChatInput>
      </div>
    </div>
  )
}

Input with Suggestions

function InputWithSuggestions() {
  const [suggestions, setSuggestions] = useState([])
  const { input, setInput } = useChatInput()
  
  const commonSuggestions = [
    "How can I help you today?",
    "Explain this concept",
    "Write some code for",
    "Summarize this document"
  ]
  
  const filteredSuggestions = commonSuggestions.filter(s =>
    s.toLowerCase().includes(input.toLowerCase()) && input.length > 0
  )
  
  return (
    <div className="relative">
      <ChatInput>
        <ChatInput.Form>
          <ChatInput.Field
            onFocus={() => setSuggestions(filteredSuggestions)}
            onBlur={() => setTimeout(() => setSuggestions([]), 150)}
          />
          <ChatInput.Submit />
        </ChatInput.Form>
      </ChatInput>
      
      {suggestions.length > 0 && (
        <div className="absolute bottom-full left-0 right-0 mb-2">
          <div className="bg-white border rounded-lg shadow-lg max-h-32 overflow-y-auto">
            {suggestions.map((suggestion, index) => (
              <button
                key={index}
                className="w-full px-3 py-2 text-left hover:bg-gray-50 text-sm"
                onClick={() => {
                  setInput(suggestion)
                  setSuggestions([])
                }}
              >
                {suggestion}
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  )
}

Canvas Customization

Custom Canvas Layout

import { ChatCanvas, useChatCanvas } from '@llamaindex/chat-ui'

function CustomCanvas() {
  const { currentArtifact, isVisible, hideCanvas } = useChatCanvas()
  
  if (!isVisible || !currentArtifact) return null
  
  return (
    <div className="w-1/2 bg-gray-50 border-l flex flex-col">
      {/* Custom header */}
      <div className="flex items-center justify-between p-4 border-b bg-white">
        <div>
          <h2 className="font-semibold text-gray-900">
            {currentArtifact.title}
          </h2>
          <p className="text-sm text-gray-500">
            {currentArtifact.type === 'code' ? 'Code Editor' : 'Document'}
          </p>
        </div>
        
        <div className="flex items-center gap-2">
          <CustomCanvasActions />
          <button
            onClick={hideCanvas}
            className="p-1 hover:bg-gray-100 rounded"
          >
            <XIcon className="h-5 w-5" />
          </button>
        </div>
      </div>
      
      {/* Custom content area */}
      <div className="flex-1 overflow-hidden">
        <ChatCanvas className="h-full">
          {/* Canvas content renders here */}
        </ChatCanvas>
      </div>
      
      {/* Custom footer */}
      <div className="border-t bg-white p-3">
        <div className="flex items-center justify-between text-sm text-gray-500">
          <span>
            {currentArtifact.type === 'code' 
              ? `${currentArtifact.language}${currentArtifact.file_name}`
              : 'Markdown Document'
            }
          </span>
          <span>
            Last modified: {new Date().toLocaleTimeString()}
          </span>
        </div>
      </div>
    </div>
  )
}

Widget Styling

Custom Markdown Styling

import { Markdown } from '@llamaindex/chat-ui/widgets'

function CustomMarkdown({ children }: { children: string }) {
  return (
    <div className="prose prose-sm max-w-none">
      <style jsx>{`
        .prose {
          --tw-prose-body: #374151;
          --tw-prose-headings: #111827;
          --tw-prose-links: #3b82f6;
          --tw-prose-bold: #111827;
          --tw-prose-code: #dc2626;
          --tw-prose-pre-bg: #f3f4f6;
          --tw-prose-pre-code: #374151;
        }
        
        .prose code {
          background: #f3f4f6;
          padding: 0.125rem 0.25rem;
          border-radius: 0.25rem;
          font-size: 0.875em;
        }
        
        .prose blockquote {
          border-left: 4px solid #3b82f6;
          background: #eff6ff;
          padding: 1rem;
          margin: 1rem 0;
        }
      `}</style>
      
      <Markdown>{children}</Markdown>
    </div>
  )
}

Custom Code Block Styling

import { CodeBlock } from '@llamaindex/chat-ui/widgets'

function StyledCodeBlock({ code, language, filename }: {
  code: string
  language: string
  filename?: string
}) {
  return (
    <div className="my-4 overflow-hidden rounded-lg border border-gray-200">
      {filename && (
        <div className="flex items-center justify-between bg-gray-50 px-4 py-2">
          <span className="text-sm font-medium text-gray-700">
            {filename}
          </span>
          <span className="text-xs text-gray-500 uppercase">
            {language}
          </span>
        </div>
      )}
      
      <div className="relative">
        <CodeBlock
          code={code}
          language={language}
          className="bg-gray-900 text-gray-100"
          showLineNumbers={true}
        />
        
        {/* Custom copy button */}
        <button
          className="absolute top-2 right-2 p-2 bg-gray-800 hover:bg-gray-700 rounded"
          onClick={() => navigator.clipboard.writeText(code)}
        >
          <CopyIcon className="h-4 w-4 text-gray-300" />
        </button>
      </div>
    </div>
  )
}

Theme System

Theme Provider

import { createContext, useContext, useState } from 'react'

interface Theme {
  mode: 'light' | 'dark'
  primaryColor: string
  borderRadius: 'none' | 'sm' | 'md' | 'lg'
  fontSize: 'sm' | 'base' | 'lg'
}

const ThemeContext = createContext<{
  theme: Theme
  updateTheme: (updates: Partial<Theme>) => void
}>({
  theme: {
    mode: 'light',
    primaryColor: 'blue',
    borderRadius: 'md',
    fontSize: 'base'
  },
  updateTheme: () => {}
})

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>({
    mode: 'light',
    primaryColor: 'blue',
    borderRadius: 'md',
    fontSize: 'base'
  })
  
  const updateTheme = (updates: Partial<Theme>) => {
    setTheme(prev => ({ ...prev, ...updates }))
  }
  
  return (
    <ThemeContext.Provider value={{ theme, updateTheme }}>
      <div 
        className={`theme-${theme.mode} theme-${theme.primaryColor}`}
        data-theme={theme.mode}
        data-radius={theme.borderRadius}
        data-font-size={theme.fontSize}
      >
        {children}
      </div>
    </ThemeContext.Provider>
  )
}

export const useTheme = () => useContext(ThemeContext)

Themed Components

function ThemedChatSection() {
  const { theme } = useTheme()
  const handler = useChat({ api: '/api/chat' })
  
  const themeClasses = {
    light: 'bg-white text-gray-900',
    dark: 'bg-gray-900 text-white'
  }
  
  const radiusClasses = {
    none: 'rounded-none',
    sm: 'rounded-sm',
    md: 'rounded-md',
    lg: 'rounded-lg'
  }
  
  const fontSizeClasses = {
    sm: 'text-sm',
    base: 'text-base',
    lg: 'text-lg'
  }
  
  return (
    <ChatSection
      handler={handler}
      className={`
        ${themeClasses[theme.mode]}
        ${fontSizeClasses[theme.fontSize]}
        transition-all duration-200
      `}
    >
      <ChatMessages 
        className={`
          ${radiusClasses[theme.borderRadius]}
          border border-current/10
        `}
      />
      
      <ChatInput 
        className={`
          ${radiusClasses[theme.borderRadius]}
          border border-current/20
        `}
      />
    </ChatSection>
  )
}

Animation and Transitions

Message Animations

import { motion, AnimatePresence } from 'framer-motion'

function AnimatedMessages() {
  const { messages } = useChatUI()
  
  return (
    <AnimatePresence>
      {messages.map((message, index) => (
        <motion.div
          key={message.id}
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -20 }}
          transition={{ duration: 0.3, delay: index * 0.1 }}
        >
          <ChatMessage message={message}>
            {/* Message content */}
          </ChatMessage>
        </motion.div>
      ))}
    </AnimatePresence>
  )
}

Typing Indicator

function TypingIndicator() {
  const { isLoading } = useChatUI()
  
  if (!isLoading) return null
  
  return (
    <motion.div
      initial={{ opacity: 0, scale: 0.8 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.8 }}
      className="flex items-center gap-2 p-4"
    >
      <div className="flex gap-1">
        {[0, 1, 2].map((i) => (
          <motion.div
            key={i}
            className="h-2 w-2 bg-gray-400 rounded-full"
            animate={{
              scale: [1, 1.2, 1],
              opacity: [0.5, 1, 0.5]
            }}
            transition={{
              duration: 1.5,
              repeat: Infinity,
              delay: i * 0.2
            }}
          />
        ))}
      </div>
      <span className="text-sm text-gray-500">AI is typing...</span>
    </motion.div>
  )
}

Responsive Design

Mobile-First Layout

function ResponsiveChatLayout() {
  const [isMobile, setIsMobile] = useState(false)
  
  useEffect(() => {
    const checkMobile = () => setIsMobile(window.innerWidth < 768)
    checkMobile()
    window.addEventListener('resize', checkMobile)
    return () => window.removeEventListener('resize', checkMobile)
  }, [])
  
  return (
    <ChatSection
      handler={handler}
      className={isMobile ? 'flex-col h-full' : 'flex-row h-full'}
    >
      <div className={isMobile ? 'flex-1' : 'flex-1 flex flex-col'}>
        <ChatMessages className={isMobile ? 'flex-1' : 'flex-1'} />
        <ChatInput className={isMobile ? 'sticky bottom-0' : ''} />
      </div>
      
      {!isMobile && <ChatCanvas className="w-1/2" />}
    </ChatSection>
  )
}

Accessibility Customization

Enhanced Accessibility

function AccessibleChat() {
  const { messages, isLoading } = useChatUI()
  
  return (
    <div
      role="log"
      aria-live="polite"
      aria-label="Chat conversation"
      className="relative"
    >
      <ChatMessages>
        <ChatMessages.List
          role="log"
          aria-busy={isLoading}
        >
          {messages.map((message, index) => (
            <div
              key={message.id}
              role="article"
              aria-label={`Message from ${message.role}`}
              tabIndex={0}
              className="focus:outline-none focus:ring-2 focus:ring-blue-500"
            >
              <ChatMessage message={message} />
            </div>
          ))}
        </ChatMessages.List>
      </ChatMessages>
      
      <ChatInput>
        <ChatInput.Form>
          <ChatInput.Field
            aria-label="Type your message"
            aria-describedby="chat-input-help"
          />
          <div id="chat-input-help" className="sr-only">
            Press Enter to send, Shift+Enter for new line
          </div>
          <ChatInput.Submit aria-label="Send message" />
        </ChatInput.Form>
      </ChatInput>
    </div>
  )
}

Next Steps

  • Examples - See complete customization examples
  • Core Components - Understand component structure for customization
  • Widgets - Customize widget appearance and behavior
  • Hooks - Use hooks for dynamic customization