Home / Snippets / Color & Theming /
Dark mode border
Borders that automatically adapt to dark mode using CSS custom properties and oklch() — no JavaScript.
Card with adaptive border
This card uses a border that adapts to the current color scheme. In dark mode, the border is subtle and blends with the background.
Quick implementation
/* HTML: <div class="card">Content</div> */
.card {
/* Subtle border that adapts to dark mode */
border: 1px solid var(--card-border);
border-radius: var(--radius);
background: var(--card);
padding: 1rem;
}
/* In site.css, these tokens are defined: */
/* :root {
--card-border: oklch(0.9 0.01 265 / 0.5);
}
@media (prefers-color-scheme: dark) {
:root {
--card-border: oklch(0.25 0.02 265 / 0.4);
}
} */
Prompt this to your LLM
Includes role, constraints, two framework variants, and edge cases to handle.
You are a senior frontend engineer specializing in modern CSS.
Goal: Build a border system that automatically adapts to dark mode using CSS custom properties.
Technical constraints:
- Use CSS custom properties for the border color, with a fallback chain.
- Define the border color in oklch() — never use hex or rgba().
- Use @media (prefers-color-scheme: dark) to override the border token.
- The light mode border should be subtle: oklch(0.9 0.01 265 / 0.5).
- The dark mode border should be softer: oklch(0.25 0.02 265 / 0.4).
Framework variant (pick one):
A) Vanilla HTML + CSS only — return a .card class with the border token applied.
B) React component — accept `children`, `className`, and `variant` props; include the CSS as a CSS module.
Edge cases to handle:
- What happens if the user has high-contrast mode enabled? Use @media (forced-colors) to adjust.
- Provide a fallback for browsers that don't support oklch() — use a light gray as the fallback.
- ARIA note: ensure the border provides sufficient visual distinction for keyboard focus states.
Return the HTML structure and the CSS, clearly separated.
Why this matters in 2026
Dark mode is no longer optional — users expect it, and operating systems ship with it enabled by default. The old approach of hardcoding border colors means either shipping two separate stylesheets or writing verbose media queries for every component. With CSS custom properties, you define the border color once and let the cascade handle the rest.
The real power comes from combining custom properties with oklch(): perceptually uniform color ensures your borders look consistent across different displays and color profiles. No more "why does this gray look blue on my screen?"
The logic
CSS custom properties define the border color at the root level. In light mode, the border is a light gray with low alpha: oklch(0.9 0.01 265 / 0.5). When the user's system prefers dark mode, @media (prefers-color-scheme: dark) overrides that token with a darker value: oklch(0.25 0.02 265 / 0.4).
Why oklch() for borders? Traditional grays like #ccc or rgba(255,255,255,0.5) don't account for perceptual uniformity. In oklch(), lightness (L) maps directly to perceived brightness, chroma (C) controls saturation, and hue (H) is optional. This means your dark mode borders stay neutral without any extra tweaking.
The cascade does the work: because the media query comes after the initial declaration, it only applies when the condition is true. No JavaScript, no class toggling, no storage needed.
Accessibility & performance
Borders must meet contrast requirements for users with low vision. Test your border colors against the WCAG 2.1 AA standard (4.5:1 for normal text, 3:1 for large text). For keyboard users, ensure focus rings are visible on top of borders — consider using :focus-visible to add an extra outline.
Performance-wise, CSS custom properties are resolved at paint time, not layout time. Changing a border color via a token doesn't trigger reflow. The @media query is evaluated once when the page loads (or when the user changes system settings), making this approach essentially free.
--card-border consistently across all your cards, inputs, and dividers. When you need to tweak the contrast, you change one token — not 50 individual rules.