arkynChangelogGuides
docs / guides / how-to-configure-components-theming

Configure the theme

Arkyn styles are shipped inside a CSS layer. This means the library can define its own style priority without depending only on selector specificity or the physical order of declarations.
In practice, CSS layers let you group styles and control which group wins when two rules target the same element. A rule declared in a later layer has higher priority than a rule declared in an earlier layer, even if both selectors have the same specificity. That is why Arkyn's stylesheet should stay in its own layer, while your global styles should live in an earlier layer.

Global Styles Must Be Wrapped in a Layer

Any raw global CSS that is not inside a layer can override Arkyn styles in unexpected ways. To avoid that, place your reset and base styles inside a dedicated layer, and keep Arkyn in a later layer.

css

@layer reset, arkyn;
@layer reset {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
*,
input,
textarea,
button {
font-family: "Open Sans", sans-serif;
font-optical-sizing: auto;
}
}
Avoid global line-height overrides
Do not place line-height: 130% or any other line-height value in unlayered global CSS. Keep this rule inside the reset layer so it does not interfere with Arkyn component styles. In some cases, developers may think text is not vertically centered, when the real cause is the global line-height affecting the component's internal layout.

Arkyn CSS Variables

Every CSS variable used by Arkyn already ships with a default fallback value, so you don't need to define a variables file just to get the library working. These variables act as the design tokens used by the components, overlays, feedback states, and text hierarchy, and you only need to declare them yourself if you want to override the defaults to match your application's design system.

css

:root {
--white: #ffffff;
/* background */
--background: #f4f4f5;
--background-underground: #fafafa;
--background-foreground: #ffffff;
/* text */
--text-heading: #09090b;
--text-body: #52525b;
--text-muted: #a1a1aa;
/* border */
--border: #e2e8f0;
/* inputs, textarea, select, checkbox, etc. */
--input-background: #fafafa;
/* tooltip */
--tooltip-text: #fafafa;
--tooltip-background: #27272a;
/* spotlight */
--spotlight-primary: 17, 109, 220; /* #116ddc */
--spotlight-secondary: 100, 116, 139; /* #64748B */
--spotlight-success: 16, 185, 129; /* #10b981 */
--spotlight-danger: 244, 63, 94; /* #f43f5e */
--spotlight-info: 14, 165, 233; /* #0ea5e9 */
--spotlight-warning: 249, 115, 22; /* #f97316 */
--spotlight-primary-foreground: 231, 240, 252; /* #E7F0FC */
--spotlight-secondary-foreground: 250, 250, 250; /* #fafafa */
--spotlight-success-foreground: 231, 248, 242; /* #E7F8F2 */
--spotlight-danger-foreground: 254, 236, 239; /* #FEECEF */
--spotlight-info-foreground: 231, 246, 253; /* #E7F6FD */
--spotlight-warning-foreground: 254, 241, 232; /* #FEF1E8 */
}
This is the full set of tokens Arkyn falls back to internally. Redeclare only the ones you want to change, anything you leave out keeps using the library's default value.

Adding a Dark Theme

A dark theme is just a second set of values for the same variables. Instead of declaring them directly on :root, scope each set behind a selector that checks whether the <html> element carries a dark class. The light set stays the default, and the dark set overrides it whenever the class is present.

css

html:not(.dark):root {
--white: #ffffff;
/* background */
--background: #f4f4f5;
--background-underground: #fafafa;
--background-foreground: #ffffff;
/* text */
--text-heading: #09090b;
--text-body: #52525b;
--text-muted: #a1a1aa;
/* border */
--border: #e2e8f0;
/* inputs, textarea, select, checkbox, etc. */
--input-background: #fafafa;
/* tooltip */
--tooltip-text: #fafafa;
--tooltip-background: #27272a;
/* spotlight */
--spotlight-primary: 17, 109, 220; /* #116ddc */
--spotlight-secondary: 100, 116, 139; /* #64748B */
--spotlight-success: 16, 185, 129; /* #10b981 */
--spotlight-danger: 244, 63, 94; /* #f43f5e */
--spotlight-info: 14, 165, 233; /* #0ea5e9 */
--spotlight-warning: 249, 115, 22; /* #f97316 */
--spotlight-primary-foreground: 231, 240, 252; /* #E7F0FC */
--spotlight-secondary-foreground: 250, 250, 250; /* #fafafa */
--spotlight-success-foreground: 231, 248, 242; /* #E7F8F2 */
--spotlight-danger-foreground: 254, 236, 239; /* #FEECEF */
--spotlight-info-foreground: 231, 246, 253; /* #E7F6FD */
--spotlight-warning-foreground: 254, 241, 232; /* #FEF1E8 */
}
html.dark:root {
--white: #ffffff;
/* background */
--background: #09090b;
--background-underground: #151518;
--background-foreground: #18181b;
/* text */
--text-heading: #e4e4e7;
--text-body: #a1a1aa;
--text-muted: #71717a;
/* border */
--border: #2c2c30;
/* inputs, textarea, select, checkbox, etc. */
--input-background: #131316;
/* tooltip */
--tooltip-text: #fafafa;
--tooltip-background: #3f3f46;
/* spotlight */
--spotlight-primary: 15, 98, 204; /* #0F62CC */
--spotlight-secondary: 161, 161, 170; /* #A1A1AA */
--spotlight-success: 14, 165, 115; /* #0EA573 */
--spotlight-danger: 225, 55, 85; /* #E13755 */
--spotlight-info: 12, 148, 210; /* #0C94D2 */
--spotlight-warning: 234, 102, 18; /* #EA6612 */
--spotlight-primary-foreground: 3, 28, 61; /* #475569 */
--spotlight-secondary-foreground: 31, 31, 34; /* #1f1f22 */
--spotlight-success-foreground: 2, 44, 34; /* #022C22 */
--spotlight-danger-foreground: 76, 5, 25; /* #4C0519 */
--spotlight-info-foreground: 8, 47, 73; /* #082F49 */
--spotlight-warning-foreground: 69, 26, 3; /* #451A03 */
}
:not(.dark) avoids a specificity tie
Both selectors target :root, so without :not(.dark) the light block and the dark block would have the same specificity and the last one declared in the file would always win, regardless of the class. Scoping the light block to :not(.dark) makes the two selectors mutually exclusive.

