Toggle

A button that toggles between a pressed and unpressed state.

View source
import type { JSX } from "solid-js"
import * as Toggle from "@danielfrg/solid-ui/toggle"
import styles from "./index.module.css"

export function DemoToggleHero() {
  return (
    <div class={styles.panel}>
      <Toggle.Root aria-label="Favorite" class={styles.button}>
        {(state) =>
          state.pressed() ? <HeartFilledIcon class={styles.icon} /> : <HeartOutlineIcon class={styles.icon} />
        }
      </Toggle.Root>
    </div>
  )
}

function HeartFilledIcon(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
      <path d="M7.99961 13.8667C7.79961 13.8667 7.59961 13.8001 7.43294 13.6667C2.83294 9.80009 1.33294 8.00009 1.33294 5.73342C1.33294 3.73342 2.86628 2.13342 4.79961 2.13342C6.06628 2.13342 7.13294 2.80009 7.99961 4.06676C8.86628 2.80009 9.93294 2.13342 11.1996 2.13342C13.1329 2.13342 14.6663 3.73342 14.6663 5.73342C14.6663 8.00009 13.1663 9.80009 8.56628 13.6667C8.39961 13.8001 8.19961 13.8667 7.99961 13.8667Z" />
    </svg>
  )
}

function HeartOutlineIcon(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
      <path
        fill-rule="evenodd"
        clip-rule="evenodd"
        d="M7.99961 4.8232C7.09587 3.26498 5.9997 2.63342 4.79961 2.63342C3.14269 2.63342 1.83294 4.00498 1.83294 5.73342C1.83294 7.79233 3.24553 9.49498 7.59037 13.1084L7.99961 13.449L8.40885 13.1084C12.7537 9.49498 14.1663 7.79233 14.1663 5.73342C14.1663 4.00498 12.8565 2.63342 11.1996 2.63342C9.99953 2.63342 8.90335 3.26498 7.99961 4.8232ZM7.99961 12.281C3.96327 8.92498 2.83294 7.48062 2.83294 5.73342C2.83294 4.52498 3.72161 3.63342 4.79961 3.63342C5.7945 3.63342 6.68498 4.24282 7.43588 5.50889L7.99961 6.46008L8.56334 5.50889C9.31424 4.24282 10.2047 3.63342 11.1996 3.63342C12.2776 3.63342 13.1663 4.52498 13.1663 5.73342C13.1663 7.48062 12.0359 8.92498 7.99961 12.281Z"
      />
    </svg>
  )
}
.panel {
  display: flex;
  gap: 1px;
  border: 1px solid var(--color-gray-200);
  background-color: var(--color-gray-50);
  border-radius: 0.375rem;
  padding: 0.125rem;
}

.button {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  outline: 0;
  border: 0;
  border-radius: 0.25rem;
  background-color: transparent;
  color: var(--color-gray-600);
  user-select: none;
  cursor: pointer;
}

.button:focus-visible {
  background-color: transparent;
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

.button:active {
  background-color: var(--color-gray-200);
}

.button[data-pressed] {
  color: var(--color-gray-900);
}

.icon {
  width: 1.25rem;
  height: 1.25rem;
}

.status {
  margin: 0.75rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Examples

Default pressed

Use defaultPressed to set the initial pressed state without controlling it.

View source
import type { JSX } from "solid-js"
import * as Toggle from "@danielfrg/solid-ui/toggle"
import styles from "./index.module.css"

export function DemoToggleDefaultPressed() {
  return (
    <div class={styles.panel}>
      <Toggle.Root aria-label="Favorite" defaultPressed class={styles.button}>
        {(state) =>
          state.pressed() ? <HeartFilledIcon class={styles.icon} /> : <HeartOutlineIcon class={styles.icon} />
        }
      </Toggle.Root>
    </div>
  )
}

function HeartFilledIcon(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
      <path d="M7.99961 13.8667C7.79961 13.8667 7.59961 13.8001 7.43294 13.6667C2.83294 9.80009 1.33294 8.00009 1.33294 5.73342C1.33294 3.73342 2.86628 2.13342 4.79961 2.13342C6.06628 2.13342 7.13294 2.80009 7.99961 4.06676C8.86628 2.80009 9.93294 2.13342 11.1996 2.13342C13.1329 2.13342 14.6663 3.73342 14.6663 5.73342C14.6663 8.00009 13.1663 9.80009 8.56628 13.6667C8.39961 13.8001 8.19961 13.8667 7.99961 13.8667Z" />
    </svg>
  )
}

function HeartOutlineIcon(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
      <path
        fill-rule="evenodd"
        clip-rule="evenodd"
        d="M7.99961 4.8232C7.09587 3.26498 5.9997 2.63342 4.79961 2.63342C3.14269 2.63342 1.83294 4.00498 1.83294 5.73342C1.83294 7.79233 3.24553 9.49498 7.59037 13.1084L7.99961 13.449L8.40885 13.1084C12.7537 9.49498 14.1663 7.79233 14.1663 5.73342C14.1663 4.00498 12.8565 2.63342 11.1996 2.63342C9.99953 2.63342 8.90335 3.26498 7.99961 4.8232ZM7.99961 12.281C3.96327 8.92498 2.83294 7.48062 2.83294 5.73342C2.83294 4.52498 3.72161 3.63342 4.79961 3.63342C5.7945 3.63342 6.68498 4.24282 7.43588 5.50889L7.99961 6.46008L8.56334 5.50889C9.31424 4.24282 10.2047 3.63342 11.1996 3.63342C12.2776 3.63342 13.1663 4.52498 13.1663 5.73342C13.1663 7.48062 12.0359 8.92498 7.99961 12.281Z"
      />
    </svg>
  )
}
.panel {
  display: flex;
  gap: 1px;
  border: 1px solid var(--color-gray-200);
  background-color: var(--color-gray-50);
  border-radius: 0.375rem;
  padding: 0.125rem;
}

.button {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  outline: 0;
  border: 0;
  border-radius: 0.25rem;
  background-color: transparent;
  color: var(--color-gray-600);
  user-select: none;
  cursor: pointer;
}

.button:focus-visible {
  background-color: transparent;
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

.button:active {
  background-color: var(--color-gray-200);
}

.button[data-pressed] {
  color: var(--color-gray-900);
}

.icon {
  width: 1.25rem;
  height: 1.25rem;
}

.status {
  margin: 0.75rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Controlled

Use pressed and onChange to control the pressed state externally.

Not favorited.

View source
import { createSignal } from "solid-js"
import type { JSX } from "solid-js"
import * as Toggle from "@danielfrg/solid-ui/toggle"
import styles from "./index.module.css"

export function DemoToggleControlled() {
  const [pressed, setPressed] = createSignal(false)

  return (
    <div>
      <div class={styles.panel}>
        <Toggle.Root aria-label="Favorite" pressed={pressed()} onChange={setPressed} class={styles.button}>
          {(state) =>
            state.pressed() ? <HeartFilledIcon class={styles.icon} /> : <HeartOutlineIcon class={styles.icon} />
          }
        </Toggle.Root>
      </div>
      <p class={styles.status}>{pressed() ? "Favorited" : "Not favorited"}.</p>
    </div>
  )
}

function HeartFilledIcon(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
      <path d="M7.99961 13.8667C7.79961 13.8667 7.59961 13.8001 7.43294 13.6667C2.83294 9.80009 1.33294 8.00009 1.33294 5.73342C1.33294 3.73342 2.86628 2.13342 4.79961 2.13342C6.06628 2.13342 7.13294 2.80009 7.99961 4.06676C8.86628 2.80009 9.93294 2.13342 11.1996 2.13342C13.1329 2.13342 14.6663 3.73342 14.6663 5.73342C14.6663 8.00009 13.1663 9.80009 8.56628 13.6667C8.39961 13.8001 8.19961 13.8667 7.99961 13.8667Z" />
    </svg>
  )
}

function HeartOutlineIcon(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
      <path
        fill-rule="evenodd"
        clip-rule="evenodd"
        d="M7.99961 4.8232C7.09587 3.26498 5.9997 2.63342 4.79961 2.63342C3.14269 2.63342 1.83294 4.00498 1.83294 5.73342C1.83294 7.79233 3.24553 9.49498 7.59037 13.1084L7.99961 13.449L8.40885 13.1084C12.7537 9.49498 14.1663 7.79233 14.1663 5.73342C14.1663 4.00498 12.8565 2.63342 11.1996 2.63342C9.99953 2.63342 8.90335 3.26498 7.99961 4.8232ZM7.99961 12.281C3.96327 8.92498 2.83294 7.48062 2.83294 5.73342C2.83294 4.52498 3.72161 3.63342 4.79961 3.63342C5.7945 3.63342 6.68498 4.24282 7.43588 5.50889L7.99961 6.46008L8.56334 5.50889C9.31424 4.24282 10.2047 3.63342 11.1996 3.63342C12.2776 3.63342 13.1663 4.52498 13.1663 5.73342C13.1663 7.48062 12.0359 8.92498 7.99961 12.281Z"
      />
    </svg>
  )
}
.panel {
  display: flex;
  gap: 1px;
  border: 1px solid var(--color-gray-200);
  background-color: var(--color-gray-50);
  border-radius: 0.375rem;
  padding: 0.125rem;
}

.button {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  outline: 0;
  border: 0;
  border-radius: 0.25rem;
  background-color: transparent;
  color: var(--color-gray-600);
  user-select: none;
  cursor: pointer;
}

.button:focus-visible {
  background-color: transparent;
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

.button:active {
  background-color: var(--color-gray-200);
}

.button[data-pressed] {
  color: var(--color-gray-900);
}

.icon {
  width: 1.25rem;
  height: 1.25rem;
}

.status {
  margin: 0.75rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}