Styled form label with optional for association.

Preview
Style
src/demo/views/label.ts49 lines
import { Schema as S } from 'effect'
import { evo } from 'foldkit/struct'
import { defineMessageUnion } from 'foldkit/message'
import type { Html, HtmlBuilder } from 'foldkit/html'

import { checkbox } from '../../generated/registry/ui/checkbox'

import { defineSlice, type UpdateReturn } from '../slice'
import type { Model, Message as AppMessage } from '../assemble'

const Message = defineMessageUnion({
  ToggledLabelCheckbox: { isChecked: S.Boolean },
})

export const labelView = (model: Model, h: HtmlBuilder<AppMessage>): Html =>
  h.div(
    [h.Class('flex gap-2')],
    [
      checkbox<AppMessage>(
        {
          id: 'terms',
          label: 'Accept terms and conditions',
          isChecked: model.isLabelChecked,
          onToggle: (isChecked) => Message.ToggledLabelCheckbox({ isChecked }),
        },
        h,
      ),
    ],
  )

const fields = { isLabelChecked: S.Boolean }

const stateSchema = S.Struct(fields)
type State = typeof stateSchema.Type

export const slice = defineSlice({
  fields,
  init: { isLabelChecked: false },
  messages: [Message.ToggledLabelCheckbox],
  handlers: (model: State) => ({
    ToggledLabelCheckbox: ({
      isChecked,
    }: typeof Message.ToggledLabelCheckbox.Type): UpdateReturn => ({
      model: evo(model, { isLabelChecked: () => isChecked }),
    }),
  }),
  samples: [Message.ToggledLabelCheckbox({ isChecked: true })],
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/label

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/ui/label.ts31 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

type Child = Html | string

import { cn } from '@/lib/utils'

/** Upstream keys label disabling on a native peer-disabled sibling variant
 *  and `group-data-[disabled=true]`, neither of which matches under foldkit
 *  (no `.peer` sibling; `data-disabled` is emitted empty). Re-keyed onto a
 *  live `group` ancestor carrying data-disabled — e.g. the fieldset compound
 *  or switch wrapper. */
export const labelClass =
  'gap-2 text-sm leading-none font-medium group-data-[disabled]:opacity-50 flex items-center select-none group-data-[disabled]/field-set:pointer-events-none group-data-[disabled]/field-set:cursor-not-allowed group-data-[disabled]/field-set:opacity-50'

type LabelConfig = Readonly<{ forId?: string; className?: string }>

/** Styled label. Mirrors the shadcn v4 `label.tsx` (no Radix primitive). */
export const label = <M>(
  config: LabelConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.label(
    [
      h.Class(cn(labelClass, config.className)),
      h.DataAttribute('slot', 'label'),
      ...(config.forId === undefined ? [] : [h.For(config.forId)]),
    ],
    children,
  )