diff --git a/src/FormElement/RadioElement.php b/src/FormElement/RadioElement.php
new file mode 100644
index 00000000..2862066b
--- /dev/null
+++ b/src/FormElement/RadioElement.php
@@ -0,0 +1,168 @@
+setName($name);
+
+ $this->getAttributes()->registerAttributeCallback(
+ 'options',
+ null,
+ [$this, 'setOptions']
+ );
+
+ // ZF1 compatibility:
+ $this->getAttributes()->registerAttributeCallback(
+ 'multiOptions',
+ null,
+ [$this, 'setOptions']
+ );
+
+ $this->getAttributes()->registerAttributeCallback(
+ 'disabled',
+ null,
+ [$this, 'disableOptions']
+ );
+
+ parent::__construct($name, $attributes);
+ }
+
+ /**
+ * Disable radio button that contains given value
+ *
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function disableOption(string $value): RadioElement
+ {
+ if ($option = $this->getOption($value)) {
+ $option->setAttribute('disabled', true);
+ }
+
+ if ($this->getValue() == $value) {
+ $this->valid = false;
+ $this->addMessage("'$value' is not allowed here");
+ }
+
+ return $this;
+ }
+
+ /**
+ * Disable radio buttons that contain given values
+ *
+ * @param mixed $values array or string
+ *
+ * @return $this
+ */
+ public function disableOptions($values): RadioElement
+ {
+ $values = (array) $values;
+ foreach ($values as $value) {
+ $this->disableOption($value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get radio button with given value
+ *
+ * @param string $value
+ *
+ * @return InputElement|null
+ */
+ public function getOption(string $value): ?InputElement
+ {
+ foreach ($this->optionContent as $radio) {
+ if ($radio->getValueAttribute() === $value) {
+ return $radio;
+ }
+ }
+
+ return null;
+ }
+
+ public function setValue($value)
+ {
+ parent::setValue($value);
+
+ foreach ($this->optionContent as $radio) {
+ $radio->getAttributes()->remove('checked');
+ if ($radio->getValueAttribute() === $value) {
+ $radio->setAttribute('checked', true);
+ }
+ }
+ }
+
+ public function validate()
+ {
+ $value = $this->getValue();
+ if (
+ $value !== null
+ && (! ($option = $this->getOption($value)) || $option->getAttributes()->has('disabled'))
+ ) {
+ $this->valid = false;
+ $this->addMessage(sprintf($this->translate("'%s' is not allowed here"), $value));
+
+ return $this;
+ }
+
+ return parent::validate();
+ }
+
+ /**
+ * Prepare options
+ *
+ * @param array $options
+ *
+ * @return $this
+ */
+ public function setOptions(array $options): RadioElement
+ {
+ foreach ($options as $value => $label) {
+ $input = new InputElement($this->getName(), [
+ 'name' => $this->getName(),
+ 'type' => $this->type,
+ 'value' => $value
+ ]);
+
+ $this->optionContent[$label] = $input;
+ }
+
+ return $this;
+ }
+
+ public function renderUnwrapped()
+ {
+ return $this->renderContent();
+ }
+
+ protected function assemble()
+ {
+ foreach ($this->optionContent as $label => $radioElm) {
+ $labelElm = new HtmlElement(
+ 'label',
+ Attributes::create(['class' => 'radio-label']),
+ $radioElm,
+ HtmlString::create($label)
+ );
+
+ $this->addHtml($labelElm);
+ }
+ }
+}
diff --git a/tests/FormElement/RadioElementTest.php b/tests/FormElement/RadioElementTest.php
new file mode 100644
index 00000000..0166fd5d
--- /dev/null
+++ b/tests/FormElement/RadioElementTest.php
@@ -0,0 +1,150 @@
+ 'Test',
+ 'multiOptions' => [
+ 'foo' => 'Foo',
+ 'bar' => 'Bar',
+ 'yes' => 'Yes'
+ ]
+ ]);
+
+ $this->assertHtml(
+ ''
+ . ''
+ . '',
+ $radio
+ );
+ }
+
+ public function testCheckCorrectRadio()
+ {
+ $radio = new RadioElement('test', [
+ 'label' => 'Test',
+ 'multiOptions' => [
+ 'foo' => 'Foo',
+ 'bar' => 'Bar',
+ 'yes' => 'Yes'
+ ],
+ 'value' => 'bar'
+ ]);
+
+ $this->assertHtml(
+ ''
+ . ''
+ . '',
+ $radio
+ );
+
+ $radio->setValue('yes');
+
+ $this->assertHtml(
+ ''
+ . ''
+ . '',
+ $radio
+ );
+
+ $radio->setValue('no');
+
+ $this->assertHtml(
+ ''
+ . ''
+ . '',
+ $radio
+ );
+ }
+
+ public function testDisableRadio()
+ {
+ $radio = new RadioElement('test', [
+ 'label' => 'Test',
+ 'multiOptions' => [
+ 'foo' => 'Foo',
+ 'bar' => 'Bar',
+ 'yes' => 'Yes',
+ 'no' => 'No'
+ ],
+ 'value' => 'bar',
+ 'disabled' => 'yes'
+ ]);
+
+ $this->assertHtml(
+ ''
+ . ''
+ . ''
+ . '',
+ $radio
+ );
+
+ $radio->disableOptions(['yes', 'no', 'foo']);
+
+ $this->assertHtml(
+ ''
+ . ''
+ . ''
+ . '',
+ $radio
+ );
+ }
+
+ public function testRadioNotValidIfCheckedValueIsInvalid()
+ {
+ StaticTranslator::$instance = new NoopTranslator();
+ $radio = new RadioElement('test', [
+ 'label' => 'Test',
+ 'multiOptions' => [
+ 'foo' => 'Foo',
+ 'bar' => 'Bar',
+ 'yes' => 'Yes'
+ ],
+ 'value' => 'bar',
+ 'disabled' => 'yes'
+ ]);
+
+ $this->assertTrue($radio->isValid());
+
+ $radio->setValue('no');
+ $this->assertFalse($radio->isValid());
+
+ $radio->setValue('tom');
+ $this->assertFalse($radio->isValid());
+ }
+
+ public function testRadioNotValidIfCheckedValueIsDisabled()
+ {
+ StaticTranslator::$instance = new NoopTranslator();
+ $radio = new RadioElement('test', [
+ 'label' => 'Test',
+ 'multiOptions' => [
+ 'foo' => 'Foo',
+ 'bar' => 'Bar',
+ 'yes' => 'Yes'
+ ],
+ 'value' => 'bar',
+ 'disabled' => 'yes'
+ ]);
+
+ $this->assertTrue($radio->isValid());
+
+ $radio->setValue('yes');
+ $this->assertFalse($radio->isValid());
+
+ $radio->setValue('bar');
+ $this->assertTrue($radio->isValid());
+
+ $radio->disableOptions(['bar', 'yes', 'foo']);
+ $this->assertFalse($radio->isValid());
+ }
+}