Skip to content

Going Headless

GolemUI’s headless mode gives you full control over the look and feel of every widget. The widget engine — accessibility, focus management, validation, state, events — keeps working without any styles. You write 100% of the CSS, targeting the BEM class names every widget emits.

This page is the build-from-scratch guide. If you only need to tweak a few colors or fonts, see Customization instead — overriding CSS variables is far less work.

The form below is rendered through the headless playground app — no courtesy stylesheet is loaded; every visual is hand-written CSS targeting the BEM class names that the widgets emit.

Pick headless mode when:

  • Your design system is completely different from the default — different spacing rhythm, different typography stack, different border-radius philosophy.
  • You’re embedding GolemUI inside a host design system (Material, Bootstrap, Tailwind UI, your in-house tokens) and you don’t want the courtesy CSS competing with it.
  • You need to drop the bundle weight of @golemui/gui-components/index.css from the critical path.
  • You’re building a print or email render of a form where the courtesy layer’s interactive states aren’t relevant.

Skip headless mode when you only want to adjust the default look — switching colors, tweaking the radius, swapping the font. Override CSS variables instead. See Customization / CSS variable overrides.

  1. Headless mode is opt-out, not opt-in: simply don’t import @golemui/gui-components/index.css anywhere in your project. With no import, no widget styles are loaded — the engine still wires every interaction, but every element renders with browser-default visuals.

    @import '@golemui/gui-components/index.css';
  2. Read the widget anatomy you’ll style against
    Section titled “Read the widget anatomy you’ll style against”

    Every widget reference page ends with an Anatomy section that shows the rendered HTML — the exact markup the widget emits. Treat each anatomy block as the contract for your CSS:

    • Textinput.gui-widget, <input type="text" />, .gui-widget__hint, .gui-widget__error.
    • Password — adds .gui-password__toggle for the show/hide button.
    • Dropdown.gui-dropdown, .gui-dropdown__list, .gui-dropdown__item, item-renderer slot.
    • Toggle.gui-toggle, .gui-toggle--slider.
    • Repeater.gui-repeater, .gui-repeater__card, .gui-repeater__add-btn.
    • Flex / Grid.gui-flex__widget, .gui-grid__widget.

    The class-naming convention is BEM: .gui-<block> / .gui-<block>__<element> / .gui-<block>--<modifier>. See Customization / Class-based overrides / Naming convention for the full pattern.

  3. Decide how interactive state surfaces visually
    Section titled “Decide how interactive state surfaces visually”

    The engine sets these standard attributes on every input — your CSS reads them to express focus, validity, and disabled state:

    SelectorMeaning
    :focusThe control has keyboard focus.
    :hoverThe pointer is over the control.
    [aria-invalid='true']Validation has failed for this input.
    :disabled / [disabled]The widget is disabled (disabled: true or state).
    [aria-readonly='true']The widget is in readonly mode.
    [aria-checked='true']A toggle / checkbox is on.
    .gui-widget__errorThe error-message element rendered under invalid inputs.
    .gui-widget__hintThe hint-text element rendered under inputs that declare a hint.
  4. Start with the form-level wrapper and the layout containers (flex/grid). Every widget the engine renders is wrapped in a .gui-widget element, so a single rule covers the per-field spacing.

    @import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap');
    .gui-form {
    font-family: 'Roboto', sans-serif;
    font-optical-sizing: auto;
    font-weight: 400;
    form {
    margin: 12px;
    }
    }
    /* Vertical spacing inside flex layouts */
    .gui-flex__widget {
    display: flex;
    flex-direction: column;
    gap: 16px;
    }
    /* Inputs are inline so labels can sit beside the trigger */
    .gui-widget {
    display: flex;
    align-items: center;
    }
  5. Target the native HTML element inside .gui-widget. The same rule covers textinput, password, etc. — read [type='...'] if you need per-type tweaks.

    .gui-widget {
    input {
    width: 100%;
    &[type='text'],
    &[type='password'] {
    font-size: 16px;
    padding: 12px 16px;
    border: 2px solid #e2e8f0;
    border-radius: 8px;
    background: white;
    color: #1e293b;
    transition: border-color 0.15s ease;
    &:focus {
    border-color: #3b82f6;
    outline: none;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
    }
    }
    &[aria-invalid='true'] {
    border-color: #ef4444;
    }
    &:disabled {
    background: #f1f5f9;
    color: #94a3b8;
    cursor: not-allowed;
    }
    }
    }
  6. Some widgets ship extra controls inside .gui-widget. The password widget, for example, renders a show/hide toggle:

    .gui-widget {
    .gui-password__toggle {
    position: absolute;
    inset-inline-end: 20px;
    background: transparent;
    border: none;
    outline: none;
    cursor: pointer;
    }
    }

    The repeater renders one card per row, an add button, and a remove button per card:

    .gui-repeater__card {
    padding: 16px;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
    margin-bottom: 12px;
    }
    .gui-repeater__add-btn,
    .gui-repeater__remove-btn {
    padding: 8px 16px;
    border-radius: 6px;
    }

    Walk each widget reference page and copy its anatomy into your stylesheet as a starting point.

  7. Two classes handle the inline messages every input renders:

    .gui-widget__hint {
    font-size: 14px;
    color: #64748b;
    margin-top: 4px;
    }
    .gui-widget__error {
    font-size: 14px;
    color: #ef4444;
    margin-top: 4px;
    }

    Combine [aria-invalid='true'] on the input with .gui-widget__error for a coherent invalid-state look.

  8. Buttons render as <button class="gui-button"> plus modifier classes for variants and icon position:

    .gui-button {
    padding: 10px 20px;
    border-radius: 6px;
    border: none;
    background: #3b82f6;
    color: white;
    font-weight: 500;
    cursor: pointer;
    &:hover {
    background: #2563eb;
    }
    &:disabled {
    background: #cbd5e1;
    cursor: not-allowed;
    }
    }

    Any button you add via gui.actions.button (whether actionType: 'submit' or a custom onClick) renders with the same .gui-button class — one rule covers all of them.

  9. Container queries are baked into the widget HTML; you only need to write the CSS that responds to them. Use the @container query syntax to react to the form’s container width:

    @container (max-width: 480px) {
    .gui-flex__widget {
    gap: 8px;
    }
    .gui-widget input {
    font-size: 14px;
    }
    }

    For RTL, prefer logical properties (inset-inline-end, padding-inline-start, margin-inline-end) rather than left / right. The engine flips text direction automatically when the active locale is RTL — your CSS just needs to flow with it.

Even in headless mode you can opt back into GolemUI’s design tokens — just define the same --gui-* CSS custom property names on :root:

:root {
--gui-color-primary-500: #3b82f6;
--gui-radius-md: 8px;
--gui-space-3: 12px;
/* …whichever tokens you want to reuse… */
}

Now your hand-written rules can read var(--gui-color-primary-500) instead of hard-coded values, and you keep the door open for mixing in parts of the courtesy layer later if you change your mind. See Customization / Design tokens reference for the full token list.

Every widget reference page (e.g. Textinput, Dropdown) ends with an Anatomy section that shows the rendered HTML. Use these as the contract you style against.

  • Styling Overview — headless vs courtesy layer, dark mode, responsive scaling, RTL.
  • Customization — CSS variables, class-based overrides, design tokens, icon customization.
  • Theming — built-in themes (Clay), creating named themes, scoped themes.
  • Widgets Reference — per-widget HTML anatomy.