Radio

Radio control primitive for mutually exclusive options, typically composed within ui-radio-group.

Default

With checked omitted, Radio starts unselected and enabled.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-radio/ui-radio.html"
>

<ui-radio
  value="option"
  name="radio-group"
>Radio</ui-radio>
<script setup>
  import { UiRadio } from "@threadlabs/looma";
</script>

<template>
  <UiRadio
    value="option"
    name="radio-group"
  >Radio</UiRadio>
</template>
import { UiRadio } from "@threadlabs/looma";

export default function Example() {
  return <UiRadio
    value="option"
    name="radio-group"
  >Radio</UiRadio>;
}
/** @jsxImportSource preact */
import { UiRadio } from "@threadlabs/looma";

export default function Example() {
  return <UiRadio
    value="option"
    name="radio-group"
  >Radio</UiRadio>;
}
<script>
  import { UiRadio } from "@threadlabs/looma";
</script>

<UiRadio
  value="option"
  name="radio-group"
>Radio</UiRadio>
import { Component } from "@angular/core";
import { UiRadio } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiRadio],
  template: `
    <label
      ui-radio
      [value]="'option'"
      [name]="'radio-group'"
    >Radio</label>
  `,
})
export default class Example {}

checked and disabled

Default-checked supplies initial selection while disabled prevents later changes.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-radio/ui-radio.html"
>

<ui-cluster
  gap="l"
>
  <ui-radio
    name="radio-state"
    value="checked"
    checked
  >Checked</ui-radio>
  <ui-radio
    name="radio-state"
    value="disabled"
    disabled
  >Disabled</ui-radio>
</ui-cluster>
<script setup>
  import { UiCluster, UiRadio } from "@threadlabs/looma";
</script>

<template>
  <UiCluster
    gap="l"
  >
    <UiRadio
      name="radio-state"
      value="checked"
      :checked="true"
    >Checked</UiRadio>
    <UiRadio
      name="radio-state"
      value="disabled"
      :disabled="true"
    >Disabled</UiRadio>
  </UiCluster>
</template>
import { UiCluster, UiRadio } from "@threadlabs/looma";

export default function Example() {
  return <UiCluster
    gap="l"
  >
    <UiRadio
      name="radio-state"
      value="checked"
      checked
    >Checked</UiRadio>
    <UiRadio
      name="radio-state"
      value="disabled"
      disabled
    >Disabled</UiRadio>
  </UiCluster>;
}
/** @jsxImportSource preact */
import { UiCluster, UiRadio } from "@threadlabs/looma";

export default function Example() {
  return <UiCluster
    gap="l"
  >
    <UiRadio
      name="radio-state"
      value="checked"
      checked
    >Checked</UiRadio>
    <UiRadio
      name="radio-state"
      value="disabled"
      disabled
    >Disabled</UiRadio>
  </UiCluster>;
}
<script>
  import { UiCluster, UiRadio } from "@threadlabs/looma";
</script>

<UiCluster
  gap="l"
>
  <UiRadio
    name="radio-state"
    value="checked"
    checked
  >Checked</UiRadio>
  <UiRadio
    name="radio-state"
    value="disabled"
    disabled
  >Disabled</UiRadio>
</UiCluster>
import { Component } from "@angular/core";
import { UiCluster, UiRadio } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCluster, UiRadio],
  template: `
    <div
      ui-cluster
      [gap]="'l'"
    >
      <label
        ui-radio
        [name]="'radio-state'"
        [value]="'checked'"
        [checked]="true"
      >Checked</label>
      <label
        ui-radio
        [name]="'radio-state'"
        [value]="'disabled'"
        [disabled]="true"
      >Disabled</label>
    </div>
  `,
})
export default class Example {}

With a description

The description slot says what a choice means, on a line under its label. It is the input's accessible description, not part of its name.

Who can open it

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-radio-group/ui-radio-group.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-radio/ui-radio.html"
>

<ui-radio-group
  label="Who can open it"
  name="radio-description"
  orientation="vertical"
  value="open"
>
  <ui-radio
    value="open"
  >
    Open
    <span
      slot="description"
    >Everyone on the site can open it, with the role they have on the site.</span>
  </ui-radio>
  <ui-radio
    value="private"
  >
    Private
    <span
      slot="description"
    >Only the people you add can open it.</span>
  </ui-radio>
