Skip to content

feat(theme): introduce theme system for autocomplete widget#75

Merged
sarahdayan merged 15 commits intomainfrom
feat/theme-system
Mar 11, 2026
Merged

feat(theme): introduce theme system for autocomplete widget#75
sarahdayan merged 15 commits intomainfrom
feat/theme-system

Conversation

@sarahdayan
Copy link
Member

@sarahdayan sarahdayan commented Mar 9, 2026

Summary

This PR introduces @experiences/theme, a new package that makes a Zod schema the single source of truth for all themeable CSS variables. The schema powers validation at the API boundary, AI tool definitions (via JSON Schema), and CSS generation from one place.

This PR focuses on the autocomplete widget.

See the Themeable CSS + Zod Schema workstream for full context.

What changes

New packages/theme package

A standalone package consumed by the runtime and toolbar.

  • Variable catalog: Every themeable property for the autocomplete widget, declared with its key, type, default value (per light/dark mode), constraints, and description. ~80 variables organized by domain: colors, typography, shape & layout, and autocomplete-specific sub-domains (input, dropdown, results, section headers).
  • CSS stylesheet: The autocomplete widget styles in the theme package so it owns both the variable definitions and the CSS that consumes them.
  • Type system: ThemeVariable, ThemeVariableType, ShadowLayer, ThemeOverrides, and ThemeOverrideValue types. Shadows use a structured ShadowLayer[] type instead of freeform CSS strings, each layer is an object with constrained fields.
  • Schema generation: createThemeOverridesSchema(variables) turns the catalog into a Zod schema enforcing types, constraints, and light/dark mode structure. .toJsonSchema() produces the AI tool definition so agents know exactly what to produce.
  • CSS generation: generateThemeCss(variables, overrides) merges partial overrides with catalog defaults and outputs :root + dark mode CSS blocks. Numbers get their unit appended, shadow layers serialize to box-shadow syntax. Every catalog variable is always present, no missing custom properties at runtime.

Runtime integration

The middleware now uses generateThemeCss() from the theme package instead of building CSS strings ad-hoc. This ensures themes always include defaults for missing variables and support light/dark mode.

Example themes and theme switcher

Five themed override sets in all examples demonstrate the system across different aesthetics. A theme switcher UI lets you preview them in both demo apps.

Design decisions

  • step is a UI hint, not validation: Slider step values (e.g., step: 0.1) are not enforced via multipleOf in the schema, because IEEE 754 floating point makes fractional multiples unreliable (0.15 fails multipleOf(0.1)). Min/max still constrain the range.
  • Shadows are structured, not freeform: Raw CSS strings like 0 0 0 1px rgba(23,23,23,0.05) are hard for humans and agents to produce and difficult to validate. ShadowLayer[] lets the schema validate each field individually while the system handles serialization.
  • Theme package lives in Experiences, not InstantSearch: Avoids creating a new public API surface in InstantSearch. We can iterate on the schema freely and migrate configurations when it evolves.

Exit criteria (workstream)

Zod schema defines all current theme variables with types, defaults, and constraints

~80 autocomplete variables declared with types, per-mode defaults, min/max constraints, units, and descriptions. Every variable type has typed validation.

Schema can generate a valid CSS stylesheet from a partial overrides object

generateThemeCss(variables, overrides) merges partial overrides with catalog defaults and outputs :root + dark mode CSS blocks with --ais-* custom properties.

Schema can produce an AI tool definition (JSON Schema or equivalent)

createThemeOverridesSchema(variables).toJsonSchema() produces a JSON Schema with per-variable types, constraints, format hints, defaults in descriptions, and light/dark mode structure.

Runtime applies generated CSS correctly

The middleware injects CSS generated by the theme package. Example themes demonstrate visual correctness across five aesthetics.

@netlify
Copy link

netlify bot commented Mar 9, 2026

Deploy Preview for algolia-experiences-js ready!

Name Link
🔨 Latest commit 5bd64f1
🔍 Latest deploy log https://app.netlify.com/projects/algolia-experiences-js/deploys/69b11f42adac6000086da286
😎 Deploy Preview https://deploy-preview-75--algolia-experiences-js.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Mar 9, 2026

Deploy Preview for algolia-experiences-react ready!

Name Link
🔨 Latest commit 5bd64f1
🔍 Latest deploy log https://app.netlify.com/projects/algolia-experiences-react/deploys/69b11f42f8f53d000843324c
😎 Deploy Preview https://deploy-preview-75--algolia-experiences-react.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

