Theming and Style Customization

Override --bgrid-* CSS variables to apply light, dark, and brand-specific themes.

#theming#css-variables#dark-mode#custom-styles#variant
Last reviewed: 2026-08-23
GitHub
import * as React from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';
import { Segmented } from 'antd';
import { Building2, Moon, Sun } from 'lucide-react';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';
import './ThemingExample.css';

type ThemeId = 'default' | 'brand' | 'dark';
type ServiceStatus = '정상' | '점검 예정' | '주의';

interface ServiceMetric {
  id: string;
  service: string;
  team: string;
  status: ServiceStatus;
  availability: number;
  responseTime: number;
  updatedAt: string;
}

interface ThemeOption {
  id: ThemeId;
  label: string;
  description: string;
  icon: React.ReactNode;
  tokens: Array<{ name: string; value: string }>;
}

const themeOptions: ThemeOption[] = [
  {
    id: 'default',
    label: '기본',
    description: '별도 오버라이드가 없는 기본 라이트 테마',
    icon: <Sun size={14} aria-hidden='true' />,
    tokens: [
      { name: '--bgrid-primary-color', value: '#3073f1' },
      { name: '--bgrid-header-bg', value: '#f3f5f8' },
      { name: '--bgrid-body-bg', value: '#ffffff' },
      { name: '--bgrid-body-active-bg', value: '#e1f0ff' },
    ],
  },
  {
    id: 'brand',
    label: '브랜드',
    description: '기업 컬러와 둥근 모서리를 적용한 커스텀 테마',
    icon: <Building2 size={14} aria-hidden='true' />,
    tokens: [
      { name: '--bgrid-primary-color', value: '#0f766e' },
      { name: '--bgrid-header-bg', value: '#0f766e' },
      { name: '--bgrid-body-bg', value: '#f7fffd' },
      { name: '--bgrid-body-active-bg', value: '#ccfbf1' },
    ],
  },
  {
    id: 'dark',
    label: '다크',
    description: '어두운 업무 화면에 맞춘 고대비 다크 테마',
    icon: <Moon size={14} aria-hidden='true' />,
    tokens: [
      { name: '--bgrid-primary-color', value: '#38bdf8' },
      { name: '--bgrid-header-bg', value: '#1e293b' },
      { name: '--bgrid-body-bg', value: '#0f172a' },
      { name: '--bgrid-body-active-bg', value: '#164e63' },
    ],
  },
];

const services: BGridDataItem<ServiceMetric>[] = [
  ['API-GW', 'API Gateway', '플랫폼', '정상', 99.99, 82, '10:42'],
  ['AUTH', '인증 서비스', '플랫폼', '정상', 99.97, 104, '10:41'],
  ['PAY', '결제 처리', '커머스', '주의', 99.82, 286, '10:40'],
  ['ORDER', '주문 관리', '커머스', '정상', 99.95, 127, '10:42'],
  ['SEARCH', '상품 검색', '프로덕트', '점검 예정', 99.91, 168, '10:39'],
  ['NOTIFY', '알림 발송', 'CRM', '정상', 99.98, 91, '10:41'],
  ['REPORT', '리포트 생성', '데이터', '주의', 99.76, 342, '10:38'],
  ['STORAGE', '파일 저장소', '플랫폼', '정상', 99.99, 73, '10:42'],
  ['PROFILE', '회원 프로필', 'CRM', '정상', 99.96, 118, '10:40'],
  ['CATALOG', '상품 카탈로그', '프로덕트', '점검 예정', 99.89, 194, '10:39'],
].map(([id, service, team, status, availability, responseTime, updatedAt]) => ({
  values: {
    id: String(id),
    service: String(service),
    team: String(team),
    status: status as ServiceStatus,
    availability: Number(availability),
    responseTime: Number(responseTime),
    updatedAt: String(updatedAt),
  },
}));

