Skip to contentAsheeUIAsheeUI

Troubleshooting

The five failures this documentation set has actually produced, what each looks like, why it happens, and the fix.

These are five failures this codebase has produced, in the order they usually bite: one on the way in, two about configuration, and two about a component that looked as though it should have worked. Each entry is the symptom in your words, the cause, and the fix. Anything not here is a component page away: Components is the index.

Everything is unstyled, or a component is missing its padding

What you see. Two shapes of one failure. Either the whole page is unstyled, no framework colours, no spacing, no borders, or almost everything is right and one control is not: a Button whose label touches its edges, an Alert with no padding, a card with its content against the border. In the second shape the class is on the element and the rule is not in the stylesheet. Inspect the button, read px-3.5 off its class list, and search the generated CSS for .px-3\.5: nothing.

Why. Two independent halves of one import.

  • The stylesheet has to come after the Tailwind import: it extends the Tailwind theme with the framework's tokens (--color-background: var(--ashee-background) and the rest) and declares the layer order the two files are read in, so an import above Tailwind's own is the first thing to check when the page has no framework colours at all.
  • Some of the class vocabulary is not in @asheeui/web. The radius and size vocabularies and most of the components' *-styles.ts modules are in @asheeui/core, and the stylesheet tells Tailwind to scan both packages with four patterns:
@source "./**/*.{ts,tsx}";
@source "./**/*.js";
@source "../../core/src/**/*.{ts,tsx}";
@source "../../core/dist/**/*.js";

A build that scans the application and @asheeui/web but never @asheeui/core generates the classes it can see and silently omits the rest. That is why the class is on the element and the rule is missing: the padding of a default button is stated in packages/core/src/components/forms/button/button-styles.ts, not in the renderer.

What to do. Import the package's stylesheet rather than pasting its tokens into your own CSS, after the Tailwind import:

@import "tailwindcss";
@import "@asheeui/web/styles";

If your build states its own content sources, a source(none) import, or an explicit list, then the last two patterns above are yours to restate against your node_modules: the package's lines are followed because the package's stylesheet is the file Tailwind reads, and a copy of the tokens loses them. Rebuild and search the generated CSS for the class you saw on the element; if it is still absent, the source list is still the answer. Installation states the import order once.

My custom theme falls back to light

What you see. color declares the theme, defaultTheme names it, and the page paints the built-in light palette. One line in the console separates the two faults: document.documentElement.className. If it says theme-light or theme-dark, the theme was never selected. If it says theme-midnight, the selection worked and the palette did not.

Why.

  • The controller is one instance for the whole application, and every AsheeUIProvider configures it. The theme list only ever grows, a provider below the application's can add a theme but never take one away (packages/web/src/theme/controller.ts), but defaultTheme is replaced by whichever provider configured last, and with nothing stored under ashee-theme the selection resolves again from that. A second provider that mounts after the application's (a panel, a route, a demo opened on demand) drops your defaultTheme back to system, which is light on a light machine. A nested provider in the same commit is harmless: React runs a child's effects first, so the application's provider has the last word.
  • With the right class and the wrong colours, the palette is the fault. A custom theme inherits the palette its extends names, and a name that is not in the map at that moment inherits the library's light instead (packages/core/src/config/resolve-color.ts falls back to defaults.light). That covers a typo, and it covers a theme that extends another custom theme written later in the same color object: custom themes resolve in the order they are written, and only light and dark are merged before them.

What to do. Keep one provider, at the root. If a second one is unavoidable, give it the same color object and an explicit defaultTheme. For the palette, declare extends as "light" or "dark", or write the base theme before the theme that extends it. A theme chosen with useTheme().setTheme is stored under ashee-theme and wins over defaultTheme on later visits, so a fault that disappears after picking the theme by hand is the first cause above. Theming has the mechanism and the API.

Configuration has no effect on one component

What you see. defaultRadius: "lg" rounds most of the product and one component ignores it: every Alert stays at md however the application is configured. Setting components.alert.radius works, which is the clue.

