diff --git a/.changeset/chilled-dots-divide.md b/.changeset/chilled-dots-divide.md
new file mode 100644
index 0000000000..a687358a76
--- /dev/null
+++ b/.changeset/chilled-dots-divide.md
@@ -0,0 +1,5 @@
+---
+"@channel.io/bezier-react": minor
+---
+
+Implement multi theme feature based on data attributes.
diff --git a/packages/bezier-react/src/features/index.ts b/packages/bezier-react/src/features/index.ts
index d63c91c461..1dc94e5a4e 100644
--- a/packages/bezier-react/src/features/index.ts
+++ b/packages/bezier-react/src/features/index.ts
@@ -1,3 +1,4 @@
+export type { Feature } from './Feature'
export { FeatureType } from './Feature'
export {
diff --git a/packages/bezier-react/src/index.ts b/packages/bezier-react/src/index.ts
index f867045692..c4efe51ee1 100644
--- a/packages/bezier-react/src/index.ts
+++ b/packages/bezier-react/src/index.ts
@@ -2,7 +2,19 @@ import '~/src/styles/index.scss'
/* Provider */
export { default as BezierProvider } from '~/src/providers/BezierProvider'
-export { default as WindowProvider, useWindow } from '~/src/providers/WindowProvider'
+export * from '~/src/providers/WindowProvider'
+export * from '~/src/providers/AlphaAppProvider'
+export {
+ useThemeName,
+ useToken,
+ ThemeProvider,
+ LightThemeProvider,
+ DarkThemeProvider,
+ InvertedThemeProvider,
+ type ThemeName,
+ type ThemeProviderProps,
+ type FixedThemeProviderProps,
+} from '~/src/providers/ThemeProvider'
/* Foundation */
export * from '~/src/foundation'
diff --git a/packages/bezier-react/src/providers/AlphaAppProvider.tsx b/packages/bezier-react/src/providers/AlphaAppProvider.tsx
new file mode 100644
index 0000000000..37c687fad5
--- /dev/null
+++ b/packages/bezier-react/src/providers/AlphaAppProvider.tsx
@@ -0,0 +1,85 @@
+import React, { useEffect } from 'react'
+
+import {
+ type Feature,
+ FeatureProvider,
+} from '~/src/features'
+import { window as defaultWindow } from '~/src/utils/dom'
+
+import { TooltipProvider } from '~/src/components/Tooltip'
+
+import {
+ type ThemeName,
+ TokenProvider,
+} from './ThemeProvider'
+import { WindowProvider } from './WindowProvider'
+
+export interface AlphaAppProviderProps {
+ children: React.ReactNode
+ /**
+ * Name of the theme to use for the app.
+ * @default 'light'
+ */
+ themeName?: ThemeName
+ /**
+ * List of features to enable for the app.
+ * @default []
+ */
+ features?: Feature[]
+ /**
+ * Window object to use for the app.
+ * @default window
+ */
+ window?: Window
+}
+
+/**
+ * `AlphaAppProvider` is a required wrapper component that provides context for the app.
+ *
+ * @example
+ *
+ * ```tsx
+ * import React from 'react'
+ * import { createRoot } from 'react-dom/client'
+ * import { AlphaAppProvider } from '@channel.io/bezier-react'
+ *
+ * const container = document.getElementById('root')
+ * const root = createRoot(container)
+ *
+ * root.render(
+ *
+ *
+ * ,
+ * )
+ * ```
+ */
+export function AlphaAppProvider({
+ children,
+ themeName = 'light',
+ features = [],
+ window = defaultWindow,
+}: AlphaAppProviderProps) {
+ useEffect(function updateRootThemeDataAttribute() {
+ const rootElement = window.document.documentElement
+ // TODO: Change data attribute constant to import from bezier-tokens
+ rootElement.setAttribute('data-bezier-theme', themeName)
+ return function cleanup() {
+ rootElement.removeAttribute('data-bezier-theme')
+ }
+ }, [
+ window,
+ themeName,
+ ])
+
+ return (
+
+
+
+
+ { children }
+
+
+
+
+ )
+}
diff --git a/packages/bezier-react/src/providers/BezierProvider.tsx b/packages/bezier-react/src/providers/BezierProvider.tsx
index 88760007f3..d1543bb0b1 100644
--- a/packages/bezier-react/src/providers/BezierProvider.tsx
+++ b/packages/bezier-react/src/providers/BezierProvider.tsx
@@ -9,14 +9,11 @@ import {
type ThemeVarsAdditionalType,
} from '~/src/foundation'
-import {
- document as defaultDocument,
- window as defaultWindow,
-} from '~/src/utils/dom'
+import { window as defaultWindow } from '~/src/utils/dom'
import { TooltipProvider } from '~/src/components/Tooltip'
-import WindowProvider from './WindowProvider'
+import { WindowProvider } from './WindowProvider'
interface BezierProviderProps {
foundation: Foundation & GlobalStyleProp
@@ -29,13 +26,10 @@ function BezierProvider({
foundation,
children,
themeVarsScope,
- externalWindow,
+ externalWindow = defaultWindow,
}: BezierProviderProps) {
return (
-
+
diff --git a/packages/bezier-react/src/providers/ThemeProvider.tsx b/packages/bezier-react/src/providers/ThemeProvider.tsx
new file mode 100644
index 0000000000..5a980b1c4d
--- /dev/null
+++ b/packages/bezier-react/src/providers/ThemeProvider.tsx
@@ -0,0 +1,151 @@
+import React, {
+ forwardRef,
+ useMemo,
+} from 'react'
+
+import { tokens } from '@channel.io/bezier-tokens'
+import { Slot } from '@radix-ui/react-slot'
+
+import { createContext } from '~/src/utils/react'
+
+type Tokens = typeof tokens
+type GlobalTokens = Tokens['global']
+type SemanticTokens = Omit
+
+interface ThemedTokenSet {
+ global: GlobalTokens
+ semantic: SemanticTokens[keyof SemanticTokens]
+}
+
+// TODO: Change theme name constant to import from bezier-tokens
+export type ThemeName = 'light' | 'dark'
+
+interface TokenContextValue {
+ themeName: ThemeName
+ tokens: ThemedTokenSet
+}
+
+const [TokenContextProvider, useTokenContext] = createContext(null, 'TokenProvider')
+
+const tokenSet: Record = Object.freeze({
+ light: {
+ global: tokens.global,
+ semantic: tokens.lightTheme,
+ },
+ dark: {
+ global: tokens.global,
+ semantic: tokens.darkTheme,
+ },
+})
+
+interface TokenProviderProps {
+ themeName: ThemeName
+ children: React.ReactNode
+}
+
+/**
+ * @private For internal use only.
+ */
+export function TokenProvider({
+ themeName,
+ children,
+}: TokenProviderProps) {
+ return (
+ ({
+ themeName,
+ tokens: tokenSet[themeName],
+ }), [themeName])}
+ >
+ { children }
+
+ )
+}
+
+/**
+ * `useThemeName` is a hook that returns the current theme name.
+ */
+export function useThemeName() {
+ return useTokenContext('useThemeName').themeName
+}
+
+/**
+ * `useToken` is a hook that returns the design token for the current theme.
+ */
+export function useToken() {
+ return useTokenContext('useToken').tokens
+}
+
+export interface ThemeProviderProps {
+ themeName: ThemeName
+ children: React.ReactElement
+}
+
+export type FixedThemeProviderProps = Omit
+
+/**
+ * `ThemeProvider` is a wrapper component that provides theme context.
+ */
+export const ThemeProvider = forwardRef(function ThemeProvider({
+ themeName,
+ children,
+}, forwardedRef) {
+ return (
+
+
+ { children }
+
+
+ )
+})
+
+/**
+ * `LightThemeProvider` is a wrapper component that provides light theme context.
+ */
+export const LightThemeProvider = forwardRef(function LightTheme({
+ children,
+}, forwardedRef) {
+ return (
+
+ { children }
+
+ )
+})
+
+/**
+ * `DarkThemeProvider` is a wrapper component that provides dark theme context.
+ */
+export const DarkThemeProvider = forwardRef(function DarkTheme({
+ children,
+}, forwardedRef) {
+ return (
+
+ { children }
+
+ )
+})
+
+/**
+ * `InvertedThemeProvider` is a wrapper component that provides inverted theme context.
+ */
+export const InvertedThemeProvider = forwardRef(function InvertedTheme({
+ children,
+}, forwardedRef) {
+ return (
+
+ { children }
+
+ )
+})
diff --git a/packages/bezier-react/src/providers/WindowProvider.tsx b/packages/bezier-react/src/providers/WindowProvider.tsx
index 74033d08cb..e8e251660d 100644
--- a/packages/bezier-react/src/providers/WindowProvider.tsx
+++ b/packages/bezier-react/src/providers/WindowProvider.tsx
@@ -23,19 +23,14 @@ interface WindowProviderProps extends PropsWithChildren {
* @required
*/
window: Window
-
- /**
- * injected document
- * @required
- */
- document: Document
}
/**
* A Provider that provides window and document object
* you can use this provider to inject an external window
*/
-function WindowProvider({ window, document, children }: WindowProviderProps) {
+export function WindowProvider({ window, children }: WindowProviderProps) {
+ const document = window.document
const rootElement = document.body
const value = useMemo(() => ({
@@ -43,8 +38,8 @@ function WindowProvider({ window, document, children }: WindowProviderProps) {
document,
rootElement,
}), [
- document,
window,
+ document,
rootElement,
])
@@ -52,5 +47,3 @@ function WindowProvider({ window, document, children }: WindowProviderProps) {
{ children }
)
}
-
-export default WindowProvider
diff --git a/packages/bezier-react/src/styles/_base.scss b/packages/bezier-react/src/styles/_base.scss
index 986b49235a..b265edfeef 100644
--- a/packages/bezier-react/src/styles/_base.scss
+++ b/packages/bezier-react/src/styles/_base.scss
@@ -1,6 +1,18 @@
-html {
+:where(:root, :host) {
font-size: 62.5%; // 10/16 = 0.625. Make REM calculations easier.
font-family: var(--font-family-sans-kr);
+ color: var(--txt-black-darkest);
+ color-scheme: light;
+}
+
+// TODO: Change data attribute constant to import from bezier-tokens
+[data-bezier-theme='light'] {
+ color-scheme: light;
+}
+
+// TODO: Change data attribute constant to import from bezier-tokens
+[data-bezier-theme='dark'] {
+ color-scheme: dark;
}
:lang(ja) {