Shim

Theming

Colors, sizes, variants and other theming options.

Overview

Shim makes it easy to set the overall look and feel of your app: semantic colors, preferred density, variants for key components, and flexible label positions keep things consistent while staying easy to tweak. Apply size, variant, and label choices per component or set defaults globally with the Theme provider.


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.

Shim provides five color intents, each with a full set of use-case tokens:

  • neutral - Gray tones for UI elements
  • accent - Primary brand color (default: Iris)
  • success - Positive states (default: Green)
  • warning - Cautionary states (default: Orange)
  • danger - Destructive actions and errors (default: Red)

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 colors

To change the colors, edit the Radix color imports and aliases in your theme.css file. For example, to change the accent color to blue:

styles/theme.css
/* Import blue scales */
@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 (1, 2, 3, 4) via the size prop that scale proportionally. Size can be set on individual components, on container components like Form, Well, or Toolbar to affect their children, or via the Theme provider to set a default for all components.

1
2
3
4
Size example
"use client";
import { ListIcon } from "@phosphor-icons/react";
import { Badge } from "@/shim-ui/badge";
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 { Select, SelectItem } from "@/shim-ui/select";
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}>
      <Badge className="aspect-square">{size}</Badge>
      <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={["grid"]}>
        <ToggleButton id="grid">Grid</ToggleButton>
        <ToggleButton id="list">List</ToggleButton>
      </ToggleButtonGroup>
      <Select defaultValue={"grid"}>
        <SelectItem id="grid">Grid</SelectItem>
        <SelectItem id="list">List</SelectItem>
      </Select>
      <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.

classic
soft
outline
Field variants example
import { Badge } from "@/shim-ui/badge";
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}>
      <Badge className="col-span-full">{variant}</Badge>

      <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 uses the same visual appearance for soft and solid variants to avoid confusion with the selected state.

soft
solid
ghost
Button variants example
import { PushPinIcon } from "@phosphor-icons/react/dist/ssr";
import { Badge } from "@/shim-ui/badge";
import { Button } from "@/shim-ui/button";
import { ToggleButton } from "@/shim-ui/toggle-button";
import { Well } from "@/shim-ui/well";

export default () => (
  <>
    {(["soft", "solid", "ghost"] as const).map((variant) => (
      <Well className="grid grid-cols-[1fr_auto]" key={variant}>
        <Badge className="col-span-full">{variant}</Badge>
        <Button intent="accent" variant={variant}>
          Let's go
        </Button>
        <ToggleButton aria-label="Pin" intent="accent" 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.

top
Role
side
Role
side-end
Role
Label position example
"use client";
import { Badge } from "@/shim-ui/badge";
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}>
      <Badge>{labelPosition}</Badge>

      <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>
  ));

Theme provider

Use the <Theme> provider to set default theming props for all components in a subtree. The provider merges values from the nearest ancestor, allowing layered overrides as needed.

layout.tsx
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:

components/callout.tsx
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>
  );
}
size1 | 2 | 3 | 41
labelPosition"top" | "side" | "side-end""top"
variants.button"soft" | "solid" | "ghost""soft"
variants.field"classic" | "soft" | "outline""classic"
variants.table"surface" | "ghost" | "zebra""classic"
variants.badge"surface" | "soft" | "solid""surface"
variants.well"classic" | "soft" | "surface" | "outline""classic"