Why. The cascade has four tiers, and the first one that answers wins: instance props, then components.<name>, then the global default, then the built-in fallback. A component that states an axis in its registration has already answered the second tier, for every instance, before the application says anything. packages/web/src/components/feedback/alert/alert-config.ts registers radius: "md", so components.alert.radius is populated and config.defaultRadius is never consulted. packages/web/src/components/forms/button/button-config.ts is the other case: it registers size, animate and fullWidth and deliberately leaves variant, color and radius out, so the application's defaultVariant, defaultColor and defaultRadius reach every button. The registration is the component's design decision, an alert is md, not a fault in the merge: packages/core/src/config/resolve-config.ts merges that registry as the base of the components tier.

What to do. State the axis for that component:

<AsheeUIProvider config={{ components: { alert: { radius: "lg" } } }}>
  <App />
</AsheeUIProvider>

The global key is the floor for the components that leave an axis unregistered; the per-component key is the lever for one that pins it. To tell which you are looking at, open the component's config module and read the object passed to registerComponentDefaults: a key in that object will not be reached by the global default. Configuration is the cascade in full.

A field never opens the on-screen keyboard

What you see. You focus or tap a field and nothing appears at the foot of the window. There is no warning and no error, the field types with the real keyboard, exactly as it would without the framework, and the on-screen keyboard never shows up anywhere in the page.

Why. enableVirtualKeyboard defaults to true, and on its own that means nothing: the field asks a context for the keyboard, and with no KeyboardProvider above it there is no context to ask. packages/web/src/components/system/keyboard/KeyboardProvider.tsx owns the open state and renders the keyboard, and packages/web/src/components/system/keyboard/use-keyboard-field.ts returns early when that context is absent, which is why the failure is silent rather than reported. A keyboard rendered in a portal at the end of the document is easy to look for in the wrong place, too: it will not appear inside your form's markup.

What to do. Mount a provider above the fields, inside AsheeUIProvider because it reads the application's configuration:

import { Input, KeyboardProvider } from "@asheeui/web";

<KeyboardProvider>
  <Input label="Recipient" />
</KeyboardProvider>;

One provider around a form, a screen or the whole application is enough; a page opts in once and switches individual fields out with enableVirtualKeyboard={false}. A nested provider owns the keyboard for its own subtree, which is how a group of fields gets a different set of keys. For a field that is not one of the framework's, useKeyboardField returns hasProvider, the difference between a field that could open a keyboard and one that cannot, so the affordance can be drawn only where it works. Keyboard covers the layouts, the keys and the configuration.

An overlay cannot be dismissed, or a page stops scrolling

What you see. A dialog is open, pressing Tab cycles focus round inside it, the page behind will not scroll, and neither Escape, nor a press on the dimmed backdrop, nor the close control in the corner appears to do anything, the only way out is a reload, or whatever else in the application sets the state that opened it.

Why. The component applied its scroll lock and its focus trap, so as far as it is concerned the layer can close: it was given an onClose. What that callback does is the other half, and the component cannot check it. If it closes a different piece of state, or does nothing while isOpen stays true, every dismissal gesture succeeds and the page does not move, the lock follows isOpen, not the gesture (packages/web/src/libs/use-body-scroll-lock.ts), and focus stays trapped for the same reason.

Two failures that used to look like this one are fixed, so rule them out before looking above:

  • A layer mounted with no onClose used to trap the page: the lock and the trap were applied anyway and no gesture was wired. It is now presented non-modally, no scroll lock, no focus trap, no aria-modal, and logs a warning that names the missing prop, so that case announces itself in the console instead.
  • One press of Escape used to close every open layer, because each layer listened on the window and none asked which was on top. Escape is now answered by the topmost layer only, through a shared stack (packages/web/src/libs/layer-stack.ts), so a modal opened over a drawer closes alone.

What to do. Read onClose: it has to set isOpen to false, or dispatch the state change that does. When a layer must not be dismissed mid-request, keep onClose and let that callback refuse while the request is pending rather than dropping the prop; turn both switches off so the only way out is the control you choose, the built-in close control, or a cancel button in the panel. If the page cannot scroll after everything looks closed, find the <Modal isOpen> or <Drawer isOpen> still mounted: the lock follows isOpen, not dismissal. Focus and dismissal is the contract these two layers share, and Modal states what the modal adds to it.