Skip to main content
Sable’s component architecture emphasizes reusability, type safety, and performance through modern React patterns and a custom design system.

Component Organization

Components are organized by domain in src/app/components/:
components/
├── editor/              # Rich text editor (Slate.js)
├── emoji-board/         # Emoji and sticker picker
├── message/             # Message display and interaction
│   ├── attachment/      # File attachments
│   ├── content/         # Message bodies
│   └── layout/          # Layout variants
├── media/               # Audio/video playback
├── image-viewer/        # Image viewer modal
├── pdf-viewer/          # PDF viewer (PDF.js)
├── nav/                 # Navigation components
├── sidebar/             # Sidebar and navigation
└── [other domains]/
Each component typically includes:
ComponentName/
├── ComponentName.tsx       # Main component
├── ComponentName.css.ts    # Styles (Vanilla Extract)
├── types.ts                # TypeScript types (if complex)
├── utils.ts                # Helper functions
└── index.ts                # Public exports

UI Library: Folds

Sable uses Folds (v2.6.1), a custom React component library providing:
  • Buttons, inputs, and form controls
  • Layout primitives (Box, Flex, Grid)
  • Overlays (modals, popovers, tooltips)
  • Navigation components
  • Typography and icons

Importing Folds Components

import {
  Box,
  Text,
  Button,
  Input,
  Dialog,
  Overlay,
} from 'folds'

Folds Configuration

Folds styles are initialized in the app entry point:
// src/index.tsx
import 'folds/dist/style.css'
import { configClass, varsClass } from 'folds'

document.body.classList.add(configClass, varsClass)

Styling with Vanilla Extract

Sable uses Vanilla Extract for type-safe, scoped CSS-in-TypeScript.

Creating Styles

// ComponentName.css.ts
import { style } from '@vanilla-extract/css'
import { recipe } from '@vanilla-extract/recipes'
import { color } from 'folds'

export const container = style({
  padding: '16px',
  borderRadius: '8px',
  backgroundColor: color.Surface.Container,
})

export const button = recipe({
  base: {
    padding: '8px 16px',
    border: 'none',
    cursor: 'pointer',
  },
  variants: {
    variant: {
      primary: {
        backgroundColor: color.Primary.Main,
        color: color.Primary.OnMain,
      },
      secondary: {
        backgroundColor: color.Secondary.Main,
        color: color.Secondary.OnMain,
      },
    },
    size: {
      small: { fontSize: '12px' },
      medium: { fontSize: '14px' },
      large: { fontSize: '16px' },
    },
  },
  defaultVariants: {
    variant: 'primary',
    size: 'medium',
  },
})

Using Styles in Components

// ComponentName.tsx
import * as css from './ComponentName.css'

export function ComponentName({ variant, size }: Props) {
  return (
    <div className={css.container}>
      <button className={css.button({ variant, size })}>
        Click me
      </button>
    </div>
  )
}

Color System

Colors are defined in src/colors.css.ts using Vanilla Extract’s theming:
import { createGlobalTheme } from '@vanilla-extract/css'

export const colors = createGlobalTheme(':root', {
  Primary: {
    Main: '#6366f1',
    OnMain: '#ffffff',
  },
  Surface: {
    Container: '#1e1e1e',
    OnContainer: '#e0e0e0',
  },
  // ...more colors
})
Access colors through Folds:
import { color } from 'folds'

const styles = style({
  color: color.Primary.Main,
  backgroundColor: color.Surface.Container,
})

React Patterns

Component Composition

Sable favors composition over inheritance:
import { Box, Text } from 'folds'

function Card({ title, children }: CardProps) {
  return (
    <Box className={css.card}>
      <Text size="lg" weight="bold">{title}</Text>
      <Box className={css.content}>
        {children}
      </Box>
    </Box>
  )
}

// Usage
<Card title="Profile">
  <UserAvatar />
  <UserInfo />
</Card>

Hooks-Based Logic

Components use hooks for stateful logic (see 100+ hooks in src/app/hooks/):
import { useMatrixClient } from '$hooks/useMatrixClient'
import { useRoom } from '$hooks/useRoom'
import { useRoomEvent } from '$hooks/useRoomEvent'

function RoomName({ roomId }: { roomId: string }) {
  const mx = useMatrixClient()
  const room = useRoom(roomId)
  const nameEvent = useRoomEvent(room, 'm.room.name')
  
  const name = nameEvent?.getContent()?.name || room.name
  
  return <Text>{name}</Text>
}

Render Props

For flexible rendering:
type RenderItemProps<T> = {
  items: T[]
  renderItem: (item: T, index: number) => ReactNode
}

function List<T>({ items, renderItem }: RenderItemProps<T>) {
  return (
    <Box>
      {items.map((item, i) => (
        <Box key={i}>{renderItem(item, i)}</Box>
      ))}
    </Box>
  )
}

// Usage
<List 
  items={messages}
  renderItem={(msg) => <MessageTile message={msg} />}
/>

Virtualization