Toggling the class from React

Arkyn does not decide when dark mode is active, your application does, by adding or removing the dark class on the <html> element. Since <html> is rendered outside the React root in a regular client-side app, the class has to be applied imperatively, not through JSX. A small hook that syncs a boolean state with document.documentElement is enough:

tsx

import { useEffect, useState } from "react";
function useDarkMode() {
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
document.documentElement.classList.toggle("dark", darkMode);
}, [darkMode]);
return { darkMode, setDarkMode };
}
export { useDarkMode };
Any component can then read and flip that state, for example with Switch:

tsx

import { Switch } from "@arkyn/components/switch";
import { useDarkMode } from "./useDarkMode";
function ThemeToggle() {
const { darkMode, setDarkMode } = useDarkMode();
return (
<Switch
unShowFieldTemplate
name="darkMode"
checked={darkMode}
onCheck={() => setDarkMode((current) => !current)}
/>
);
}
export { ThemeToggle };

Adding Multiple Color Themes

The same idea scales beyond a light/dark pair. Instead of a single dark class, use a data-theme attribute whose value selects a named color palette. Each theme becomes its own block, and the default palette is the one that applies when the attribute is missing.

css

html[data-theme="ocean"]:root {
--background: #0b1727;
--background-underground: #0f1f33;
--background-foreground: #142c47;
--text-heading: #e2e8f0;
--text-body: #cbd5e1;
--text-muted: #94a3b8;
--border: #1e3a5f;
--spotlight-primary: 56, 189, 248; /* #38BDF8 */
--spotlight-success: 45, 212, 191; /* #2DD4BF */
}
html[data-theme="sunset"]:root {
--background: #2b1710;
--background-underground: #331c13;
--background-foreground: #3d221a;
--text-heading: #fef3e2;
--text-body: #fde4c9;
--text-muted: #c2a68d;
--border: #55301f;
--spotlight-primary: 251, 146, 60; /* #FB923C */
--spotlight-danger: 248, 113, 113; /* #F87171 */
}
Undeclared variables fall back further down the chain
A themed block only needs to override the variables that actually change. Any variable it does not redeclare falls back to the block that applies with no data-theme set, if you defined one, and from there to Arkyn's own built-in default for that token.
The React side follows the same pattern as the dark mode hook, but stores the theme name instead of a boolean:

tsx

import { useEffect, useState } from "react";
function useAppTheme(defaultTheme = "light") {
const [theme, setTheme] = useState(defaultTheme);
useEffect(() => {
if (theme === "light") {
document.documentElement.removeAttribute("data-theme");
} else {
document.documentElement.setAttribute("data-theme", theme);
}
}, [theme]);
return { theme, setTheme };
}
export { useAppTheme };

tsx

import { useAppTheme } from "./useAppTheme";
function ThemePicker() {
const { theme, setTheme } = useAppTheme();
return (
<select value={theme} onChange={(event) => setTheme(event.target.value)}>
<option value="light">Light</option>
<option value="ocean">Ocean</option>
<option value="sunset">Sunset</option>
</select>
);
}
export { ThemePicker };
Dark mode can be folded into this same system as just another data-theme value instead of a separate class, if your app already needs more than two palettes.

Import Order

The import order must always place the global CSS before Arkyn's bundled stylesheet. This gives your layer structure a chance to define the priority correctly.

ts

import "<yourPath>/reset.css";
import "@arkyn/components/styles";
If you also keep the Arkyn CSS variables in a separate file, import that file before the bundled stylesheet as well, so the tokens are available when Arkyn's components are evaluated.

Notes

  • Keep your base resets inside a layer such as reset, and reserve the arkyn layer for the library stylesheet.
  • When you need to override a token, prefer changing the CSS variable value instead of writing stronger selectors.
  • If a component looks incorrect, first verify that the global stylesheet is imported before @arkyn/components/styles.
  • Persisting the active theme (for example in localStorage) and applying it before the first paint avoids a visible flash of the wrong theme on load, but that step is optional and outside the scope of Arkyn itself.
  • Whether you use a boolean class or a data-theme attribute, the rule stays the same: your app owns the selector on <html>, and Arkyn only reacts to the resulting CSS variables.
Related in @arkyn/components
On this page
    arkyn