</ui-radio-group>
<script setup>
  import { UiRadio, UiRadioGroup } from "@threadlabs/looma";
</script>

<template>
  <UiRadioGroup
    label="Who can open it"
    name="radio-description"
    orientation="vertical"
    value="open"
  >
    <UiRadio
      value="open"
    >Open<template #description>
        <span>Everyone on the site can open it, with the role they have on the site.</span>
      </template></UiRadio>
    <UiRadio
      value="private"
    >Private<template #description>
        <span>Only the people you add can open it.</span>
      </template></UiRadio>
  </UiRadioGroup>
</template>
import { UiRadio, UiRadioGroup } from "@threadlabs/looma";

export default function Example() {
  return <UiRadioGroup
    label="Who can open it"
    name="radio-description"
    orientation="vertical"
    value="open"
  >
    <UiRadio
      value="open"
      slots={{
        description: <span>Everyone on the site can open it, with the role they have on the site.</span>,
      }}
    >Open</UiRadio>
    <UiRadio
      value="private"
      slots={{
        description: <span>Only the people you add can open it.</span>,
      }}
    >Private</UiRadio>
  </UiRadioGroup>;
}
/** @jsxImportSource preact */
import { UiRadio, UiRadioGroup } from "@threadlabs/looma";

export default function Example() {
  return <UiRadioGroup
    label="Who can open it"
    name="radio-description"
    orientation="vertical"
    value="open"
  >
    <UiRadio
      value="open"
      slots={{
        description: <span>Everyone on the site can open it, with the role they have on the site.</span>,
      }}
    >Open</UiRadio>
    <UiRadio
      value="private"
      slots={{
        description: <span>Only the people you add can open it.</span>,
      }}
    >Private</UiRadio>
  </UiRadioGroup>;
}
<script>
  import { UiRadio, UiRadioGroup } from "@threadlabs/looma";
</script>

{#snippet description()}
  <span>Everyone on the site can open it, with the role they have on the site.</span>
{/snippet}

{#snippet radioDescription()}
  <span>Only the people you add can open it.</span>
{/snippet}

<UiRadioGroup
  label="Who can open it"
  name="radio-description"
  orientation="vertical"
  value="open"
>
  <UiRadio
    value="open"
    slots={{ description }}
  >Open</UiRadio>
  <UiRadio
    value="private"
    slots={{ description: radioDescription }}
  >Private</UiRadio>
</UiRadioGroup>
import { Component } from "@angular/core";
import { UiRadio, UiRadioGroup } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiRadioGroup, UiRadio],
  template: `
    <fieldset
      ui-radio-group
      [label]="'Who can open it'"
      [name]="'radio-description'"
      [orientation]="'vertical'"
      [value]="'open'"
    >
      <label
        ui-radio
        [value]="'open'"
      >Open<ng-container ngProjectAs="[slot='description']">
          <span>Everyone on the site can open it, with the role they have on the site.</span>
        </ng-container></label>
      <label
        ui-radio
        [value]="'private'"
      >Private<ng-container ngProjectAs="[slot='description']">
          <span>Only the people you add can open it.</span>
        </ng-container></label>
    </fieldset>
  `,
})
export default class Example {}

size

size="sm" keeps the radio's box and centres its label in the small control height, in the small controls' type size, so it lines up with a small button.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-radio/ui-radio.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-button/ui-button.html"
>

<ui-cluster
  gap="s"
  align="center"
>
  <ui-radio
    size="sm"
  >Radio</ui-radio>
  <ui-button
    size="sm"
  >Small button</ui-button>
</ui-cluster>
<script setup>
  import { UiButton, UiCluster, UiRadio } from "@threadlabs/looma";
</script>

<template>
  <UiCluster
    gap="s"
    align="center"
  >
    <UiRadio
      size="sm"
    >Radio</UiRadio>
    <UiButton
      size="sm"
    >Small button</UiButton>
  </UiCluster>
</template>
import { UiButton, UiCluster, UiRadio } from "@threadlabs/looma";

