HextaUI

Appearance

The Appearance section of an AI product's settings: theme, accent, background tint, density, chat font, text sizes and accessibility options, all held as a draft until people save.

Appearance groups everything about how the app looks into one section. Theme cards show a small window in each theme, drawn in the accent you've picked, so the choice is clear before you save.

System follows the device and says which one it is right now. High contrast strengthens edges and text and keeps backgrounds neutral, so the tint slider turns off while it's picked.

The accent hue is a real color track with eight quick picks under it, and the readout names the color, like Blue 250°. Background tint mixes that accent into the surfaces. Density and chat font cards show a sample of each option, and the two text sizes are steppers with limits.

Reduce motion and Reduce transparency say when the device already asks for either.

Everything is a draft until it's saved. The save bar from Settings rises as soon as something differs, Discard brings back the saved look, and Reset puts every option back to its default as a draft you can still discard.

  1. Add the Pro registry to components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Add your token

    Create a token on your account page and put it in .env.local as HEXTAUI_PRO_TOKEN.

  3. Add the block

    pnpm dlx shadcn@latest add @hextaui-pro/appearance

Save to your API

Pass the saved values and an async onSave. Nothing is sent until someone presses Save in the bar. Throw to show your message in the bar and keep their picks.

"use client"

import { IconPalette } from "@tabler/icons-react"

import { SettingsSection, SettingsShell } from "../settings/settings"
import { AppearanceSettings, type AppearanceValues } from "@/components/blocks/appearance/appearance"

export function AppearancePage({ appearance }: { appearance: AppearanceValues }) {
  return (
    <SettingsShell
      sections={[{ id: "appearance", label: "Appearance", icon: <IconPalette /> }]}
      className="h-svh"
    >
      <SettingsSection id="appearance">
        <AppearanceSettings
          values={appearance}
          onSave={async (values) => {
            const response = await fetch("/api/appearance", {
              method: "PUT",
              body: JSON.stringify(values),
            })
            if (!response.ok) throw new Error("Check your connection and try again.")
          }}
        />
      </SettingsSection>
    </SettingsShell>
  )
}

Apply the saved look

Once saved, apply the values to your app however it themes itself. normalizeAppearance repairs anything stored by an older version, so a bad value never breaks the page.

"use client"

import * as React from "react"

import { AppearanceSettings, normalizeAppearance, type AppearanceValues } from "@/components/blocks/appearance/appearance"

function applyAppearance(values: AppearanceValues) {
  const root = document.documentElement
  const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches
  const dark = values.theme === "dark" || values.theme === "contrast" || (values.theme === "system" && systemDark)
  root.classList.toggle("dark", dark)
  root.dataset.contrast = values.theme === "contrast" ? "more" : "normal"
  root.dataset.density = values.density
  root.dataset.chatFont = values.chatFont
  root.dataset.motion = values.reduceMotion ? "reduce" : "full"
  root.dataset.transparency = values.reduceTransparency ? "reduce" : "full"
  root.style.setProperty("--accent-hue", String(values.hue))
  root.style.setProperty("--accent-tint", `${values.intensity}%`)
  root.style.setProperty("--text-interface", `${values.interfaceSize}px`)
  root.style.setProperty("--text-code", `${values.codeSize}px`)
}

export function AppearanceForm({ stored }: { stored: Partial<AppearanceValues> | null }) {
  const [saved, setSaved] = React.useState(() => normalizeAppearance(stored))

  React.useEffect(() => applyAppearance(saved), [saved])

  return (
    <AppearanceSettings
      values={saved}
      onSave={async (values) => {
        const response = await fetch("/api/appearance", {
          method: "PUT",
          body: JSON.stringify(values),
        })
        if (!response.ok) throw new Error("Check your connection and try again.")
        setSaved(values)
      }}
    />
  )
}

Anatomy

The parts you compose, from the outside in.

PartDescription
AppearanceSettingsThe section's content: the groups of options, wired to the shell's save bar.
normalizeAppearanceTurns stored or partial values into valid ones: clamps sizes, wraps the hue and falls back for unknown names.
appearanceDefaultsThe values Reset goes back to when you don't pass your own.

AppearanceSettings

Use inside a SettingsSection. Also accepts every div prop.

PropTypeDefault
valuesWhat's saved now: { theme, hue, intensity, density, chatFont, interfaceSize, codeSize, reduceMotion, reduceTransparency }. Invalid values are repaired, and the repair doesn't count as an unsaved change.
AppearanceValues–
onSaveSave the draft. Throw to show the message in the save bar and keep the draft.
(values) => void | errors | Promise<void | errors>–
defaultsWhat Reset puts back.
AppearanceValuesappearanceDefaults
PropTypeDefault
themeSystem follows prefers-color-scheme.
"system" | "light" | "dark" | "contrast""system"
hueThe accent's OKLCH hue, 0 to 359.
number250
intensityHow much of the accent tints backgrounds, 0 to 100.
number30
densitySpace between and inside messages.
"compact" | "comfortable""comfortable"
chatFontThe font for messages.
"sans" | "serif" | "mono""sans"
interfaceSizeText size in pixels, 12 to 18.
number14
codeSizeCode size in pixels, 11 to 20.
number13
reduceMotionStop spinners and sliding panels.
booleanfalse
reduceTransparencySolid backgrounds instead of blurred ones.
booleanfalse
KeyAction
←→In the theme, density or font cards, pick the previous or next card. On the hue track, move one degree.
Shift→On the hue track, move 15 degrees. Page Up and Page Down do the same.
HomeEndOn the hue track, jump to either end.
↑↓In a text size field, change the size by one pixel.
⌘SSave while something is unsaved. Ctrl+S on Windows and Linux.
  • Each card group is a radio group named by its row, and each card is named by its own label, so screen readers hear "Dark, radio, 3 of 4".
  • The hue track reads its color by name, like "Blue, 250 degrees", and the quick picks are toggle buttons named by color.
  • Reset keeps focus after it runs and announces that the defaults are back and need saving.

Code

2 files, added to components/blocks/appearance.