Theming
Sizes, variants, and global defaults.
Overview
Shim components support theming options like size and visual variants to adapt the look and feel to your design. Set global defaults using the theme provider to ensure consistency across your application.
Colors
Shim uses Radix UI Colors for its color system. Radix provides a carefully designed color palette with automatic dark mode support and semantic scales that work across different use cases.
Color scales
Each color has a 12-step scale designed for specific use cases. Shim maps these to semantic tokens you can use in your components:
- Steps 1-2: Backgrounds and subtle surfaces
- Steps 3-5: Interactive backgrounds (hover, active states)
- Steps 6-8: Borders and focus rings
- Steps 9-10: Solid colors and hover states
- Steps 11-12: Text colors with proper contrast
Semantic color tokens
Shim provides five color intents, each with a full set of use-case tokens:
neutral- Gray tones for UI elementsaccent- Primary brand color (default: Iris)success- Positive states (default: Green)warning- Cautionary states (default: Orange)danger- Destructive actions and errors (default: Red)
Each intent includes tokens like base, panel, bg, bg-hover, bg-active,
line, border, border-hover, solid, solid-hover, text,
text-contrast, and text-subtle.
Using color tokens
Reference color tokens in your Tailwind classes:
<div className="bg-neutral-panel text-neutral-text border border-neutral-line">
<p className="text-neutral-text-subtle">Subtle text</p>
<p className="text-accent-text-contrast">Contrast text</p>
<button className="bg-accent-bg hover:bg-accent-bg-hover">Action</button>
</div>Customizing accent colors
To change the accent color, edit the Radix color imports in your theme.css
file. For example, to use blue instead of iris:
/* Replace iris imports with blue */
@import "@radix-ui/colors/blue.css";
@import "@radix-ui/colors/blue-alpha.css";
@import "@radix-ui/colors/blue-dark.css";
@import "@radix-ui/colors/blue-dark-alpha.css";
/* Then update the accent aliases in @theme */
@theme {
--color-accent-1: var(--blue-a1);
--color-accent-2: var(--blue-a2);
/* ... continue for all 12 steps */
}See the Radix Colors documentation for the full color palette and detailed usage guidance.
Size
Most components support four sizes that scale proportionally.
size: 1size: 2size: 3size: 4"use client";
import { ListIcon } from "@phosphor-icons/react";
import { Button } from "@/shim-ui/button";
import { ICON_SIZE_MAP } from "@/shim-ui/lib/theme";
import { Menu, MenuItem, MenuSeparator, MenuTrigger } from "@/shim-ui/menu";
import { Slider } from "@/shim-ui/slider";
import { Switch } from "@/shim-ui/switch";
import { TextField } from "@/shim-ui/text-field";
import { ToggleButton } from "@/shim-ui/toggle-button";
import { ToggleButtonGroup } from "@/shim-ui/toggle-button-group";
import { Well } from "@/shim-ui/well";
export default () =>
([1, 2, 3, 4] as const).map((size) => (
<Well key={size} size={size}>
<code className="col-span-full mb-2 text-xs">size: {size}</code>
<div className="flex gap-[inherit]">
<Button className="flex-1">Let's go</Button>
<MenuTrigger>
<Button aria-label="Menu" isIconOnly>
<ListIcon size={ICON_SIZE_MAP[size]} />
</Button>
<Menu>
<MenuItem>Edit</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuSeparator />
<MenuItem intent="danger">Delete</MenuItem>
</Menu>
</MenuTrigger>
</div>
<ToggleButtonGroup defaultSelectedKeys={["1"]}>
<ToggleButton id="1">Left</ToggleButton>
<ToggleButton id="2">Right</ToggleButton>
</ToggleButtonGroup>
<TextField aria-label="TextField" defaultValue="Hello" />
<div className="flex gap-[inherit]">
<Slider
aria-label="Slider"
className="flex-1"
defaultValue={50}
isFilled
/>
<Switch aria-label="Switch" defaultSelected />
</div>
</Well>
));Variants
Field variants
Forms and field-based components support "classic", "soft", and "outline" variants that adjust visual styles while preserving spacing and layout.
variant: "classic"variant: "soft"variant: "outline"import { Checkbox, CheckboxGroup } from "@/shim-ui/checkbox";
import { NumberField } from "@/shim-ui/number-field";
import { Radio, RadioGroup } from "@/shim-ui/radio-group";
import { SearchField } from "@/shim-ui/search-field";
import { Slider } from "@/shim-ui/slider";
import { Switch } from "@/shim-ui/switch";
import { TextField } from "@/shim-ui/text-field";
import { Well } from "@/shim-ui/well";
export default () =>
(["classic", "soft", "outline"] as const).map((variant) => (
<Well className="grid grid-cols-4 gap-4" key={variant}>
<code className="col-span-full mb-2 text-xs">variant: "{variant}"</code>
<TextField
className="col-span-2"
defaultValue="Hello"
variant={variant}
/>
<NumberField className="col-span-2" defaultValue={0} variant={variant} />
<SearchField className="col-span-full" variant={variant} />
<Switch className="col-span-1" defaultSelected variant={variant} />
<Slider
className="col-span-3"
defaultValue={50}
isFilled
variant={variant}
/>
<RadioGroup className="col-span-2" defaultValue="1" variant={variant}>
<Radio value="1">Option 1</Radio>
<Radio value="2">Option 2</Radio>
</RadioGroup>
<CheckboxGroup
className="col-span-2"
defaultValue={["1"]}
variant={variant}
>
<Checkbox value="1">Option 1</Checkbox>
<Checkbox value="2">Option 2</Checkbox>
</CheckboxGroup>
</Well>
));Button variants
Buttons support "soft", "ghost", and "solid" variants for different visual emphasis levels. Note that ToggleButton and ToggleButtonGroup do not support the "solid" variant to avoid confusion with selected states.
variant: "soft"variant: "ghost"variant: "solid"variant: "soft" (ToggleButton)variant: "ghost" (ToggleButton)import { PushPinIcon } from "@phosphor-icons/react/dist/ssr";
import { Button } from "@/shim-ui/button";
import { ToggleButton } from "@/shim-ui/toggle-button";
import { ToggleButtonGroup } from "@/shim-ui/toggle-button-group";
import { Well } from "@/shim-ui/well";
export default () => (
<>
{(["soft", "ghost", "solid"] as const).map((variant) => (
<Well className="gap-4" key={variant}>
<code className="col-span-full mb-2 text-xs">variant: "{variant}"</code>
<Button className="col-span-full" variant={variant}>
Button
</Button>
</Well>
))}
{(["soft", "ghost"] as const).map((variant) => (
<Well className="grid grid-cols-[1fr_auto] gap-4" key={variant}>
<code className="col-span-full mb-2 text-xs">
variant: "{variant}"{" "}
<span className="font-normal font-sans">(ToggleButton)</span>
</code>
<ToggleButtonGroup defaultSelectedKeys={[1]} variant={variant}>
<ToggleButton id={1}>Left</ToggleButton>
<ToggleButton id={2}>Center</ToggleButton>
<ToggleButton id={3}>Right</ToggleButton>
</ToggleButtonGroup>
<ToggleButton variant={variant}>
<PushPinIcon size={16} />
</ToggleButton>
</Well>
))}
</>
);Label position
Form field components support positioning the label on the side or side-end of the input for more compact layouts.
labelPosition: "top"labelPosition: "side"labelPosition: "side-end""use client";
import { Select, SelectItem } from "@/shim-ui/select";
import { TextField } from "@/shim-ui/text-field";
import { Well } from "@/shim-ui/well";
export default () =>
(["top", "side", "side-end"] as const).map((labelPosition) => (
<Well className="gap-4" key={labelPosition}>
<code className="col-span-full mb-2 text-xs">
labelPosition: "{labelPosition}"
</code>
<TextField
label="Name"
labelPosition={labelPosition}
placeholder="Your name"
/>
<TextField
label="Email"
labelPosition={labelPosition}
placeholder="Your email"
type="email"
/>
<Select
items={[
{ id: "developer", name: "Developer" },
{ id: "designer", name: "Designer" },
{ id: "manager", name: "Manager" },
]}
label="Role"
labelPosition={labelPosition}
placeholder="Select your role"
>
{({ name }) => <SelectItem>{name}</SelectItem>}
</Select>
</Well>
));Set global defaults
Wrap your application (or any subtree) in the <Theme> provider to broadcast
shared props to every Shim component underneath it. The provider merges values
from the nearest ancestor, so you can layer overrides as needed.
import type { ReactNode } from "react";
import { Theme } from "@/shim-ui/lib/theme";
import "./globals.css";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<Theme
size={2}
labelPosition="side"
variants={{ field: "soft", button: "ghost" }}
>
{children}
</Theme>
</body>
</html>
);
}Inside a page or component you can scope a more specific override while inheriting the rest of the defaults:
import { Theme } from "@/shim-ui/lib/theme";
import { Button } from "@/shim-ui/button";
export function Callout() {
return (
<Theme variants={{ button: "solid" }}>
{...}
<Button>Get started</Button>
<Button>Learn more</Button>
</Theme>
);
}Theme API reference
Prop | Type | Default |
|---|---|---|
size | 1 | 2 | 3 | 4 | 1 |
labelPosition | "side" | "top" | "top" |
variants | { button?, field?, table?, badge? } | { button: "soft", field: "classic", table: "classic", badge: "surface" } |
Variant types
Category | Values | Applied to |
|---|---|---|
button | "soft" | "solid" | "ghost" | Button, ToggleButton, ToggleButtonGroup |
field | "classic" | "soft" | "outline" | TextField, Checkbox, RadioGroup, Switch, Slider, and other form inputs |
table | "classic" | "grid" | Table components |
badge | "surface" | "soft" | "solid" | Badge components |