sarahdayan and others added 11 commits March 10, 2026 12:00
- Add `@experiences/theme` package with `ThemeVariable` types and `generateThemeCss()` for generating light/dark CSS custom property blocks
- Integrate theme CSS generation into runtime middleware, replacing ad-hoc cssVariables injection
- Replace bundled satellite.css + upstream autocomplete.css with a local autocomplete.css, dropping the unused satellite theme
- Add `ThemeSwitcher` component to the React example for dev/demo theming
- Add `examples/shared/` with reusable theme definitions across JS and React examples
- Improve dev setup: local proxy serves dist files during development, file watching triggers HMR, `useAlgoliaExperiences` supports relative URLs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add createThemeOverridesSchema() that builds a Zod validation schema
from the theme variable catalog. Number variables get min/max/step
constraints, all fields include descriptions with units and defaults
for AI agent consumption. ThemeOverrides now accepts string | number
values with coercion at the CSS generation boundary.

Reorganize packages/theme into types.ts, lib/, and widgets/ for
clearer separation of concerns. Move autocomplete.css into
packages/runtime/src/styles/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Test createThemeOverridesSchema validation (type checking, numeric
constraints, empty/partial objects), JSON Schema output (property keys,
types, descriptions, constraints), and generateThemeCss (light/dark
blocks, defaults, unit appending, unitless numbers, overrides,
per-mode overrides, unknown keys ignored).

Also add toJsonSchema() method and zod-to-json-schema dependency for
AI tool definition generation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The stylesheet is a product of the theme variable catalog, not the
runtime. Moving it to packages/theme/src/widgets/ co-locates it with
the variable definitions it consumes. The runtime build config now
reads the CSS from the theme package.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add "Format: R, G, B." to color variable descriptions so agents know
to return RGB triplets. Add min/max constraints (0–1) to all
alpha and opacity variables to prevent out-of-range values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap the overrides schema in `{ light, dark }` so agents return per-mode
values in a single tool call. Add constraints to `base-unit` and change
`font-weight-medium` from text to number with valid CSS weight range.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert 7 variables from unvalidated text to number type with min/max/step
constraints: font weights, header font size, item line height, transition
duration, detached modal top offset, and scrollbar color mix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace freeform text shadow variables with a constrained `shadow` type
that stores an array of structured layer objects (offsetX, offsetY, blur,
spread, color, opacity). This makes shadows validatable by the schema and
easier for AI agents to produce correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lations

The `step` constraint is a UI hint for slider increments, not a
validation rule. IEEE 754 rounding causes values like 0.15 to fail
`multipleOf(0.1)`, rejecting valid agent output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rrideValue

Move isShadowLayers to its own predicates/ folder, strengthen the check
to require both offsetX and blur, and add unit tests. Export
ThemeOverrideValue from types.ts to remove the duplicate declaration in
generate-theme-css.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move test files from a top-level __tests__/ directory to colocated
__tests__/ folders within each source module (lib/, predicates/).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sarahdayan and others added 3 commits March 10, 2026 12:08
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sarahdayan sarahdayan marked this pull request as ready for review March 10, 2026 14:53
@sarahdayan sarahdayan requested a review from dhayab as a code owner March 10, 2026 14:53
Comment on lines -48 to +46
style.textContent =
__CHAT_CSS__ + __AUTOCOMPLETE_CSS__ + __SATELLITE_CSS__ + SKELETON_CSS;
style.textContent = __AUTOCOMPLETE_CSS__ + SKELETON_CSS;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a change you forgot to undo? Chat is now unstyled for example.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it on purpose because the chat's CSS embarks a lot more styles than just the chat widget, it also comes with other InstantSearch styles including Autocomplete that were clashing with this new theme.

Since we're focusing on Autocomplete right now, we'll likely remove support for chat and other widgets for now, so I think it's okay to drop. When we introduce chat, we'll want to modularize its theming like we're doing with AC right now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok that's fine, we'll remove other widgets in another PR and reintroduce them progressively then.

@sarahdayan sarahdayan requested a review from dhayab March 11, 2026 15:11
@sarahdayan sarahdayan merged commit 9962c8a into main Mar 11, 2026
8 checks passed
@sarahdayan sarahdayan deleted the feat/theme-system branch March 11, 2026 15:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants