Skip to main content

RadioCard

Overview​

The RadioCard component is a radio button drawn as a card, with a title and a description, picked wherever you click it.

Reach for it where each option wants a line or two of its own and the whole box should be the thing you click: a plan, a shipping method, an answer in a setup step. For choices you can take any of, use CheckboxCard.

It's a real radio button inside a <label>, so it sends its value with a form, and the cards that share a name are one choice: Tab lands on the picked one and the arrow keys move between them. A screen reader hears the title as its name and the description after it. Inside Radios it takes the group's name and selection, as Radio does.

info

Bulma has no selectable card, so RadioCard brings its own styles, in bestax.css and its variants, or in extras.css next to your own Bulma. The surface is Bulma's .card, so a site's Card theming applies to it.


Import​

import { RadioCard } from '@allxsmith/bestax-bulma';

Usage​

Basic Usage​

Put the cards in Radios with a name, and give each a value, a title and a description. Click anywhere on a card to pick it, or Tab to the group and use the arrow keys.

<Radios label="Plan" name="plan" defaultValue="pro">
  <RadioCard
    value="starter"
    title="Starter"
    description="One site, community support."
  />
  <RadioCard value="pro" title="Pro" description="Ten sites, email support." />
  <RadioCard
    value="team"
    title="Team"
    description="Unlimited sites, a support plan."
  />
</Radios>

The cards share the row and wrap once they'd get narrower than --bulma-radio-card-min-width.

Controlled​

The group holds the selection, here in state.

function example() {
  const [shipping, setShipping] = useState('standard');
  return (
    <>
      <Radios
        label="Shipping"
        name="shipping"
        value={shipping}
        onChange={setShipping}
      >
        <RadioCard
          value="standard"
          title="Standard"
          description="Arrives in 4 to 6 days."
        />
        <RadioCard
          value="express"
          title="Express"
          description="Arrives tomorrow."
        />
        <RadioCard value="pickup" title="Pickup" description="From the shop." />
      </Radios>
      <Paragraph mt="3">Shipping: {shipping}</Paragraph>
    </>
  );
}

Without the Indicator​

With hasIndicator={false} the card drops its radio, and the edge alone shows which is picked: it's thicker as well as colored, so the selection never rests on color.

<Radios label="Delivery time" name="time" defaultValue="morning">
  <RadioCard
    value="morning"
    hasIndicator={false}
    title="Morning"
    description="8 to 12"
  />
  <RadioCard
    value="afternoon"
    hasIndicator={false}
    title="Afternoon"
    description="12 to 5"
  />
  <RadioCard
    value="evening"
    hasIndicator={false}
    title="Evening"
    description="5 to 9"
  />
</Radios>

More in the Card​

Anything you pass as children follows the description, such as a price. A link works, and following it doesn't pick the card. A <label> holds phrasing content only, so use spans, images and links: a button or another input can't go inside one at all, and a <div>, a heading or a <p> isn't valid there either.

<Radios label="Plan" name="priced-plan" defaultValue="monthly">
  <RadioCard value="monthly" title="Monthly" description="Cancel anytime.">
    <Span display="block" mt="2" textWeight="semibold">
      $12 a month
    </Span>
  </RadioCard>
  <RadioCard value="yearly" title="Yearly" description="Two months free.">
    <Span display="block" mt="2" textWeight="semibold">
      $120 a year
    </Span>
    <a href="#yearly-terms">How billing works</a>
  </RadioCard>
</Radios>

What's in the children isn't read when the radio takes focus, so put what a screen reader should hear in title and description.

Columns​

A Bulma Grid inside the group lines the cards up in columns. Give it flexGrow="1" so it fills the group's row.

<Radios label="Time" name="slot" defaultValue="10">
  <Grid minCol={10} flexGrow="1">
    {['9', '10', '11', '12', '13', '14'].map(hour => (
      <RadioCard
        key={hour}
        value={hour}
        hasIndicator={false}
        title={`${hour}:00`}
        description="Open"
      />
    ))}
  </Grid>
