테마 및 스타일 커스터마이징 (Theming)
CSS 변수(--axdg-*)를 오버라이드하여 라이트/다크 모드 및 기업 브랜드 컬러에 맞춘 커스텀 테마를 적용하는 방법을 학습합니다.
import * as React from 'react';
import { AXDataGrid, AXDGColumn } from '@axboot/datagrid';
import { useContainerSize } from '../hooks/useContainerSize';
import DataGridContainer from '../components/DataGridContainer';
interface Props {}
interface IListItem {
id: string;
title: string;
writer: string;
createAt: string;
}
const list = Array.from(Array(5)).map((v, i) => ({
values: {
id: `ID_${i}`,
title: `title_${i}`,
writer: `writer_${i}`,
createAt: `2022-09-08`,
},
}));
function BasicExample(props: Props) {
const [columns, setColumns] = React.useState<AXDGColumn<IListItem>[]>([
{
key: 'id',
label: 'No',
width: 100,
},
{
key: 'title',
label: 'Title',
width: 300,
itemRender: ({ values }) => {
return (
<>
{values.writer} / {values.title}
</>
);
},
},
{
key: 'writer',
label: 'Writer',
width: 100,
itemRender: ({ values: values }) => {
return <>{values.writer} / A</>;
},
},
{
key: 'createAt',
label: 'Date-A',
width: 100,
},
{
key: 'createAt',
label: 'Date-B',
width: 100,
},
{
key: 'createAt',
label: 'Date-C',
width: 100,
},
{
key: 'createAt',
label: 'Date-D',
width: 100,
},
{
key: 'createAt',
label: 'Date-E',
width: 100,
},
]);
const containerRef = React.useRef<HTMLDivElement>(null);
const { width: containerWidth, height: containerHeight } = useContainerSize(containerRef);
return (
<DataGridContainer ref={containerRef}>
<AXDataGrid<IListItem>
width={containerWidth}
height={containerHeight}
data={list}
columns={columns}
onChangeColumns={(columnIndex, { width, columns }) => {
console.log('onChangeColumnWidths', columnIndex, width, columns);
setColumns(columns);
}}
// rowChecked={{
// checkedIndexes: [],
// onChange: (ids, selectedAll) => {
// console.log('onChange rowSelection', ids, selectedAll);
// },
// }}
onClick={item => console.log(item)}
/>
</DataGridContainer>
);
}
export default BasicExample;import * as React from 'react';
import './DataGridContainer.css';
interface DataGridContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
/**
* Keeps a DataGrid in a measured, fixed layout box.
*
* AXDataGrid's rendered root is absolutely positioned within this relative
* container. This makes a ResizeObserver measurement authoritative when a
* surrounding flex or grid layout shrinks as well as when it expands.
*/
const DataGridContainer = React.forwardRef<HTMLDivElement, DataGridContainerProps>(
({ className, ...rest }, ref) => (
<div ref={ref} className={`data-grid-container ${className ?? ''}`.trim()} {...rest} />
),
);
DataGridContainer.displayName = 'DataGridContainer';
export default DataGridContainer;.data-grid-container {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
font-size: 13px;
}
.data-grid-container > .axdg-root {
position: absolute;
inset: 0;
}import * as React from 'react';
export function useContainerSize(ref: React.MutableRefObject<HTMLElement | null>, additionalDeps: unknown[] = []) {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const resizeObserver = React.useRef(
new ResizeObserver(entries => {
if (entries.length !== 1) {
throw new Error('Invalid Container length');
}
const [entry] = entries;
const { width, height } = entry.contentRect;
setWidth(width);
setHeight(height);
}),
);
React.useEffect(() => {
if (!ref.current) return;
const observer = resizeObserver.current;
const element = ref.current;
setWidth(element.clientWidth);
setHeight(element.clientHeight);
observer.observe(element);
return () => {
observer.unobserve(element);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...additionalDeps, ref]);
return {
width,
height,
};
}1. 개요 및 CSS 변수 아키텍처
AXBOOT DataGrid는 현대적인 CSS 커스텀 프로퍼티(CSS Variables) 설계를 따르고 있어, 별도의 복잡한 테마 프로바이더나 무거운 CSS-in-JS 런타임 없이도 순수 CSS 변수 오버라이드만으로 폰트, 배경색, 테두리 색상, 활성 행 하이라이트 색상을 즉시 변경할 수 있습니다.
2. 주요 CSS 변수 계약 명세
/* 데이터그리드 컨테이너 또는 글로벌 CSS에서 오버라이드 가능 */
[role='ax-datagrid'] {
/* 폰트 및 타이포그래피 */
--axdg-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--axdg-font-size: 12px;
/* 테두리 및 구분선 */
--axdg-border-color-base: #cbd5e1;
--axdg-border-color-light: #dbe2ea;
--axdg-border-color-subtle: #eef2f6;
--axdg-header-separator-color: #94a3b8;
/* 헤더 스타일 */
--axdg-header-bg: #f8fafc;
--axdg-header-color: #1e293b;
/* 바디 행 및 셀 */
--axdg-body-bg: #ffffff;
--axdg-body-color: #0f172a;
--axdg-body-odd-bg: #f8fafc;
--axdg-body-hover-bg: #f1f5f9;
--axdg-body-hover-odd-bg: #e9eef5;
--axdg-body-active-bg: #e2e8f0;
/* 선택 범위 안의 포커스 셀 */
--axdg-active-cell-bg: #ffffff;
--axdg-active-cell-ring-color: #2563eb;
--axdg-active-cell-ring-width: 2px;
--axdg-cell-selected-border-width: var(--axdg-active-cell-ring-width);
/* 선택 범위의 컬럼 헤더와 라인넘버 축 */
--axdg-selection-axis-bg: #dbeafe;
--axdg-selection-axis-color: #2563eb;
--axdg-selection-axis-border-color: #2563eb;
/* 값이 변경된 셀 */
--axdg-cell-edited-bg: #fff7ed;
--axdg-cell-edited-color: #c2410c;
--axdg-cell-edited-border-color: #fdba74;
--axdg-cell-value-changed-bg: #fff7ed;
--axdg-cell-value-changed-color: #c2410c;
--axdg-cell-value-changed-border-color: #fdba74;
/* 포인트 컬러 (선택선, 활성 뱃지) */
--axdg-primary-color: #2563eb;
/* 정렬 및 필터 Toolbox */
--axdg-toolbox-bg: #ffffff;
--axdg-toolbox-color: #334155;
--axdg-toolbox-muted-color: #64748b;
--axdg-toolbox-control-bg: #ffffff;
--axdg-toolbox-control-color: #334155;
--axdg-toolbox-control-border-color: #cbd5e1;
--axdg-toolbox-control-placeholder-color: #94a3b8;
--axdg-toolbox-hover-bg: #f1f5f9;
--axdg-toolbox-active-bg: #dbeafe;
--axdg-toolbox-danger-color: #dc2626;
--axdg-toolbox-danger-bg: #fef2f2;
--axdg-toolbox-button-bg: #f8fafc;
--axdg-toolbox-primary-hover-color: #1d4ed8;
--axdg-toolbox-primary-contrast-color: #ffffff;
--axdg-toolbox-notice-bg: #f8fafc;
--axdg-toolbox-scroll-thumb-bg: #b8c2d1;
--axdg-toolbox-scroll-track-bg: #f1f5f9;
--axdg-toolbox-focus-ring-color: #bfdbfe;
}
편집 저장 또는 다중 셀 붙여넣기가 직접 발생한 셀에는 axdg-cell-edited가 적용됩니다. 변경된 데이터 key를 공유하는 모든 셀에는 axdg-cell-value-changed가 적용되므로 동일 key·다른 id 컬럼도 값 변경 상태를 표시합니다. 일반 선택 셀에는 선택 테마가 우선하며, 선택을 이동하면 변경 셀 배경과 inset border가 표시됩니다.
선택 영역 안의 포커스 셀은 선택 배경 대신 --axdg-active-cell-bg를 사용합니다. 단일 셀 선택에서는 --axdg-active-cell-ring-*으로 지정한 inset 링을 표시하지만, 다중 셀 선택에서는 포커스 셀의 개별 링을 제거하고 전체 선택 범위 외곽선만 표시합니다. 외곽선 두께는 --axdg-cell-selected-border-width로 조정하며 기본값은 단일 셀 포커스 링 두께와 같습니다.
활성 셀이나 다중 선택 범위에 포함된 컬럼 헤더에는 axdg-column-axis-active, 라인넘버에는 axdg-row-axis-active가 자동으로 적용됩니다. 배경·글자·강조선 색상은 --axdg-selection-axis-* 변수로 조정할 수 있으며 frozen 컬럼과 일반 컬럼에 동일하게 적용됩니다.
정렬 및 필터 Toolbox는 문서 최상위 포털에 렌더링되지만, 열린 팝오버에는 해당 그리드 인스턴스의 --axdg-toolbox-* 변수가 자동으로 전달됩니다. 따라서 그리드 컨테이너에 지정한 테마가 필터 입력, 옵션, 버튼, 안내 문구와 내부 스크롤바에도 동일하게 적용됩니다.
3. 실무 완성형 예제: 다크 테마(Dark Theme) 적용
import React from 'react';
import { AXDataGrid, type AXDGColumn, type AXDGDataItem } from '@axboot/datagrid';
export default function DarkThemeGrid() {
const columns: AXDGColumn<any>[] = [
{ key: 'id', label: 'ID', width: 70, align: 'center' },
{ key: 'name', label: '서비스명', width: 180 },
{ key: 'status', label: '상태', width: 100, align: 'center' },
];
const data: AXDGDataItem<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 {
--axdg-border-color-base: #334155;
--axdg-border-color-light: #475569;
--axdg-border-color-subtle: #1e293b;
--axdg-header-separator-color: #475569;
--axdg-header-bg: #1e293b;
--axdg-header-color: #f8fafc;
--axdg-body-bg: #0f172a;
--axdg-body-color: #e2e8f0;
--axdg-body-odd-bg: #111c2f;
--axdg-body-hover-bg: #1e293b;
--axdg-body-hover-odd-bg: #263449;
--axdg-body-active-bg: #334155;
--axdg-primary-color: #38bdf8;
}
`}</style>
<AXDataGrid
className="custom-dark-grid"
width={450}
height={180}
columns={columns}
data={data}
rowKey="id"
/>
</div>
);
}