Foundation
Abnormal Theme
Color, type, radius and motion tokens, with light and dark values. Import abnormal.css once after Tailwind.
Preview
backgroundforegroundsurfacemutedprimarybrandhighlightsignalsuccessdestructiveborderinputchipcontrolmarkpaneltext-xs: the quick brown fox
text-sm: the quick brown fox
text-base: the quick brown fox
text-lg: the quick brown fox
text-xl: the quick brown fox
text-2xl: the quick brown fox
text-3xl: the quick brown fox
Install
pnpm dlx shadcn@latest add @abnormal/themeUsage
const colors = [
"background",
"foreground",
"surface",
"muted",
"primary",
"brand",
"highlight",
"signal",
"success",
"destructive",
"border",
"input",
];
const radii = ["rounded-chip", "rounded-control", "rounded-mark", "rounded-panel"];
const sizes = ["text-xs", "text-sm", "text-base", "text-lg", "text-xl", "text-2xl", "text-3xl"];
export default function ThemeDemo() {
return (
<div className="grid w-full gap-8">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
{colors.map((color) => (
<div key={color} className="grid gap-1.5">
<div className="h-12 rounded-control border border-border" style={{ background: `var(--${color})` }} />
<code className="font-mono text-xs text-muted-foreground">{color}</code>
</div>
))}
</div>
<div className="flex flex-wrap gap-3">
{radii.map((radius) => (
<div key={radius} className={`grid size-24 place-items-center border border-border bg-surface ${radius}`}>
<code className="font-mono text-xs text-muted-foreground">{radius.replace("rounded-", "")}</code>
</div>
))}
</div>
<div className="grid gap-2">
{sizes.map((size) => (
<p key={size} className={size}>
{size}: the quick brown fox
</p>
))}
</div>
</div>
);
}
Source
/*
* Abnormal design tokens. Install with `shadcn add @abnormal/theme`, then import it once right after Tailwind:
*
* @import "tailwindcss";
* @import "./abnormal.css";
*
* The palette is ink #141412 on warm paper #f7f7f4; dark mode is the ink itself.
* - The primary color is the ink: light #141412 with white text (18.4:1), dark #f0f0ea with ink text (16.1:1).
* - Highlighter yellow #ffe14d is the only chromatic brand color. It marks what is current, selected or new,
* always carries ink text (14.2:1), and is never text on a light background nor the background of long text.
* - Cards have no fill (card is the page color); layering comes from thin borders.
* - control and input are semi-transparent ink, so they read the same over the page, the sidebar and popovers.
* - hover is opaque: pinned table columns rely on it to cover the content scrolling underneath.
*
* The theme is written on <html data-theme="light|dark">. Before any script runs it follows the system preference.
*/
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
@custom-variant data-open {
&:where([data-state="open"]),
&:where([data-open]:not([data-open="false"])) {
@slot;
}
}
@custom-variant data-closed {
&:where([data-state="closed"]),
&:where([data-closed]:not([data-closed="false"])) {
@slot;
}
}
@custom-variant data-checked {
&:where([data-state="checked"]),
&:where([data-checked]:not([data-checked="false"])) {
@slot;
}
}
@custom-variant data-unchecked {
&:where([data-state="unchecked"]),
&:where([data-unchecked]:not([data-unchecked="false"])) {
@slot;
}
}
@custom-variant data-disabled {
&:where([data-disabled="true"]),
&:where([data-disabled]:not([data-disabled="false"])) {
@slot;
}
}
:root {
--brand: #141412;
--brand-foreground: #ffffff;
--highlight: #ffe14d;
--highlight-foreground: #141412;
/* Soft yellow surface for selected rows and the chosen option: text 16.6:1, secondary text 6.1:1. */
--highlight-muted: #fff4bf;
/* Small indicators that must stay visible on any surface (dots, progress, numbering): ink in light, yellow in dark. */
--signal: #141412;
--bg: #f7f7f4;
--surface: #ecece6;
--text: #141412;
--text-secondary: #5c5c55;
--border: #e3e3dc;
--hover: #efefea;
--control: #1414120b;
--select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235c5c55' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
--input: #14141222;
/* Status colors stay at least 4.5:1 on their own 10-25% tints over the page and popovers. */
--badge-ink: #704800;
--success: #335c24;
--destructive: #9f1a2c;
--card: var(--bg);
--popover-bg: #ffffff;
--sidebar: #f2f2ee;
--sidebar-foreground: var(--text);
--sidebar-border: var(--border);
/* Focus outline: 3.3:1 against the paper background, meeting the non-text contrast requirement. */
--focus: #8a8a80;
--panel-shadow: 0 0 #0000;
--popover-shadow: 0 4px 12px #1414120d, 0 16px 40px #14141217;
/* Categorical chart colors, fixed order. The last two are below 3:1, so charts always carry a legend. */
--chart-1: #2a78d6;
--chart-2: #eb6834;
--chart-3: #1baf7a;
--chart-4: #eda100;
color-scheme: light;
}
:root[data-theme="dark"] {
--brand: #f0f0ea;
--brand-foreground: #141412;
/* Text 11.5:1, secondary text 5.3:1. */
--highlight-muted: #34311b;
--signal: #ffe14d;
--bg: #141412;
--surface: #252520;
--text: #f0f0ea;
--text-secondary: #a6a69c;
--border: #2e2e29;
--hover: #1c1c19;
--control: #f0f0ea0d;
--select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a6a69c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
--input: #f0f0ea1f;
--badge-ink: #e8b85a;
--success: #a8c48f;
--destructive: #ff8a96;
--popover-bg: #1c1c19;
--sidebar: #0f0f0d;
/* 3.6:1 against #141412. */
--focus: #6f6f66;
--popover-shadow: 0 0 0 1px #f0f0ea08, 0 16px 48px #00000080;
--chart-1: #3987e5;
--chart-2: #d95926;
--chart-3: #199e70;
--chart-4: #c98500;
color-scheme: dark;
}
/* The same dark values, for the moment before a script has written data-theme. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--brand: #f0f0ea;
--brand-foreground: #141412;
--highlight-muted: #34311b;
--signal: #ffe14d;
--bg: #141412;
--surface: #252520;
--text: #f0f0ea;
--text-secondary: #a6a69c;
--border: #2e2e29;
--hover: #1c1c19;
--control: #f0f0ea0d;
--select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a6a69c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
--input: #f0f0ea1f;
--badge-ink: #e8b85a;
--success: #a8c48f;
--destructive: #ff8a96;
--popover-bg: #1c1c19;
--sidebar: #0f0f0d;
--focus: #6f6f66;
--popover-shadow: 0 0 0 1px #f0f0ea08, 0 16px 48px #00000080;
--chart-1: #3987e5;
--chart-2: #d95926;
--chart-3: #199e70;
--chart-4: #c98500;
color-scheme: dark;
}
}
/* shadcn's semantic names, mapped onto the tokens above. */
:root {
--background: var(--bg);
--foreground: var(--text);
--card-foreground: var(--text);
--popover: var(--popover-bg);
--popover-foreground: var(--text);
--primary: var(--brand);
--primary-foreground: var(--brand-foreground);
--secondary: var(--surface);
--secondary-foreground: var(--text);
--muted: var(--surface);
--muted-foreground: var(--text-secondary);
--accent: var(--hover);
--accent-foreground: var(--text);
--ring: var(--focus);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-success: var(--success);
--color-border: var(--border);
--color-input: var(--input);
--color-control: var(--control);
--color-ring: var(--ring);
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
--color-highlight: var(--highlight);
--color-highlight-foreground: var(--highlight-foreground);
--color-highlight-muted: var(--highlight-muted);
--color-signal: var(--signal);
--color-surface: var(--surface);
--color-badge-ink: var(--badge-ink);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--shadow-panel: var(--panel-shadow);
--shadow-popover: var(--popover-shadow);
/*
* Body text is Inter, headings are Bricolage Grotesque, code and data are JetBrains Mono.
* next/font projects set --font-inter and friends; projects using @abnormal/fonts fall back to the family names.
* None of them has Chinese glyphs, so the Chinese fallbacks are chained in explicitly.
*/
--font-sans:
var(--font-inter, "Inter"), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
--font-heading:
var(--font-bricolage, "Bricolage Grotesque"), var(--font-inter, "Inter"), -apple-system, BlinkMacSystemFont,
"Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
--font-mono:
var(--font-jetbrains-mono, "JetBrains Mono"), ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono",
"PingFang SC", "Microsoft YaHei", monospace;
/* A stable reading hierarchy: body 14px, secondary text 12-13px, page title 28px. */
--text-xs: 12px;
--text-xs--line-height: 1.5;
--text-sm: 13px;
--text-sm--line-height: 1.5;
--text-base: 14px;
--text-base--line-height: 1.5;
--text-md: 15px;
--text-md--line-height: 1.5;
--text-lg: 16px;
--text-lg--line-height: 1.45;
--text-xl: 20px;
--text-xl--line-height: 1.4;
--text-2xl: 24px;
--text-2xl--line-height: 1.3;
--text-3xl: 28px;
--text-3xl--line-height: 1.2;
--text-4xl: 42px;
--text-4xl--line-height: 1.2;
/* Body text inherits 1.55; long-form copy opens up to 1.8. Use as text-md/prose. */
--leading-body: 1.55;
--leading-prose: 1.8;
/* Corners are tight, matching the square brand tile: badges 4, controls 6, popovers 8, cards and dialogs 12. */
--radius-chip: 4px;
--radius-control: 6px;
--radius-mark: 8px;
--radius-dialog: 12px;
--radius-panel: 12px;
/*
* Enter and exit animations use transform rather than translate: Tailwind's -translate-x-1/2 sets the CSS translate
* property, and an animation that also sets translate would override the centering of a dialog.
*/
--animate-arrive: arrive 0.15s ease-out both;
--animate-leave: leave 0.12s ease-in both;
--animate-fade-in: fade-in 0.15s ease-out both;
--animate-fade-out: fade-out 0.12s ease-in both;
--animate-slide-in-right: slide-in-right 0.22s cubic-bezier(0.32, 0.72, 0, 1) both;
--animate-slide-out-right: slide-out-right 0.16s ease-in both;
--animate-slide-in-left: slide-in-left 0.22s cubic-bezier(0.32, 0.72, 0, 1) both;
--animate-slide-out-left: slide-out-left 0.16s ease-in both;
--animate-flash: flash 1.2s ease-out both;
--animate-pop: pop 0.18s cubic-bezier(0.34, 1.56, 0.64, 1) both;
@keyframes arrive {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes leave {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(3px);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slide-in-right {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes slide-out-right {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
@keyframes slide-in-left {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
@keyframes slide-out-left {
from {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
@keyframes flash {
from {
background-color: color-mix(in oklab, var(--success) 14%, transparent);
}
to {
background-color: transparent;
}
}
@keyframes pop {
from {
opacity: 0;
transform: scale(0.6);
}
to {
opacity: 1;
transform: scale(1);
}
}
}
@layer base {
* {
border-color: var(--border);
scrollbar-color: var(--input) transparent;
}
html {
-webkit-text-size-adjust: 100%;
font-synthesis: none;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
font-size: 14px;
line-height: 1.55;
font-feature-settings: "cv11", "ss03";
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3,
h4 {
font-family: var(--font-heading);
font-weight: 600;
letter-spacing: -0.02em;
}
/* Selected text is the brand's highlighter: yellow with ink text in both themes. */
::selection {
background: var(--highlight);
color: var(--highlight-foreground);
}
a,
button,
summary {
-webkit-tap-highlight-color: transparent;
}
/* Keyboard focus: a 2px solid gray outline. Input-type controls hug their border; buttons and links leave a 2px gap. */
:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[data-slot="select-trigger"]:focus-visible {
outline-offset: -1px;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation: none !important;
transition: none !important;
}
}
}
/* Toasts use the same floating layer style as menus. */
[data-sonner-toaster] [data-sonner-toast][data-styled="true"] {
box-shadow: var(--popover-shadow);
font-family: inherit;
font-size: 14px;
border-radius: var(--radius-panel);
}
/*
* Keep phone inputs at 16px, to stop iOS zooming the whole page on focus.
* Outside any layer on purpose: the controls set their size with text-base, and a utility beats anything in @layer base.
*/
@media (max-width: 639px) {
[data-slot="input"],
[data-slot="textarea"],
[data-slot="native-select"],
[data-slot="select-trigger"] {
font-size: 16px;
}
}