---
title: "Styling & Animation"
description: "How widget styling works in the GlassHome SDK — per-widget CSS bundles, Tailwind utilities, your own CSS files, theme variables, container queries, and animation."
canonical: https://glasshome.app/docs/widget-styling
section: "Widget Development"
updated: 2026-06-13
---
# Styling & Animation

Each widget renders inside its own **closed shadow root** in the dashboard. That root is cut off from the dashboard stylesheet, so every widget carries its own complete CSS. The build produces one `<name>.css` next to `<name>.js`, and the host adopts it into the widget's shadow root at mount.

The isolation runs both ways: your styles never leak into the dashboard or another widget, and the dashboard's styles never leak into you. Everything your widget renders has to be styled by CSS the build put in your bundle.

  ### Tailwind, built in

The full Tailwind v4 utility set plus tw-animate-css. Only the classes you actually use ship in your bundle.
  ### Your own CSS

Import a .css file from your widget and it gets bundled into the widget stylesheet, scoped to your shadow root.
  ### Theme follows the home

The user

## What's in the bundle

When you build, the SDK runs Tailwind over exactly three sources and writes the result to your widget's stylesheet:

1. **Your widget's own files** (the directory of your `index.tsx`) — so the utility classes in your JSX are included.
2. **`@glasshome/ui`** — the theme and component styles every GlassHome widget ships with. Keep it as a dependency.
3. **The SDK shell styles** — the `.glasshome-widget-*` classes and CSS variables the `Widget` components use.

Tailwind is run with `source(none)`, which disables the automatic project-root scan. The practical effect: each widget gets **only the utilities it uses**, not the superset of every widget in your repo. Classes have to appear literally in your source for Tailwind to see them, so build class names statically rather than concatenating them at runtime.

## Tailwind utilities

Use Tailwind exactly as you would anywhere else:

```tsx
<Widget.Content class="flex items-center gap-3 rounded-lg bg-card/40 p-4">
  <span class="font-semibold text-lg text-foreground">22°C</span>
</Widget.Content>
```

`tw-animate-css` is bundled too, so its animation utilities (`animate-in`, `fade-in`, `slide-in-from-bottom`, …) are available without extra setup.

### Dark mode

`dark:` variants work. The host mirrors the dashboard's `dark` class onto your shadow host element, and the SDK redefines the `dark` variant to be shadow-aware, so:

```tsx
<div class="bg-white text-black dark:bg-zinc-900 dark:text-white" />
```

reacts to the dashboard theme. For logic that needs the boolean, use `isDark()` from the SDK.

## Adding your own CSS

Import a stylesheet anywhere in your widget's module graph and it is bundled into the same `<name>.css`, scoped to your shadow root:

```tsx
// src/my-widget/index.tsx
import "./styles.css";
```

```css
/* src/my-widget/styles.css */
.spark {
  background: radial-gradient(circle, var(--widget-color), transparent);
}

@keyframes drift {
  from { transform: translateX(0); }
  to   { transform: translateX(8px); }
}
```

Plain CSS, nesting, custom properties, and `@keyframes` all work. Because the stylesheet lives in your shadow root, your selectors only match your own DOM, so you don't need to namespace class names defensively.

## Theme variables

The user's theme reaches your widget by **inheritance**: variables like `--background`, `--foreground`, `--card`, `--primary`, `--border`, `--muted`, `--accent`, and `--radius` are defined on the dashboard document and inherit across the shadow boundary into your root. That is how a widget follows the household theme and live theme changes.

Use them, through Tailwind (`bg-card`, `text-foreground`, `rounded-[var(--radius)]`) or directly (`color: var(--foreground)`).

> **Never redefine theme variables on :host:** Defining a theme variable on <code>:host</code> in your widget CSS freezes it at build-time and stops the value inheriting, so your widget stops reacting to the theme. The build <strong>fails</strong> if it finds one of the theme variables above defined under <code>:host</code>. Set your own (`--widget-*` or custom) variables freely; just leave the theme ones to inherit.

## Responsive sizing with container queries

There are **no size tiers and no JS measurement** for visual scale. The widget shell is a CSS size container (`container-type: size; container-name: widget`), and the SDK's own padding, gap, icon, and text sizes are fluid functions of that container. Two widgets at the same rendered box look identical, and resizing is smooth.

Scale your own content the same way. Tailwind's container-query variants resolve against the `widget` container:

```tsx
<span class="text-3xl @[150px]:text-4xl @[300px]:text-6xl">
  {time()}
</span>
```

Or write the query by hand in your CSS:

```css
@container widget (min-aspect-ratio: 1) and (max-height: 149px) {
  .my-layout { flex-direction: row; }
}
```

