Customizing Components

Override styles, wrap components with app defaults, and theme your app.

Override styles per-instance

The simplest customization is css_classes: on a single usage:

<%= kui(:button, css_classes: "w-full") do %>
  Full-width button
<% end %>

tailwind_merge ensures your classes win over the component defaults without duplication. This is the equivalent of passing className in React or :class in Vue.

Wrap components with your defaults

If you find yourself repeating the same props, create a thin wrapper partial in your app:

<% # app/views/shared/_primary_button.html.erb %>
<% # locals: (css_classes: "", **rest) %>
<%= kui(:button, variant: :solid, color: :primary, css_classes: css_classes, **rest) do %>
  <%= yield %>
<% end %>
<% # Usage — your app's conventions, Kiso's rendering %>
<%= render "shared/primary_button" do %>
  Get started
<% end %>

This is the same pattern as wrapping a shadcn component in your own React component to bake in project defaults. The Kiso component handles styling and structure; your wrapper handles your app’s conventions.

Override styles globally

If you want all instances of a component to look different, use Kiso.configure in an initializer instead of repeating css_classes: on every call site:

# config/initializers/kiso.rb
Kiso.configure do |config|
  config.theme[:button] = { base: "rounded-full" }
  config.theme[:card_header] = { base: "p-8 sm:p-10" }
  config.theme[:badge] = { defaults: { variant: :outline } }
end

Override hashes accept base:, variants:, compound_variants:, defaults:, and ui: — the same structure as the component’s theme definition. Overrides are applied once at boot via ClassVariants::Instance#merge, so there’s zero per-render cost. Changes require a server restart.

The ui: key targets inner sub-part elements globally:

Kiso.configure do |config|
  config.theme[:card] = { base: "rounded-xl", ui: { header: "p-8", footer: "px-8" } }
  config.theme[:alert] = { ui: { close: "opacity-50" } }
end

Layer order: theme default < global config (base + ui) < instance ui: < instance css_classes:. Each layer wins over the previous.

Override inner elements per-instance

css_classes: only reaches the root element. For inner sub-parts, use ui::

<%= kui(:card, ui: { header: "p-8 bg-muted", title: "text-xl" }) do %>
  <%= kui(:card, :header) do %>
    <%= kui(:card, :title) { "Dashboard" } %>
  <% end %>
  <%= kui(:card, :content) { "Body content" } %>
<% end %>

Self-rendering components (Alert, Dialog, Slider, Switch, etc.) apply ui: to their internal structure:

<%= kui(:alert, icon: "triangle-alert", color: :warning, ui: {
  close: "opacity-50",
  wrapper: "gap-4"
}) do %>
  <%= kui(:alert, :title) { "Heads up" } %>
  <%= kui(:alert, :description) { "Something happened." } %>
<% end %>

<%= kui(:slider, ui: { track: "bg-muted", thumb: "bg-primary" }) %>

Available slot names match the sub-part names used in kui(:component, :part). See each component’s docs page for its available slots.

Theme your app with CSS variables

Kiso’s semantic tokens (bg-primary, text-foreground, etc.) resolve to CSS custom properties. Override them in your Tailwind stylesheet to match your brand. See the CSS Variables Reference for the complete list of overridable tokens.

@import "tailwindcss";
@import "../builds/tailwind/kiso";

@theme {
  --color-primary: var(--color-violet-600);
  --color-primary-foreground: var(--color-white);
  --color-secondary: var(--color-pink-600);
  --color-secondary-foreground: var(--color-white);
}

Every component automatically picks up your overrides — no changes to individual component calls needed. This is similar to overriding a design token system in Chakra UI or shadcn’s globals.css approach.

Dark mode

Kiso uses CSS variable swapping for dark mode. A .dark class on your <html> or <body> element activates the dark palette:

<html class="dark">

Components don’t use Tailwind’s dark: prefix. Instead, the CSS variables change value inside .dark {}, and every bg-primary, text-foreground, bg-muted etc. automatically resolves to its dark equivalent. You don’t need to do anything per-component.

Pass HTML attributes through

**component_options forwards any extra locals to the root element. Use this for id, data-*, aria-*, and Stimulus attributes:

<%= kui(:button,
    id: "delete-btn",
    data: { action: "click->confirm#show" },
    aria: { label: "Delete item" }) do %>
  Delete
<% end %>

This is equivalent to React’s {...rest} spread or Vue’s v-bind="$attrs".