diff --git a/src/docs/patterns/color/background/background.mdx b/src/docs/patterns/color/background/background.mdx
new file mode 100644
index 00000000..9c2c9f5f
--- /dev/null
+++ b/src/docs/patterns/color/background/background.mdx
@@ -0,0 +1,37 @@
+import { Canvas, Meta, Story } from "@storybook/blocks";
+import { ColorBackground } from "./background";
+
+
+
+# Background Color
+
+The `background` utility contains classes that set the
+[CSS `background-color`][1] property. This is the recommended way to set
+background color since they automatically change based on the current theme.
+
+```ts
+import { background } from "@moai/core";
+
+
Text
;
+```
+
+There are only 2 colors in the `background` utility at the moment:
+
+
+
+The `weak` value sets a light gray background on light theme and a daker
+background on dark theme. It should be used for underlying backgrounds, like
+the background of your app. It can also be used to separate an area, such as
+the header of a table.
+
+The `strong` value sets a white background on light theme and a lighter
+background on dark theme. It should be used for elevated containers, such as
+panes, toolbars or popovers.
+
+## See also
+
+- The [Pane][2] component uses `strong` background along with border and shadow
+ to better elevate contents.
+
+[1]: https://developer.mozilla.org/en-US/docs/Web/CSS/background-color
+[2]: /docs/components-pane-docs
diff --git a/src/docs/patterns/color/background/background.module.css b/src/docs/patterns/color/background/background.module.css
new file mode 100644
index 00000000..d23bccd1
--- /dev/null
+++ b/src/docs/patterns/color/background/background.module.css
@@ -0,0 +1,11 @@
+.name {
+ font-family: "Source Code Pro", monospace;
+ width: 160px;
+ min-width: 160px;
+}
+
+.container {
+ overflow: auto;
+ white-space: nowrap;
+ border-width: 2px;
+}
diff --git a/src/docs/patterns/color/background/background.tsx b/src/docs/patterns/color/background/background.tsx
new file mode 100644
index 00000000..a7e6e540
--- /dev/null
+++ b/src/docs/patterns/color/background/background.tsx
@@ -0,0 +1,48 @@
+import { border, background, Table, text } from "../../../../core";
+import { ColorSample } from "../sample/sample";
+import s from "./background.module.css";
+
+type BackgroundKey = keyof typeof background;
+
+interface Row {
+ key: BackgroundKey;
+}
+
+const MakeColumn =
+ (theme: "light" | "dark", text: string) =>
+ (row: Row): JSX.Element => (
+
+
+
+ );
+
+const LightStrong = MakeColumn("light", text.normal);
+const LightWeak = MakeColumn("light", text.muted);
+const DarkStrong = MakeColumn("dark", text.normal);
+const DarkWeak = MakeColumn("dark", text.muted);
+
+interface Props {
+ rows: Row[];
+}
+
+export const ColorBackground = (props: Props): JSX.Element => (
+
+
+ size={Table.sizes.small}
+ fixed={{ firstColumn: true }}
+ fill
+ rows={props.rows}
+ rowKey={(row) => row.key}
+ columns={[
+ { title: "Name", className: s.name, render: "key" },
+ { title: "Light", render: LightStrong },
+ { title: "Light (muted)", render: LightWeak },
+ { title: "Dark", render: DarkStrong },
+ { title: "Dark (muted)", render: DarkWeak },
+ ]}
+ />
+
+);
diff --git a/src/docs/patterns/color/border/border.mdx b/src/docs/patterns/color/border/border.mdx
new file mode 100644
index 00000000..02cab140
--- /dev/null
+++ b/src/docs/patterns/color/border/border.mdx
@@ -0,0 +1,28 @@
+import { Meta, Canvas, Story } from "@storybook/blocks";
+import { ColorBorder } from "./border";
+
+
+
+# Border Color
+
+The `border` utility contains classes that set the [CSS `border-color`][1]
+property. This is the recommended way to set border color since they
+automatically change based on the current theme.
+
+Note that this only set the color. To have a border, you also need to set a
+width, e.g. using the "border" class from Tailwind. You don't need to set the
+border style since it's already default to "solid" in our [CSS reset][2].
+
+```ts
+import { border } from "@moai/core";
+
+// The "border" class comes from Tailwind to set the border width
+Text
;
+```
+
+There are 2 colors in the `border` utility at the moment:
+
+
+
+[1]: https://developer.mozilla.org/en-US/docs/Web/CSS/border-color
+[2]: https://github.com/moaijs/moai/blob/739a87de82bd061bb41f38c5a51a410b59944a3d/lib/core/src/style/reset.css#L404-L417
diff --git a/src/docs/patterns/color/border/border.module.css b/src/docs/patterns/color/border/border.module.css
new file mode 100644
index 00000000..d23bccd1
--- /dev/null
+++ b/src/docs/patterns/color/border/border.module.css
@@ -0,0 +1,11 @@
+.name {
+ font-family: "Source Code Pro", monospace;
+ width: 160px;
+ min-width: 160px;
+}
+
+.container {
+ overflow: auto;
+ white-space: nowrap;
+ border-width: 2px;
+}
diff --git a/src/docs/patterns/color/border/border.tsx b/src/docs/patterns/color/border/border.tsx
new file mode 100644
index 00000000..a0512dbe
--- /dev/null
+++ b/src/docs/patterns/color/border/border.tsx
@@ -0,0 +1,48 @@
+import { background, border, Table } from "../../../../core";
+import { ColorSample } from "../sample/sample";
+import s from "./border.module.css";
+
+type BorderKey = keyof typeof border;
+
+interface Row {
+ key: BorderKey;
+}
+
+const MakeColumn =
+ (theme: "light" | "dark", back: string) =>
+ (row: Row): JSX.Element => (
+
+
+
+ );
+
+const LightStrong = MakeColumn("light", background.strong);
+const LightWeak = MakeColumn("light", background.weak);
+const DarkStrong = MakeColumn("dark", background.strong);
+const DarkWeak = MakeColumn("dark", background.weak);
+
+interface Props {
+ rows: Row[];
+}
+
+export const ColorBorder = (props: Props): JSX.Element => (
+
+
+ size={Table.sizes.small}
+ fixed={{ firstColumn: true }}
+ fill
+ rows={props.rows}
+ rowKey={(row) => row.key}
+ columns={[
+ { title: "Name", className: s.name, render: "key" },
+ { title: "Light", render: LightStrong },
+ { title: "Light (alt bg)", render: LightWeak },
+ { title: "Dark", render: DarkStrong },
+ { title: "Dark (alt bg)", render: DarkWeak },
+ ]}
+ />
+
+);
diff --git a/src/docs/patterns/color/category/category.mdx b/src/docs/patterns/color/category/category.mdx
new file mode 100644
index 00000000..f715c30a
--- /dev/null
+++ b/src/docs/patterns/color/category/category.mdx
@@ -0,0 +1,23 @@
+import { Meta, Canvas, Story } from "@storybook/blocks";
+import { ColorCategoryTable } from "./category";
+
+
+
+# Category Colors
+
+Category colors have no semantic meaning attached. Instead, they are used to
+show relationships between elements (e.g. categorization, labelling in data
+visualizations). They still conform to the AA level of [WCAG of contrast
+ratios][1].
+
+## Usage
+
+[At the moment][2], Moai does not support using category colors directly.
+Instead, category colors are used via several components that support them:
+
+
+
+(The list is quite short at the moment. We are adding more to it!)
+
+[1]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Perceivable/Color_contrast
+[2]: https://github.com/moaijs/moai/issues/210
diff --git a/src/docs/patterns/color/category/category.module.css b/src/docs/patterns/color/category/category.module.css
new file mode 100644
index 00000000..b5002ec9
--- /dev/null
+++ b/src/docs/patterns/color/category/category.module.css
@@ -0,0 +1,3 @@
+.container {
+ border-width: 2px;
+}
diff --git a/src/docs/patterns/color/category/category.tsx b/src/docs/patterns/color/category/category.tsx
new file mode 100644
index 00000000..d30c9640
--- /dev/null
+++ b/src/docs/patterns/color/category/category.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import { border, Table } from "../../../../core";
+import { GalleryTag } from "../../../../gallery";
+import s from "./category.module.css";
+
+interface Row {
+ name: string;
+ link: string;
+ example: React.ReactNode;
+}
+
+const Name = (row: Row): JSX.Element => (
+
+);
+
+const Example = (row: Row): JSX.Element => ;
+
+export const ColorCategoryTable = (): JSX.Element => (
+
+
+ rows={[
+ {
+ name: "Tag",
+ link: "/docs/components-tag",
+ example: ,
+ },
+ ]}
+ rowKey={(row) => row.name}
+ columns={[
+ { title: "Component", render: Name },
+ { title: "Example", render: Example },
+ ]}
+ fill
+ fixed={{ firstColumn: true }}
+ />
+
+);
diff --git a/src/docs/patterns/color/color.mdx b/src/docs/patterns/color/color.mdx
new file mode 100644
index 00000000..785ed116
--- /dev/null
+++ b/src/docs/patterns/color/color.mdx
@@ -0,0 +1,20 @@
+import { Canvas, Meta, Story } from "@storybook/blocks";
+
+
+
+# Colors (index page)
+
+For recommended color usages, see [Text][1], [Background][2], and [Border][3].
+They provide accessible colors that are automatically changed based on the
+current theme.
+
+For colors that are persistent across themes, see [Static Colors][4].
+
+For colors that have no semantic meaning and should be used for data
+visualizations, see [Category Colors][5]
+
+[1]: /docs/patterns-color-text--docs
+[2]: /docs/patterns-color-background--docs
+[3]: /docs/patterns-color-border--docs
+[4]: /docs/patterns-color-static--docs
+[5]: /docs/patterns-color-category--docs
diff --git a/src/docs/patterns/color/sample/sample.module.css b/src/docs/patterns/color/sample/sample.module.css
new file mode 100644
index 00000000..b50ab3b2
--- /dev/null
+++ b/src/docs/patterns/color/sample/sample.module.css
@@ -0,0 +1,17 @@
+.container {
+ padding: 12px;
+ width: 120px;
+ font-variant-numeric: "tabular-nums";
+ border-radius: 4px;
+
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.border {
+ width: 16px;
+ height: 16px;
+ border-radius: 16px;
+ border-width: 1px;
+}
diff --git a/src/docs/patterns/color/sample/sample.tsx b/src/docs/patterns/color/sample/sample.tsx
new file mode 100644
index 00000000..e2036f43
--- /dev/null
+++ b/src/docs/patterns/color/sample/sample.tsx
@@ -0,0 +1,82 @@
+import Color from "color";
+import { useEffect, useRef, useState } from "react";
+import { HiCheckCircle } from "react-icons/hi";
+import { CategoryColor, Icon, Tag, categoryColors } from "../../../../core";
+import s from "./sample.module.css";
+
+export type ColorSampleUsage = "text" | "icon" | "both";
+
+interface Props {
+ background: string;
+ foreground:
+ | { type: "text"; cls: string; usage: ColorSampleUsage }
+ | { type: "border"; cls: string };
+}
+
+const getColor = (contrast: number): CategoryColor => {
+ const rounded = Math.round(contrast * 10) / 10;
+ if (rounded >= 4.5) return categoryColors.green;
+ if (rounded >= 3) return categoryColors.yellow;
+ return categoryColors.red;
+};
+
+const getContrast = (
+ props: Props,
+ backElement: HTMLDivElement,
+ foreElement: HTMLElement,
+): number => {
+ const back = window.getComputedStyle(backElement).backgroundColor;
+ const foreStyle = window.getComputedStyle(foreElement);
+ const isText = props.foreground.type === "text";
+ // Only use long hand name.
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle#notes
+ const fore = foreStyle[isText ? "color" : "borderLeftColor"];
+ return Color(back).contrast(Color(fore));
+};
+
+const ColorIcon = (): JSX.Element => (
+
+);
+
+export const ColorSample = (props: Props): JSX.Element => {
+ const backRef = useRef(null);
+ const foreRef = useRef(null);
+ const [contrast, setContrast] = useState(0);
+
+ useEffect(() => {
+ window.setTimeout(() => {
+ const [back, fore] = [backRef.current, foreRef.current];
+ if (back === null) throw Error("backElm is null");
+ if (fore === null) throw Error("foreElm is null");
+ setContrast(getContrast(props, back, fore));
+ }, 0); // Wait for all styles are applied
+ }, [setContrast, props]);
+
+ return (
+
+ {/* "background" also set color so the "fore" must be in another
+ element of the "back" */}
+ {props.foreground.type === "text" ? (
+
+ {props.foreground.usage !== "icon" && Aa}
+ {props.foreground.usage === "both" && }
+ {props.foreground.usage !== "text" && }
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/docs/patterns/color/static/grid.module.css b/src/docs/patterns/color/static/grid.module.css
new file mode 100644
index 00000000..0516c7d0
--- /dev/null
+++ b/src/docs/patterns/color/static/grid.module.css
@@ -0,0 +1,6 @@
+.container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, 300px);
+ gap: 16px;
+ max-width: 640px;
+}
diff --git a/src/docs/patterns/color/static/grid.tsx b/src/docs/patterns/color/static/grid.tsx
new file mode 100644
index 00000000..9154fc0d
--- /dev/null
+++ b/src/docs/patterns/color/static/grid.tsx
@@ -0,0 +1,11 @@
+import s from "./grid.module.css";
+import { ColorStaticTable } from "./table";
+
+export const ColorStaticGrid = (): JSX.Element => (
+
+
+
+
+
+
+);
diff --git a/src/docs/patterns/color/static/sample.module.css b/src/docs/patterns/color/static/sample.module.css
new file mode 100644
index 00000000..3720a242
--- /dev/null
+++ b/src/docs/patterns/color/static/sample.module.css
@@ -0,0 +1,11 @@
+.container {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.circle {
+ width: 24px;
+ height: 24px;
+ border-radius: 24px;
+}
diff --git a/src/docs/patterns/color/static/sample.tsx b/src/docs/patterns/color/static/sample.tsx
new file mode 100644
index 00000000..f136ca5d
--- /dev/null
+++ b/src/docs/patterns/color/static/sample.tsx
@@ -0,0 +1,33 @@
+import Color from "color";
+import { useEffect, useRef, useState } from "react";
+import { text } from "../../../../core";
+import s from "./sample.module.css";
+
+interface Props {
+ name: string;
+}
+
+export const ColorStaticSample = (props: Props): JSX.Element => {
+ const ref = useRef(null);
+ const [hex, setHex] = useState("");
+
+ useEffect(() => {
+ window.setTimeout(() => {
+ const div = ref.current;
+ if (div === null) throw Error("Ref is not attached");
+ const bg = window.getComputedStyle(div).backgroundColor;
+ setHex(Color(bg).hex());
+ }, 0); // Wait for all colors to be applied correctly
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/src/docs/patterns/color/static/static.mdx b/src/docs/patterns/color/static/static.mdx
new file mode 100644
index 00000000..9d0b991e
--- /dev/null
+++ b/src/docs/patterns/color/static/static.mdx
@@ -0,0 +1,55 @@
+import { Canvas, Meta, Story } from "@storybook/blocks";
+import { ColorStaticGrid } from "./grid";
+
+
+
+# Static Colors
+
+Static colors are persistent across themes. They are the low-level palette
+that Moai is built on top of. They ensure consistency across all parts of the
+interface.
+
+In practice, however, you usually don't need to use static colors directly.
+Instead, use high-level utilities like [`text`][1], [`background`][2], and
+[`border`][3] which provide dynamic colors that are changed based on the
+current theme and [always accessible][4].
+
+## Usage
+
+Static colors are defined as [CSS variables][5] at [`root`][6]. They are
+available everywhere in the app:
+
+```css
+.heading {
+ color: var(--highlight-5);
+}
+```
+
+Since static colors are persistent across themes, you usually want to provide
+the value for each theme manually:
+
+```css
+html.light .heading {
+ color: var(--highlight-6);
+}
+html.dark .heading {
+ color: var(--highlight-4);
+}
+```
+
+In fact, the [implementations][7] of `text`, `background`, and `border`
+utilities are good examples of handling colors in both light and dark themes.
+
+## Colors
+
+The below tables list all static colors in Moai and their variable names.
+
+
+
+[1]: /docs/patterns-color-text--docs
+[2]: /docs/patterns-color-background--docs
+[3]: /docs/patterns-color-border--docs
+[4]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Perceivable/Color_contrast
+[5]: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties
+[6]: https://developer.mozilla.org/en-US/docs/Web/CSS/:root
+[7]: https://github.com/moaijs/moai/blob/739a87de82bd061bb41f38c5a51a410b59944a3d/lib/core/src/text/text.module.css
diff --git a/src/docs/patterns/color/static/table.module.css b/src/docs/patterns/color/static/table.module.css
new file mode 100644
index 00000000..e9491afe
--- /dev/null
+++ b/src/docs/patterns/color/static/table.module.css
@@ -0,0 +1,16 @@
+.container {
+ border-width: 2px;
+}
+
+.container table {
+ table-layout: fixed;
+}
+
+.color {
+ width: 160px;
+}
+
+.name {
+ width: 120px;
+ font-variant-numeric: tabular-nums;
+}
diff --git a/src/docs/patterns/color/static/table.tsx b/src/docs/patterns/color/static/table.tsx
new file mode 100644
index 00000000..97bd95ef
--- /dev/null
+++ b/src/docs/patterns/color/static/table.tsx
@@ -0,0 +1,31 @@
+import { border, Table } from "../../../../core";
+import { ColorStaticSample } from "./sample";
+import s from "./table.module.css";
+
+interface Props {
+ name: string;
+}
+
+const Color =
+ (props: Props) =>
+ (row: number): JSX.Element => (
+
+ );
+
+const Name =
+ (props: Props) =>
+ (row: number): JSX.Element => ;
+
+export const ColorStaticTable = (props: Props): JSX.Element => (
+
+
+ fill
+ rows={[...Array(10).keys()]}
+ rowKey={(row) => row.toString()}
+ columns={[
+ { title: "Color", className: s.color, render: Color(props) },
+ { title: "Name", className: s.name, render: Name(props) },
+ ]}
+ />
+
+);
diff --git a/src/docs/patterns/color/text/text.mdx b/src/docs/patterns/color/text/text.mdx
new file mode 100644
index 00000000..00ee27d9
--- /dev/null
+++ b/src/docs/patterns/color/text/text.mdx
@@ -0,0 +1,77 @@
+import { Canvas, Meta, Story } from "@storybook/blocks";
+import { ColorText } from "./text";
+
+
+
+# Text Color
+
+The `text` utility contains classes that set the [CSS `color`][1] property.
+This is the recommended way to set color for texts and icons since they
+automatically change based on the current theme and always conform to the
+[WCAG of contrast ratios][3] at AA level.
+
+```ts
+import { text } from "@moai/core";
+
+Text;
+```
+
+## Gray colors
+
+There are 2 gray colors in the `text` utility. They can be used for both texts
+and icons due to their high contrast ratios:
+
+
+
+The `normal` value sets the color back to the default text color, e.g. a
+near-black in light theme. Since it's the app's default, you usually don't need
+to use this class, except to override an inherited color from a parent.
+
+The `muted` value dims the color. It is usually used for secondary texts, like
+labels and descriptive texts. Their contrast ratios are always above 4.5 to
+[ensure legibility][3] in both light and dark themes.
+
+## Semantic colors
+
+There are 3 semantic colors in the `text` utility. Each has a `Strong` version
+for texts (contrast 4.5+) and a `Weak` version for icons (contrast 3+):
+
+
+
+Semantic colors are used to communicate meaning and intention to users.
+However, they should not be the only method as some users [may not recognize][4]
+them. Other methods, such as helper texts or icons, should be used together to
+ensure good accessibility.
+
+Moai uses green for positive intentions and red for negative ones. This may
+not work for some users whose culture perceives red as a positive color. In
+these cases, you may want to [customize][6] the `text` utility to follow your
+users.
+
+## See also
+
+- For static colors that don't change based on the current theme, see
+ [Static Colors][7].
+- For a wide range of colors that don't have semantic meaning, see
+ [Category Colors][8].
+
+[1]: https://developer.mozilla.org/en-US/docs/Web/CSS/color
+[3]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Perceivable/Color_contrast
+[4]: https://en.wikipedia.org/wiki/Color_blindness
+[6]: /docs/draft-extension--docs
+[7]: /docs/patterns-color-static--docs
+[8]: /docs/patterns-color-category--docs
diff --git a/src/docs/patterns/color/text/text.module.css b/src/docs/patterns/color/text/text.module.css
new file mode 100644
index 00000000..50ec057d
--- /dev/null
+++ b/src/docs/patterns/color/text/text.module.css
@@ -0,0 +1,16 @@
+.name {
+ font-family: "Source Code Pro", monospace;
+ width: 160px;
+ min-width: 160px;
+}
+
+.usage {
+ width: 160px;
+ min-width: 160px;
+}
+
+.container {
+ overflow: auto;
+ white-space: nowrap;
+ border-width: 2px;
+}
diff --git a/src/docs/patterns/color/text/text.tsx b/src/docs/patterns/color/text/text.tsx
new file mode 100644
index 00000000..1d039fb5
--- /dev/null
+++ b/src/docs/patterns/color/text/text.tsx
@@ -0,0 +1,62 @@
+import { background, border, Table, text } from "../../../../core";
+import { ColorSample, ColorSampleUsage } from "../sample/sample";
+import s from "./text.module.css";
+
+type TextKey = keyof typeof text;
+
+interface Row {
+ key: TextKey;
+ usage: ColorSampleUsage;
+}
+
+// const usageTexts: Record = {
+// both: "Icon and text",
+// icon: "Icon only",
+// text: "Text only",
+// };
+
+// const Usage = (row: Row): JSX.Element => {usageTexts[row.usage]};
+
+const MakeColumn =
+ (theme: "light" | "dark", back: string) =>
+ (row: Row): JSX.Element => (
+
+
+
+ );
+
+const LightStrong = MakeColumn("light", background.strong);
+const LightWeak = MakeColumn("light", background.weak);
+const DarkStrong = MakeColumn("dark", background.strong);
+const DarkWeak = MakeColumn("dark", background.weak);
+
+interface Props {
+ rows: Row[];
+}
+
+export const ColorText = (props: Props): JSX.Element => (
+
+
+ size={Table.sizes.small}
+ fixed={{ firstColumn: true }}
+ fill
+ rows={props.rows}
+ rowKey={(row) => row.key}
+ columns={[
+ { title: "Name", className: s.name, render: "key" },
+ // { title: "Usage", className: s.usage, render: Usage },
+ { title: "Light", render: LightStrong },
+ { title: "Light (alt bg)", render: LightWeak },
+ { title: "Dark", render: DarkStrong },
+ { title: "Dark (alt bg)", render: DarkWeak },
+ ]}
+ />
+
+);