Doc Page

Quick Start (Vanilla)

Vanilla setup path with explicit DOM bindings, lifecycle cleanup, and platform constraints.

Updated: 2026-05-30 · Owner: docs@nake-ui

Pick An Adapter

Choose the adapter that matches the host runtime. All adapters share the same behavioral contract, but each one exposes lifecycle and state in the way that feels natural for that framework.

Adapter Install path Best fit
React @nake-ui/adapter-react JSX compound components and SSR/hydration checks
Vanilla @nake-ui/adapter-vanilla Plain DOM, progressive enhancement, custom frameworks
Vue @nake-ui/adapter-vue Vue 3 composition and SFC templates
Solid @nake-ui/adapter-solid Signals and fine-grained JSX rendering
Svelte @nake-ui/adapter-svelte Svelte compound primitive namespaces

Use one adapter per rendered primitive instance. Avoid mixing framework wrappers around the same DOM nodes unless you own the lifecycle boundary and cleanup order.

Sources: shared/en/quick-start

Install

pnpm add @nake-ui/primitives @nake-ui/adapter-vanilla

The vanilla adapter has no framework peer dependency. It binds real DOM nodes and returns a lifecycle handle.

Sources: shared/en/quick-start, vanilla/en/quick-start

Render A First Primitive

Write stable HTML first:

<div data-demo="tabs">
  <div data-slot="list" aria-label="Project sections"></div>
  <button data-demo-tab="overview" type="button">Overview</button>
  <button data-demo-tab="activity" type="button">Activity</button>
  <section id="panel-overview">Release readiness.</section>
  <section id="panel-activity">Recent activity.</section>
</div>

Then bind it:

import { bindTabs } from "@nake-ui/adapter-vanilla";

const list = document.querySelector<HTMLElement>("[data-slot='list']");
const overview = document.querySelector<HTMLElement>("[data-demo-tab='overview']");
const activity = document.querySelector<HTMLElement>("[data-demo-tab='activity']");
const overviewPanel = document.querySelector<HTMLElement>("#panel-overview");
const activityPanel = document.querySelector<HTMLElement>("#panel-activity");

if (list && overview && activity && overviewPanel && activityPanel) {
  const binding = bindTabs(
    {
      list,
      tabs: { overview, activity },
      panels: { overview: overviewPanel, activity: activityPanel }
    },
    [{ id: "overview" }, { id: "activity" }],
    { id: "project-tabs", defaultValue: "overview", activationMode: "manual" }
  );

  window.addEventListener("beforeunload", () => binding.destroy(), { once: true });
}

Sources: shared/en/quick-start, vanilla/en/quick-start

Verify The Contract

After binding:

  • The list has data-ui="tabs", role="tablist", and data-orientation.
  • Each tab has role="tab", aria-selected, aria-controls, and roving tabIndex.
  • Each panel has role="tabpanel", aria-labelledby, data-state, and hidden when inactive.
  • destroy() can run more than once without leaving duplicate listeners.

Sources: shared/en/quick-start, vanilla/en/quick-start

Styling

Optional starter skin:

pnpm add @nake-ui/theme-vanilla
import "@nake-ui/theme-vanilla/styles.css";

You can also write plain CSS against public attributes. The vanilla adapter does not own layout or visual classes.

Sources: shared/en/quick-start, vanilla/en/quick-start

Agent Checklist

Ask agents to generate both:

  1. Markup with stable selectors and IDs.
  2. Binding code with bind*, registrations, sync() when needed, and destroy() cleanup.

The cleanup path is part of the vanilla contract.

Sources: shared/en/quick-start, vanilla/en/quick-start

Platform Boundaries

Vanilla is the best adapter for progressive enhancement and custom framework integration. The host must own mount, update, and unmount timing. Floating primitives measure on the client and need valid anchor/content nodes before positioning sync.

Sources: shared/en/quick-start, vanilla/en/quick-start

Next Reading

  • Installation (Vanilla)
  • Adapter Integration (Vanilla)
  • Primitive Concepts
  • Components Overview
  • Troubleshooting

Sources: vanilla/en/quick-start