const columns: BGridColumn<ServiceMetric>[] = [
  { key: 'service', label: '서비스', width: 190 },
  { key: 'team', label: '담당 팀', width: 110, align: 'center' },
  {
    key: 'status',
    label: '운영 상태',
    width: 120,
    align: 'center',
    itemRender: ({ value }) => (
      <span className={`theming-example-status theming-example-status--${value === '정상' ? 'healthy' : value === '주의' ? 'warning' : 'scheduled'}`}>
        {String(value)}
      </span>
    ),
  },
  {
    key: 'availability',
    label: '가용성',
    width: 110,
    align: 'right',
    itemRender: ({ value }) => <strong>{Number(value).toFixed(2)}%</strong>,
  },
  {
    key: 'responseTime',
    label: '응답 시간',
    width: 120,
    align: 'right',
    itemRender: ({ value }) => `${Number(value).toLocaleString()}ms`,
  },
  { key: 'updatedAt', label: '최근 확인', width: 100, align: 'center' },
];

export default function ThemingExample() {
  const [theme, setTheme] = React.useState<ThemeId>('default');
  const containerRef = React.useRef<HTMLDivElement>(null);
  const { width, height } = useContainerSize(containerRef);
  const activeTheme = themeOptions.find(option => option.id === theme) ?? themeOptions[0];

  return (
    <div className='theming-example'>
      <div className='theming-example__toolbar'>
        <div className='theming-example__intro'>
          <strong>같은 데이터 · 같은 컬럼 · 다른 CSS 변수</strong>
          <span>{activeTheme.description}</span>
        </div>

        <Segmented
          aria-label='데이터그리드 테마 선택'
          value={theme}
          options={themeOptions.map(option => ({
            value: option.id,
            label: (
              <span className='theming-example__option'>
                {option.icon}
                {option.label}
              </span>
            ),
          }))}
          onChange={value => setTheme(value as ThemeId)}
        />
      </div>

      <div className='theming-example__tokens' aria-live='polite'>
        <div className='theming-example__class-name'>
          <span>적용 클래스</span>
          <code>.bgrid-theme-{theme}</code>
        </div>
        {activeTheme.tokens.map(token => (
          <div className='theming-example__token' key={token.name}>
            <span className='theming-example__swatch' style={{ backgroundColor: token.value }} aria-hidden='true' />
            <span>
              <code>{token.name}</code>
              <small>{token.value}</small>
            </span>
          </div>
        ))}
      </div>

      <DataGridContainer ref={containerRef} className='theming-example__grid-container'>
        <BGrid<ServiceMetric>
          className={`theming-example-grid bgrid-theme-${theme}`}
          width={width}
          height={height}
          headerHeight={38}
          itemHeight={22}
          columns={columns}
          data={services}
          rowKey='id'
          showLineNumber
          frozenColumnIndex={1}
          cellSelectionOptions={{ enabled: true }}
          cellNavigationOptions={{ enabled: true, defaultActiveCell: { rowIndex: 0, columnIndex: 0 } }}
          status={{ content: `${services.length}개 서비스 · ${activeTheme.label} 테마` }}
        />
      </DataGridContainer>

      <p className='theming-example__hint'>행에 마우스를 올리거나 셀을 클릭하면 hover·선택 색상도 테마에 맞게 바뀝니다.</p>
    </div>
  );
}

1. Overview and CSS variable architecture

BeautifulGrid uses CSS custom properties, so you can change fonts, backgrounds, borders, and active-row highlights by overriding CSS variables. No separate theme provider or heavyweight CSS-in-JS runtime is required.

Switch among Default, Brand, and Dark in the live demo above. The data and column configuration stay the same; only the grid root class and its --bgrid-* variables change. The color chips show the active variables and resolved values. Click a cell or hover over a row to compare the selection and hover states in each theme.

A theme changes the visual tokens for the entire grid. Style customization adds business-specific UI, such as a status badge rendered by itemRender. Scope either approach beneath the grid instance’s className so it does not affect other grids.


2. Core CSS variable contract

