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.tsmodules 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
AsheeUIProviderconfigures 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), butdefaultThemeis replaced by whichever provider configured last, and with nothing stored underashee-themethe selection resolves again from that. A second provider that mounts after the application's (a panel, a route, a demo opened on demand) drops yourdefaultThemeback tosystem, 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
extendsnames, and a name that is not in the map at that moment inherits the library'slightinstead (packages/core/src/config/resolve-color.tsfalls back todefaults.light). That covers a typo, and it covers a theme that extends another custom theme written later in the samecolorobject: custom themes resolve in the order they are written, and onlylightanddarkare 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
onCloseused 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, noaria-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.