Large lists use TanStack Virtual:
import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualRoomList({ rooms }: { rooms: Room[] }) {
  const parentRef = useRef<HTMLDivElement>(null)
  
  const virtualizer = useVirtualizer({
    count: rooms.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  })
  
  return (
    <Box ref={parentRef} className={css.scrollContainer}>
      <Box style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map((item) => (
          <Box
            key={item.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              transform: `translateY(${item.start}px)`,
            }}
          >
            <RoomTile room={rooms[item.index]} />
          </Box>
        ))}
      </Box>
    </Box>
  )
}

Key Component Categories

Editor Components

The rich text editor (src/app/components/editor/) is built with Slate.js: Main Files:
  • Editor.tsx - Main editor component
  • Elements.tsx - Custom element renderers (blocks, inlines)
  • Toolbar.tsx - Formatting toolbar
  • input.ts - Input normalization
  • output.ts - Serialize to Matrix events
  • keyboard.ts - Keyboard shortcuts
Autocomplete (autocomplete/):
  • EmoticonAutocomplete.tsx - Emoji suggestions
  • UserMentionAutocomplete.tsx - @mention completion
  • RoomMentionAutocomplete.tsx - #room completion
  • AutocompleteMenu.tsx - Menu UI
Usage:
import { Editor } from '$components/editor'

function MessageComposer() {
  const [value, setValue] = useState<Descendant[]>(initialValue)
  
  return (
    <Editor
      value={value}
      onChange={setValue}
      placeholder="Send a message..."
      onSubmit={handleSend}
    />
  )
}

Emoji Board

The emoji picker (src/app/components/emoji-board/): Structure:
  • EmojiBoard.tsx - Main component
  • components/ - Subcomponents
    • Layout.tsx - Grid layout
    • Group.tsx - Emoji groups
    • Item.tsx - Individual emoji
    • Sidebar.tsx - Category navigation
    • SearchInput.tsx - Search bar
    • Tabs.tsx - Emoji/Sticker tabs
Features:
  • Virtualized grid for performance
  • Search with emoji shortcodes
  • Recent emoji tracking
  • Custom emoji and sticker packs
  • Skin tone selection

Message Components

Message rendering (src/app/components/message/): Attachment Types (attachment/):
  • Image, video, audio attachments
  • File downloads
  • Blurhash placeholders
  • Encrypted media decryption
Content Rendering (content/):
  • HTML message bodies
  • Markdown rendering
  • Code blocks with syntax highlighting
  • Link previews
  • Reply threads
Layouts (layout/):
  • Modern layout (default)
  • Compact layout (IRC-style)
  • Bubble layout (chat bubbles)
Example:
import { Message } from '$components/message'
import { MessageLayout } from '$state/settings'

function MessageTile({ event, layout }: MessageTileProps) {
  return (
    <Message
      event={event}
      layout={layout}
      onReply={handleReply}
      onReact={handleReact}
    />
  )
}

Media Components

Audio/video playback (src/app/components/media/):
  • Progressive loading
  • Custom controls
  • Authenticated requests (via service worker)
  • Thumbnail generation
Viewers:
  • image-viewer/ - Lightbox with zoom and pan gestures
  • pdf-viewer/ - PDF.js integration with pagination
  • image-editor/ - Basic image cropping/editing
Navigation UI (src/app/components/nav/, sidebar/):
  • Room/space hierarchy
  • Unread indicators
  • Drag-and-drop reordering (Pragmatic Drag & Drop)
  • Collapsible categories
  • Search and filters

Animation

Sable uses Framer Motion for animations:
import { motion, AnimatePresence } from 'framer-motion'

function FadeIn({ children }: { children: ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      transition={{ duration: 0.2 }}
    >
      {children}
    </motion.div>
  )
}

// With AnimatePresence for mounting/unmounting
<AnimatePresence>
  {isOpen && (
    <FadeIn>
      <Dialog>Content</Dialog>
    </FadeIn>
  )}
</AnimatePresence>

Gesture Handling

Mobile gestures use @use-gesture/react:
import { useSwipeable } from '@use-gesture/react'

function SwipeableMessage({ onReply }: SwipeableMessageProps) {
  const bind = useSwipeable({
    onSwipeRight: () => onReply(),
    threshold: 50,
  })
  
  return <div {...bind()}>Message content</div>
}

Accessibility

Components use react-aria for accessibility:
import { useButton } from 'react-aria'

function AccessibleButton(props: ButtonProps) {
  const ref = useRef<HTMLButtonElement>(null)
  const { buttonProps } = useButton(props, ref)
  
  return (
    <button {...buttonProps} ref={ref}>
      {props.children}
    </button>
  )
}

Testing Patterns

While test files are not included in the source, components are designed for testability:
  • Pure functional components
  • Logic extracted to hooks
  • Dependency injection via props
  • Controlled vs. uncontrolled variants

Best Practices

  1. Use Folds components before building custom UI
  2. Colocate styles with components using Vanilla Extract
  3. Extract complex logic to custom hooks
  4. Leverage TypeScript for prop validation
  5. Memoize expensive computations with useMemo
  6. Virtualize long lists with TanStack Virtual
  7. Handle loading states explicitly
  8. Provide accessibility attributes (ARIA labels, roles)
  9. Test in mobile viewport - Sable is mobile-first
  10. Follow the existing patterns - consistency matters

Build docs developers (and LLMs) love