</Radios>

Colors​

color paints the radio and the edge of the picked card. It takes the values radioColors holds.

<Radios name="accent" defaultValue="success">
  {radioColors.map(color => (
    <RadioCard key={color} value={color} color={color} title={color} />
  ))}
</Radios>

Disabled​

A disabled card dims, takes no clicks, and the arrow keys skip it.

<Radios label="Size" name="size" defaultValue="m">
  <RadioCard value="s" title="Small" description="In stock." />
  <RadioCard value="m" title="Medium" description="In stock." />
  <RadioCard value="l" disabled title="Large" description="Sold out." />
</Radios>

Form Submission​

The picked card sends its name and value, like any radio, and required on the cards stops a form going without one.

function example() {
  const [sent, setSent] = useState('');
  return (
    <form
      onSubmit={e => {
        e.preventDefault();
        setSent(String(new FormData(e.currentTarget).get('ship')));
      }}
    >
      <Radios label="Shipping" name="ship">
        <RadioCard
          value="standard"
          required
          title="Standard"
          description="4 to 6 days."
        />
        <RadioCard
          value="express"
          required
          title="Express"
          description="Tomorrow."
        />
      </Radios>
      <Button type="submit" color="primary">
        Send
      </Button>
      {sent && <Paragraph mt="3">Sent: {sent}</Paragraph>}
    </form>
  );
}

Theming the Card​

The card is Bulma's .card, so the Card variables style its surface, and its own variables, listed under CSS & Sass Variables, style the edge, the radio and the text. Set either on a class you pass through className, which lands on the .radio-card label that declares them, and write the selector with both classes (.radio-card.compact-card, .bestax-radio-card.compact-card under the prefixed flavor) so it wins whatever order the stylesheets load in.

Here the padding is tighter and the cards wrap sooner:

.radio-card.compact-card {
--bulma-card-content-padding: 1rem;
--bulma-radio-card-min-width: 16rem;
}
<Radios name="plan">
<RadioCard className="compact-card" value="starter" title="Starter" />
<RadioCard className="compact-card" value="pro" title="Pro" />
</Radios>

Accessibility​

  • The card is a <label> around a native <input type="radio">, so a click anywhere on it picks the radio and it sends its value with a form.
  • Cards that share a name are one group to the browser: Tab lands on the picked card, or the first, and the arrow keys move and pick, skipping disabled cards. Give Radios a name, or each card the same one; without it each card is a group of its own.
  • The radio is named by the title, through aria-labelledby, and described by the description, through aria-describedby. An aria-label or aria-labelledby you pass wins. Without a title, the card's own text names it, as a label's text does.
  • The children aren't read when the radio takes focus. Put what matters in title and description.
  • A link inside the card is its own Tab stop, and following it doesn't pick the card. A button or another input can't go inside a <label>.
  • Keyboard focus draws an outline around the whole card, in Bulma's focus color, whatever the card's color.
  • The selection never rests on color: the radio shows it, and the picked card's edge is twice as thick, with or without the radio.
  • Contrast: against the card, a picked edge in primary or link clears WCAG 1.4.11's 3:1 on a light page, and in every color on a dark one. info, success, warning and danger fall short on a light page, where the edge's width and the radio still show the selection.
  • In forced colors (Windows contrast themes), Bulma's card loses its shadow, so the edge is a real border there: the system's text color, Highlight and still thicker when picked, and gray when disabled. The radio takes system colors as Radio's does, and the focus outline is Highlight.
  • The edge and the radio change without a transition under prefers-reduced-motion: reduce.

  • CheckboxCard: The same card for choices you can take any of.
  • Radios: The group that names a set of cards and holds the selection.
  • Radio: A plain radio with a label beside it.
  • SegmentedControl: A short row of options that sends no form value.
  • Card: Bulma's card, whose variables style this one's surface.
  • Helper Props: Bulma helper props for spacing, color, etc.