/* Override these values on a DataGrid container or in global CSS. */
[role='grid'] {
  /* Font and typography */
  --bgrid-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --bgrid-font-size: 13px;

  /* Borders and separators */
  --bgrid-border-color-base: #cbd5e1;
  --bgrid-border-color-light: #dbe2ea;
  --bgrid-border-color-subtle: #eef2f6;
  --bgrid-header-separator-color: #94a3b8;

  /* Header */
  --bgrid-header-bg: #f8fafc;
  --bgrid-header-color: #1e293b;

  /* Body rows and cells */
  --bgrid-body-bg: #ffffff;
  --bgrid-body-color: #0f172a;
  --bgrid-body-odd-bg: #f8fafc;
  --bgrid-body-hover-bg: #f1f5f9;
  --bgrid-body-hover-odd-bg: #e9eef5;
  --bgrid-body-active-bg: #e2e8f0;

  /* Cell-selection range, drawn as a separate overlay */
  --bgrid-cell-selected-bg: var(--bgrid-body-active-bg);
  --bgrid-cell-selected-overlay-opacity: 0.72;
  --bgrid-cell-selected-border-color: rgba(37, 99, 235, 0.78);
  --bgrid-cell-selected-border-width: var(--bgrid-active-cell-ring-width);

  /* Focused cell within the selection range */
  --bgrid-active-cell-bg: #ffffff;
  --bgrid-active-cell-ring-color: #2563eb;
  --bgrid-active-cell-ring-width: 2px;

  /* Column-header and row-number axes for the selection range */
  --bgrid-selection-axis-bg: #dbeafe;
  --bgrid-selection-axis-color: #2563eb;
  --bgrid-selection-axis-border-color: #2563eb;

  /* Cells with changed values */
  --bgrid-cell-edited-bg: #fff7ed;
  --bgrid-cell-edited-color: #c2410c;
  --bgrid-cell-edited-border-color: #fdba74;
  --bgrid-cell-value-changed-bg: #fff7ed;
  --bgrid-cell-value-changed-color: #c2410c;
  --bgrid-cell-value-changed-border-color: #fdba74;

  /* Accent color (selection outline, active badge) */
  --bgrid-primary-color: #2563eb;

  /* Row-reorder motion and preview */
  --bgrid-row-reorder-duration: 150ms;
  --bgrid-row-reorder-easing: cubic-bezier(0.2, 0, 0, 1);
  --bgrid-row-reorder-guide-color: #2563eb;
  --bgrid-row-reorder-preview-bg: #e2e8f0;
  --bgrid-row-reorder-preview-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);

  /* Sorting and filtering toolbox */
  --bgrid-toolbox-bg: #ffffff;
  --bgrid-toolbox-color: #334155;
  --bgrid-toolbox-muted-color: #64748b;
  --bgrid-toolbox-control-bg: #ffffff;
  --bgrid-toolbox-control-color: #334155;
  --bgrid-toolbox-control-border-color: #cbd5e1;
  --bgrid-toolbox-control-placeholder-color: #94a3b8;
  --bgrid-toolbox-hover-bg: #f1f5f9;
  --bgrid-toolbox-active-bg: #dbeafe;
  --bgrid-toolbox-danger-color: #dc2626;
  --bgrid-toolbox-danger-bg: #fef2f2;
  --bgrid-toolbox-button-bg: #f8fafc;
  --bgrid-toolbox-primary-hover-color: #1d4ed8;
  --bgrid-toolbox-primary-contrast-color: #ffffff;
  --bgrid-toolbox-notice-bg: #f8fafc;
  --bgrid-toolbox-scroll-thumb-bg: #b8c2d1;
  --bgrid-toolbox-scroll-track-bg: #f1f5f9;
  --bgrid-toolbox-focus-ring-color: #bfdbfe;

  /* Search highlights and cell context menu */
  --bgrid-search-bg: #ffffff;
  --bgrid-search-color: #334155;
  --bgrid-search-control-bg: #f8fafc;
  --bgrid-search-button-hover-bg: #f1f5f9;
  --bgrid-search-match-bg: rgba(250, 204, 21, 0.28);
  --bgrid-search-match-border-color: #ca8a04;
  --bgrid-search-current-bg: rgba(249, 115, 22, 0.3);
  --bgrid-search-current-border-color: #f97316;
  --bgrid-context-menu-bg: #ffffff;
  --bgrid-context-menu-color: #334155;
  --bgrid-context-menu-border-color: #cbd5e1;
  --bgrid-context-menu-hover-bg: #f1f5f9;
}

