Getting Started
IntroductionHow to Use
Components
FrameMenuAlertAccordionDialogTabsToastNewButtonInputSwitchTextareaRadio GroupCheckboxChartComboboxSelectNewAlert DialogSoonAvatarSoonBreadcrumbSoonButton GroupSoonCalendarSoonDatepickerSoonDaterangepickerSoonSheetSoonNumber InputSoonPaginationSoonTableSoon

Checkbox

A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time.
Installation
Expand

npx @left4code/cosmic-ui-cli@latest add checkbox
 
Expand

This writes the component and everything it imports, then installs the packages it needs. Run npx @left4code/cosmic-ui-cli@latest initfirst if you haven't already (it detects Vue automatically).

Install the following dependencies:

pnpm
npm
yarn
bun
yarn add @zag-js/checkbox @zag-js/vue

This component is built directly on the Zag.js state machine (not a wrapper). No Portal or presence composable needed, the check mark just toggles opacity, no exit animation. The hidden native input (needed for keyboard, focus and form submission) renders automatically insideCheckboxRoot. Vue SFCs can't export multiple components from one file, so Checkbox is a folder instead of a single file — copy all of it into your project.

components/ui/checkbox/context.ts
Expand

import type { InjectionKey, ComputedRef } from "vue";
import type * as checkbox from "@zag-js/checkbox";

type CheckboxApi = ComputedRef<ReturnType<typeof checkbox.connect>>;

const CheckboxApiKey: InjectionKey<CheckboxApi> = Symbol("checkbox-api");

export { CheckboxApiKey };
export type { CheckboxApi };
 
Expand
components/ui/checkbox/CheckboxRoot.vue
Expand

<script setup lang="ts">
import { computed, provide, useAttrs, useId } from "vue";
import { useMachine, normalizeProps } from "@zag-js/vue";
import * as checkbox from "@zag-js/checkbox";
import { twMerge } from "tailwind-merge";
import { CheckboxApiKey } from "./context";

defineOptions({ inheritAttrs: false });
const rawAttrs = useAttrs() as Partial<checkbox.Props> & { class?: string };
const id = useId();

const machineProps = computed(() => {
  const { class: _class, ...rest } = rawAttrs;
  return { id, ...rest };
});
const service = useMachine(checkbox.machine, machineProps);
const api = computed(() => checkbox.connect(service, normalizeProps));

provide(CheckboxApiKey, api);
</script>

<template>
  <label
    v-bind="api.getRootProps()"
    :class="twMerge(['flex gap-3.5 items-center cursor-pointer', rawAttrs.class])"
  >
    <slot />
    <input v-bind="api.getHiddenInputProps()" />
  </label>
</template>
 
Expand
components/ui/checkbox/CheckboxLabel.vue
Expand

<script setup lang="ts">
import { inject } from "vue";
import { twMerge } from "tailwind-merge";
import { CheckboxApiKey } from "./context";

const { class: className } = defineProps<{ class?: string }>();

const injectedApi = inject(CheckboxApiKey);
if (!injectedApi) throw new Error("Checkbox parts must be used within <CheckboxRoot>");
const api = injectedApi;
</script>

<template>
  <span v-bind="api.getLabelProps()" :class="twMerge(['order-2', className])">
    <slot />
  </span>
</template>
 
Expand
components/ui/checkbox/CheckboxControl.vue
Expand

<script setup lang="ts">
import { inject } from "vue";
import { twMerge } from "tailwind-merge";
import Frame, { parsePaths } from "@/components/ui/frame.vue";
import { Check } from "@lucide/vue";
import { CheckboxApiKey } from "./context";

const { class: className } = defineProps<{ class?: string }>();

const injectedApi = inject(CheckboxApiKey);
if (!injectedApi) throw new Error("Checkbox parts must be used within <CheckboxRoot>");
const api = injectedApi;

const framePaths = parsePaths(
  '[{"show":true,"style":{"strokeWidth":"1","stroke":"var(--color-frame-1-stroke)","fill":"var(--color-frame-1-fill)"},"path":[["M","50% - 28.125%","0"],["L","50% + 28.125%","0"],["L","100% + 0","50% - 28.125%"],["L","100% + 0","50% + 28.125%"],["L","50% + 28.125%","100% - 0"],["L","50% - 28.125%","100% + 0"],["L","0","50% + 28.125%"],["L","0","50% - 28.125%"],["L","50% - 28.125%","0"]]}]'
);
</script>

<template>
  <div
    v-bind="api.getControlProps()"
    :class="
      twMerge([
        'group relative size-5 flex items-center justify-center data-[state=checked]:drop-shadow-[0_0px_20px_var(--color-primary)]',
        '[--color-frame-1-stroke:var(--color-primary)]/80',
        '[--color-frame-1-fill:var(--color-primary)]/10',
        className,
      ])
    "
  >
    <Frame :paths="framePaths" />
    <Check
      class="group-data-[state=checked]:opacity-100 opacity-0 size-6 -mt-1 -mr-2 stroke-(--color-primary)/80 drop-shadow-[0_0px_2px_var(--color-primary)] transition-all duration-100"
    />
  </div>
</template>
 
Expand
components/ui/checkbox/index.ts
Expand

export { default as CheckboxRoot } from "./CheckboxRoot.vue";
export { default as CheckboxLabel } from "./CheckboxLabel.vue";
export { default as CheckboxControl } from "./CheckboxControl.vue";
 
Expand

Update the import paths to match your project setup.

Usage
Expand

import { CheckboxRoot, CheckboxLabel, CheckboxControl } from "@/components/ui/checkbox";
 
Expand
Expand

<CheckboxRoot>
  <CheckboxLabel>Accept terms and conditions</CheckboxLabel>
  <CheckboxControl />
</CheckboxRoot>
 
Expand
Powered by synthetic caffeine · Deployed by Left4code · Signal traceable on GitHub.