Actions

Toggle

A single button that stays pressed or unpressed, for one on or off choice such as pin, mute or wrap lines.

Built on
<button>aria-pressed
Client JavaScript
Yes, for state
Works without React
React only (needs JavaScript)
Source, gzipped
1.2 kB
Dependencies
React
Try its options

Loading its options.

Code
import { Toggle } from "@/components/equip/toggle";

export default function Demo() {
  return (
    <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
      <Toggle aria-label="Pin to sidebar" defaultPressed>
        <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M9.75 2.25 13.75 6.25M11.5 4l-3 3-3-.5L4 8l4 4 1.5-1.5L9 7.5l3-3M6 10l-3.5 3.5" />
        </svg>
      </Toggle>
      <Toggle>
        <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M8 2.5a3.5 3.5 0 0 0-3.5 3.5v2.5L3 11h10l-1.5-2.5V6A3.5 3.5 0 0 0 8 2.5ZM6.5 13a1.5 1.5 0 0 0 3 0" />
        </svg>
        Watch
      </Toggle>
    </div>
  );
}
Your versionchange its options in Look, This part

Examples

Sizes and states

An icon-only toggle needs an aria-label; the label says what it does and aria-pressed says whether it is on.

Code
import { Toggle } from "@/components/equip/toggle";

function Star() {
  return (
    <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="m8 2.25 1.75 3.6 3.95.55-2.85 2.8.7 3.95L8 11.3l-3.55 1.85.7-3.95L2.3 6.4l3.95-.55L8 2.25Z" />
    </svg>
  );
}

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 12, justifyItems: "start" }}>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
        <Toggle size="s" aria-label="Star repository">
          <Star />
        </Toggle>
        <Toggle size="s" defaultPressed aria-label="Star repository">
          <Star />
        </Toggle>
        <Toggle size="s" disabled aria-label="Star repository">
          <Star />
        </Toggle>
      </div>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
        <Toggle>Show archived</Toggle>
        <Toggle defaultPressed>Show archived</Toggle>
        <Toggle disabled>Show archived</Toggle>
      </div>
    </div>
  );
}

Controlled

Keep the label the same when the state changes: a screen reader already says pressed or not pressed.

12:04:11  GET /api/invoices 200 41ms
12:04:12  POST /api/invoices 201 88ms
12:04:15  GET /api/customers?page=2 200 63ms
Code
"use client";

import { useState } from "react";
import { Toggle } from "@/components/equip/toggle";

const lines = [
  "12:04:11  GET /api/invoices 200 41ms",
  "12:04:12  POST /api/invoices 201 88ms",
  "12:04:15  GET /api/customers?page=2 200 63ms",
];

export default function Example() {
  const [wrap, setWrap] = useState(false);
  return (
    <div style={{ display: "grid", gap: 8, width: "100%", maxWidth: 320 }}>
      <Toggle size="s" pressed={wrap} onPressedChange={setWrap} style={{ justifySelf: "end" }}>
        <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M2.5 4h11M2.5 8h9a2 2 0 0 1 0 4H9m0 0 1.5-1.5M9 12l1.5 1.5M2.5 12h4" />
        </svg>
        Wrap lines
      </Toggle>
      <pre
        style={{
          margin: 0,
          padding: 8,
          border: "1px solid var(--eq-line)",
          borderRadius: "var(--eq-radius-m)",
          font: "var(--eq-text-xs) / 1.5 var(--eq-font-mono)",
          fontVariationSettings: "var(--eq-font-mono-settings)",
          whiteSpace: wrap ? "pre-wrap" : "pre",
          overflowX: "auto",
        }}
      >
        {lines.join("\n")}
      </pre>
    </div>
  );
}

Props

PropTypeDefaultWhat it does
pressedbooleannoneControlled pressed state.
defaultPressedbooleanfalseUncontrolled starting state.
onPressedChange(pressed: boolean) => voidnoneCalled with the new state after each press.
size"s" | "m""m"Control height, matching Button at the same size.
...restComponentProps<"button">noneEvery native button attribute and ref. type defaults to button. An icon-only toggle needs an aria-label.