When you need to branch *rendered content* (not just size) — for example, hiding a forecast strip on a small chip — read the measured box from the context and apply your own pixel thresholds:

```tsx
const ctx = useWidgetContext();
const compact = () => ctx.dimensions().width < 200;
```

## Coloring the shell

`<Widget>` draws a gradient shell from a single accent color. Set it with props:

| Prop | Effect |
|---|---|
| `tone` | Semantic color: `"success" \| "warning" \| "danger" \| "info" \| "neutral" \| "accent"`. Resolves to `--widget-color`. |
| `color` | CSS color override for `--widget-color`. Overrides `tone`. |
| `colorTo` | Second gradient stop (`--widget-color-to`). |
| `gradient` | A full CSS gradient string; replaces the auto-generated shell. |

```tsx
<Widget tone={isOn() ? "accent" : "neutral"}>…</Widget>
<Widget color="oklch(0.7 0.18 250)" colorTo="oklch(0.6 0.2 280)">…</Widget>
```

### SDK CSS custom properties

These are defined by the SDK on the widget shell and are yours to read (and, except where noted, to override) in your own CSS:

| Variable | Meaning |
|---|---|
| `--widget-color` | The shell's accent color (driven by `tone`/`color`). |
| `--widget-color-to` | Second gradient stop. |
| `--widget-icon-color` | Per-icon / per-fill accent (set by `Widget.Icon color` / `SliderFill color`). |
| `--widget-glow-strength` | Glow intensity multiplier for icons and fills. |
| `--widget-fill-value` | Slider fill level, `0`–`100`. |
| `--tone-{name}` | The resolved color for each semantic tone. |

The fluid scale tokens (`--widget-pad`, `--widget-gap`, `--widget-icon-box`, `--widget-title-size`, `--widget-value-size`, …) are also on the shell; override them on `.glasshome-widget` if you want a denser or looser look than the default.

### SDK shell classes

The `Widget` slot components render with these classes. Target them when you want to restyle a slot rather than replace it:

| Class | Slot |
|---|---|
| `.glasshome-widget` | The shell (gradient, border, container context) |
| `.glasshome-widget-content` | `Widget.Content` layout wrapper |
| `.glasshome-widget-icon` | `Widget.Icon` tile |
| `.glasshome-widget-title` | `Widget.Title` |
| `.glasshome-widget-status` | `Widget.Status` |
| `.glasshome-widget-value` | `Widget.Value` |
| `.glasshome-widget-badge` | Title badge |

If you ever need the SDK tokens in a root the host didn't set up (a custom mount, a test), call `injectTokens(root)` to attach them.

## Animation

Standard CSS and Tailwind animation work inside the shadow root. A few SDK pieces help you keep motion correct and cheap:

- **`tw-animate-css` utilities** for one-shot enter/exit animations (`animate-in fade-in`, etc.).
- **`loading` prop** on `<Widget>` renders a pulsing overlay while data is pending: `<Widget loading={!hasEntities()}>`.
- **`Widget.SliderFill`** animates a 0–100 fill (brightness, volume, …). Pass `isDragging` to drop the transition while the user drags so the fill tracks the finger 1:1.

```tsx
<Widget.SliderFill value={brightnessPercent()} isDragging={dragging()} />
```

### Respect reduced motion

`useReducedMotion()` is a reactive accessor over the user's `prefers-reduced-motion` setting. Gate non-essential animation on it:

```tsx
const reduced = useReducedMotion();
<div classList={{ "animate-pulse": !reduced() }} />
```

### Pause off-screen animation

`useIntersectionPause(el)` returns `true` while the element is outside the viewport, so you can stop animating widgets the user can't see:

```tsx
let ref: HTMLDivElement | undefined;
const paused = useIntersectionPause(() => ref);

createEffect(() => {
  if (paused()) stopTicker();
  else startTicker();
});
```

## Charts

For SVG charts, `svgColors` gives energy-domain colors ready to drop into `fill`/`stroke`. Each key (`solar`, `grid`, `battery`, `ev`, `home`, `positive`, `negative`) exposes `solid`, `stroke`, and a reduced-opacity `fill` for area shading:

```tsx
import { svgColors } from "@glasshome/widget-sdk";

<path d={areaPath()} fill={svgColors.solar.fill} stroke={svgColors.solar.stroke} />
```

For drawing smooth lines, `monotoneCubicPath(points)` returns an SVG path string from a list of points.

## See also

  ### [Widget SDK reference](/docs/widget-sdk)

Components, hooks, props, and the data layer.
  ### [Widget Development](/docs/widget-development)

Scaffold, develop, and connect to a running dashboard.