Cells changed directly by an edit commit or multi-cell paste receive bgrid-cell-edited. Every cell associated with a changed data key receives bgrid-cell-value-changed, so columns with different IDs but the same key also show the changed-value state. Selection styling takes precedence while a cell is selected; after the selection moves away, the changed-cell background and inset border become visible again.

Cell selection is drawn as a separate rectangular overlay that ignores pointer events, rather than by changing each cell’s border. A range that crosses frozen rows or columns is split into panel-specific segments, but only the outer boundary of the complete selection is shown. The range therefore remains rectangular—even when it includes merged cells—and stays synchronized with scrolling. Control its background with --bgrid-cell-selected-bg, its opacity with --bgrid-cell-selected-overlay-opacity, and its outline with the --bgrid-cell-selected-border-* variables.

The focused cell within a selection uses --bgrid-active-cell-bg instead of the selection background. A single-cell selection displays the inset ring defined by --bgrid-active-cell-ring-*. In a multi-cell selection, the focused cell’s individual ring is removed and only the outline around the complete range is shown. Set that outline thickness with --bgrid-cell-selected-border-width; by default, it matches the single-cell focus-ring width.

Column headers included in the active cell or multi-cell range receive bgrid-column-axis-active, and the corresponding row numbers receive bgrid-row-axis-active. Customize their background, text, and accent colors with the --bgrid-selection-axis-* variables. These styles apply identically to frozen and scrollable columns.

The sorting and filtering toolbox, editor plugins, and cell context menu share a per-grid floating root in a document-level portal. The grid’s --bgrid-* variables are copied to that portal automatically, so popovers and menus inherit the container theme. The search input remains inside the grid and uses --bgrid-search-*; matching cells use --bgrid-search-match-* and --bgrid-search-current-*.


3. Complete example: applying a dark theme

import React from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';

export default function DarkThemeGrid() {
  const columns: BGridColumn<any>[] = [
    { key: 'id', label: 'ID', width: 70, align: 'center' },
    { key: 'name', label: 'Service Name', width: 180 },
    { key: 'status', label: 'Status', width: 100, align: 'center' },
  ];

  const data: BGridDataItem<any>[] = [
    { values: { id: 1, name: 'Auth Gateway', status: 'Healthy' } },
    { values: { id: 2, name: 'Payment Worker', status: 'Healthy' } },
  ];

  return (
    <div style={{ padding: 16, backgroundColor: '#0f172a', borderRadius: 8 }}>
      <style>{`
        .custom-dark-grid {
          --bgrid-border-color-base: #334155;
          --bgrid-border-color-light: #475569;
          --bgrid-border-color-subtle: #1e293b;
          --bgrid-header-separator-color: #475569;
          --bgrid-header-bg: #1e293b;
          --bgrid-header-color: #f8fafc;
          --bgrid-body-bg: #0f172a;
          --bgrid-body-color: #e2e8f0;
          --bgrid-body-odd-bg: #111c2f;
          --bgrid-body-hover-bg: #1e293b;
          --bgrid-body-hover-odd-bg: #263449;
          --bgrid-body-active-bg: #334155;
          --bgrid-primary-color: #38bdf8;
        }
      `}</style>

      <BGrid
        className="custom-dark-grid"
        width={450}
        height={180}
        columns={columns}
        data={data}
        rowKey="id"
      />
    </div>
  );
}