Anatomy

root.eq-toggleA native button with aria-pressed; square when its only child is an icon.

Keyboard

EnterSpaceToggles the pressed state.

Notes

  • The state lives in aria-pressed, so a screen reader says toggle button, pressed or not pressed; keep the label the same in both states.
  • Pressed is the solid fill with solid ink, the same as a pressed Toggle group item, so the two sit together.
  • For several related toggles in a row, use Toggle group, which adds a toolbar and arrow-key movement.
  • A toggle is not a form control and does not submit; for a setting saved with a form, use a switch or a checkbox.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/toggle.tsx49 lines
"use client";

import { useState } from "react";
import type { ComponentProps, MouseEvent } from "react";
import "./toggle.css";

export type ToggleProps = ComponentProps<"button"> & {
  /** Controlled pressed state. */
  pressed?: boolean;
  /** Uncontrolled starting state. */
  defaultPressed?: boolean;
  /** Called with the new state after a press. */
  onPressedChange?: (pressed: boolean) => void;
  size?: "s" | "m";
};

export function Toggle({
  pressed,
  defaultPressed = false,
  onPressedChange,
  size = "m",
  className,
  onClick,
  children,
  ...rest
}: ToggleProps) {
  const [inner, setInner] = useState(defaultPressed);
  const on = pressed ?? inner;

  function handleClick(event: MouseEvent<HTMLButtonElement>) {
    onClick?.(event);
    if (event.defaultPrevented) return;
    if (pressed === undefined) setInner(!on);
    onPressedChange?.(!on);
  }

  return (
    <button
      type="button"
      {...rest}
      className={["eq-toggle", className].filter(Boolean).join(" ")}
      data-size={size}
      aria-pressed={on}
      onClick={handleClick}
    >
      {children}
    </button>
  );
}
components/equip/toggle.css79 lines
@layer equip {
  /* The same item as a toggle group's, standing alone: so it carries its own border. */
  .eq-toggle {
    box-sizing: border-box;
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    gap: var(--eq-space-2);
    min-width: var(--eq-h-m);
    height: var(--eq-h-m);
    margin: 0;
    padding: 0 var(--eq-space-3);
    border: 1px solid var(--eq-line-strong);
    border-radius: var(--eq-radius-m);
    background: var(--eq-raised);
    color: var(--eq-ink-2);
    font: var(--eq-weight-strong) var(--eq-text-m) / 1 var(--eq-font);
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
    transition:
      background-color var(--eq-dur) var(--eq-ease),
      border-color var(--eq-dur) var(--eq-ease),
      color var(--eq-dur) var(--eq-ease),
      scale var(--eq-dur) var(--eq-ease);
  }

  .eq-toggle[data-size="s"] {
    gap: var(--eq-space-1);
    min-width: var(--eq-h-s);
    height: var(--eq-h-s);
    padding-inline: var(--eq-space-2);
    border-radius: var(--eq-radius-s);
    font-size: var(--eq-text-s);
  }

  /* An icon-only toggle is square. */
  .eq-toggle:has(> svg:only-child) {
    padding: 0;
  }

  .eq-toggle > svg {
    flex: none;
    width: 16px;
    height: 16px;
  }

  @media (hover: hover) {
    .eq-toggle:hover:not(:disabled, [aria-pressed="true"]) {
      background: color-mix(in srgb, var(--eq-raised), var(--eq-ink) 6%);
      color: var(--eq-ink);
    }

    .eq-toggle[aria-pressed="true"]:hover:not(:disabled) {
      background: color-mix(in srgb, var(--eq-solid) 88%, var(--eq-solid-ink));
    }
  }

  .eq-toggle:active:not(:disabled) {
    scale: var(--eq-press);
  }

  .eq-toggle[aria-pressed="true"] {
    border-color: var(--eq-solid);
    background: var(--eq-solid);
    color: var(--eq-solid-ink);
  }

  .eq-toggle:focus-visible {
    outline: 2px solid var(--eq-focus);
    outline-offset: 2px;
  }

  .eq-toggle:disabled {
    opacity: 0.45;
    cursor: not-allowed;
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index