Additional Resources​


Props​

PropTypeDefaultDescription
titleReact.ReactNode—What the option is, such as "Express". It names the radio, so a screen reader says it with the state and its place in the group. Without it, the card's whole text names the radio, as a label's text does.
descriptionReact.ReactNode—A line or two about the option, read after the name and the state. It's the radio's description when title, aria-label or aria-labelledby names it. Without one of those it's part of the name already, so it isn't read twice.
childrenReact.ReactNode—Anything else in the card, after the description, such as a price. A label holds phrasing content only, so spans, images and links: a link works and doesn't pick the card, but a button or another input can't go inside a label at all. It isn't read when the radio takes focus, so put what a screen reader should hear in title and description.
hasIndicatorbooleantrueShows the radio at the start of the card. Without it, the card's edge alone shows the selection, and it's thicker when picked, so the selection never rests on color.
color'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'—The color of the radio and of a picked card's edge.
textColorBulma color | 'inherit' | 'current'—Text color helper, on the card. The title and the children take it, and the description keeps its own color.
disabledbooleanfalseWhether the card is disabled. A disabled card dims, takes no clicks, and the arrow keys skip it.
classNamestring—Additional CSS classes, on the outer <label>, the card. That's the element that declares the --bulma-radio-card-* variables, and Bulma's --bulma-card-* ones, so a class is how to set one: style and the other input attributes go to the <input>. Name .radio-card beside your class in the selector, so it beats the card's own declaration whatever order the stylesheets load in. A color prop declares the active color again on .radio-card.is-<color>, which ties that selector, so leave color unset when a class sets it.
refReact.Ref<HTMLInputElement>—Forwarded to the underlying element.
namestring—The form field name, which the cards of one choice share, so the arrow keys move between them. Inside Radios, the group's name when this is unset.
valuestring—What a form sends when this card is the one picked. Inside a Radios group that takes value, defaultValue or onChange, the card is checked while the group's value is this.
...All standard <input> attributes and Bulma helper props—Helper props go on the outer <label>, the .card, with className. The other attributes go on the <input>. See Helper Props

CSS & Sass Variables​

RadioCard registers these variables on its own .radio-card element. Override them there (or via className) — a value set on an ancestor is only inherited, and loses to the component-level declaration. See Theme.

CSS VariableSass VariableDefault
--bulma-radio-card-border-width$radio-card-border-width1px
--bulma-radio-card-border-color$radio-card-border-colorvar(--bulma-border)
--bulma-radio-card-hover-border-color$radio-card-hover-border-colorvar(--bulma-border-hover)
--bulma-radio-card-checked-border-width$radio-card-checked-border-width2px
--bulma-radio-card-active-color$radio-card-active-colorvar(--bulma-primary)
--bulma-radio-card-focus-color$radio-card-focus-colorhsl(var(--bulma-focus-h), var(--bulma-focus-s), var(--bulma-focus-l))
--bulma-radio-card-focus-width$radio-card-focus-widthvar(--bulma-focus-width)
--bulma-radio-card-focus-offset$radio-card-focus-offset2px
--bulma-radio-card-gap$radio-card-gap0.75em
--bulma-radio-card-min-width$radio-card-min-width12rem
--bulma-radio-card-title-weight$radio-card-title-weightvar(--bulma-weight-semibold)
--bulma-radio-card-description-color$radio-card-description-colorvar(--bulma-text-weak)
--bulma-radio-card-indicator-size$radio-card-indicator-size1.25em
--bulma-radio-card-indicator-border-width$radio-card-indicator-border-width2px
--bulma-radio-card-indicator-border-color$radio-card-indicator-border-colorvar(--bulma-text-weak)
--bulma-radio-card-disabled-opacity$radio-card-disabled-opacity0.5
--bulma-radio-card-transition-duration$radio-card-transition-durationvar(--bulma-duration)