export default function Example() {
  return <UiCluster
    gap="s"
    align="center"
  >
    <UiRadio
      size="sm"
    >Radio</UiRadio>
    <UiButton
      size="sm"
    >Small button</UiButton>
  </UiCluster>;
}
/** @jsxImportSource preact */
import { UiButton, UiCluster, UiRadio } from "@threadlabs/looma";

export default function Example() {
  return <UiCluster
    gap="s"
    align="center"
  >
    <UiRadio
      size="sm"
    >Radio</UiRadio>
    <UiButton
      size="sm"
    >Small button</UiButton>
  </UiCluster>;
}
<script>
  import { UiButton, UiCluster, UiRadio } from "@threadlabs/looma";
</script>

<UiCluster
  gap="s"
  align="center"
>
  <UiRadio
    size="sm"
  >Radio</UiRadio>
  <UiButton
    size="sm"
  >Small button</UiButton>
</UiCluster>
import { Component } from "@angular/core";
import { UiButton, UiCluster, UiRadio } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCluster, UiRadio, UiButton],
  template: `
    <div
      ui-cluster
      [gap]="'s'"
      [align]="'center'"
    >
      <label
        ui-radio
        [size]="'sm'"
      >Radio</label>
      <ng-template
        ui-button
        [size]="'sm'"
      >Small button</ng-template>
    </div>
  `,
})
export default class Example {}

API

Package: @threadlabs/looma · Native root: label

Attributes

NameDescriptionPropertyTypeDefaultAllowed values
checkedWhether the radio is checked initially or when set externally. A form reset returns it to this. Inside a ui-radio-group the group's value decides instead.checkedbooleanfalse—
disabledDisables the native radio, so it cannot be chosen, focused, or reached with the arrow keys.disabledbooleanfalse—
nameThe native name that groups radios: of the radios that share a name, only one can be checked, and the arrow keys move between them. Inside a ui-radio-group the group sets the name, so leave this empty there.namestring—
requiredMarks the native radio required, so native form validation fails until a radio with the same name is checked.requiredbooleanfalse—
sizeAligns the radio with fields and buttons of the same size. It keeps its box size; the label takes that size's field type size and its first line centres in that control height (sm 2rem, lg 3rem by default), so in a row of inputs, selects, and buttons of one size it shares their height and their text baseline. Touch input keeps the same visible alignment. md is the standard layout.sizekeywordmdsm, md, lg
valueThe value this radio stands for: submitted with a form, reported in its change event, and matched against a ui-radio-group's value to decide which radio is checked.valuestringon—

Framework props

NameTypeDefaultAllowed values
checkedbooleanfalse—
disabledbooleanfalse—
namestring—
requiredbooleanfalse—
size"sm" | "md" | "lg"mdsm, md, lg
valuestringon—

Events

NameDetailDescription
changeobject
  • checked: boolean (required)
  • value: string (required)
  • trigger: keyword (required)
Emitted when a toggleable control changes checked state.

Slots

NamePurpose
defaultDefault child content.
descriptionNamed content for the description region.

Design tokens

Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.

Component tokens

TokenDeclared valuesFallback
--ui-radio-gap—var(--ui-space-2)
--ui-radio-size—var(--ui-choice-mark-size) | 1.125rem

Shared tokens

TokenFallback
--ui-accent-solid—
--ui-border-strong—
--ui-border-width—
--ui-choice-hover-surface—
--ui-choice-mark-size—
--ui-control-bordervar(--ui-border-strong)
--ui-control-border-hovervar(--ui-border-strong)
--ui-control-inset—
--ui-control-size-lg—
--ui-control-size-sm—
--ui-control-surfacevar(--ui-surface-default)
--ui-disabled-border—
--ui-disabled-surface—
--ui-disabled-text—
--ui-focus-ringvar(--ui-accent-solid)
--ui-focus-width—
--ui-font-size-md—
--ui-font-size-sm—
--ui-line-height—
--ui-line-height-md—
--ui-motion-ease—
--ui-motion-fast—
--ui-motion-feedback—
--ui-motion-feedback-ease—
--ui-neutral-wash-hover—
--ui-radius-round—
--ui-space-1—
--ui-space-2—
--ui-surface-default—
--ui-surface-highlightnone | linear-gradient(transparent, transparent)
--ui-text-on-accent—
--ui-text-primary—
--ui-text-secondary—