Skip to content

Control

Source

Example

html
<div class="x-control">
    <input type="text">
</div>

More examples can be found in the Winduum docs.

Example CSS

css
@import "winduum/src/components/control/props/color.css" layer(theme);
@import "winduum/src/components/control/props/default.css" layer(theme);
@import "winduum/src/components/control/props/floating.css" layer(theme);
@import "winduum/src/components/control/props/icon.css" layer(theme);
@import "winduum/src/components/control/props/select-picker.css" layer(theme);
@import "winduum/src/components/control/props/select.css" layer(theme);
@import "winduum/src/components/control/color.css" layer(utilities);
@import "winduum/src/components/control/default.css" layer(utilities);
@import "winduum/src/components/control/file.css" layer(utilities);
@import "winduum/src/components/control/floating-interactive.css" layer(utilities);
@import "winduum/src/components/control/floating.css" layer(utilities);
@import "winduum/src/components/control/icon.css" layer(utilities);
@import "winduum/src/components/control/interactive.css" layer(utilities);
@import "winduum/src/components/control/invalid.css" layer(utilities);
@import "winduum/src/components/control/select-multiple.css" layer(utilities);
@import "winduum/src/components/control/select-picker.css" layer(utilities);
@import "winduum/src/components/control/select.css" layer(utilities);

/* Example customization */
@layer theme {
  :root, :host {
    --x-control-block-size: 2.5rem;
    --x-control-block-size-textarea: 2.5rem;
  }
}

Winduum CSS Code

winduum/src/components/control/props/color.css

css
:root, :host {
  --x-control-color-swatch-inline-size: 1.25rem;
  --x-control-color-swatch-block-size: 1.25rem;
  --x-control-color-swatch-border-radius: var(--radius-full);
}

winduum/src/components/control/props/default.css

css
:root, :host {
  --x-control-block-size: 3rem;
  --x-control-block-size-textarea: 8rem;
  --x-control-padding-inline: 0.75rem;
  --x-control-padding-block: 0.75rem;
  --x-control-border-width: 1px;
  --x-control-border-radius: var(--radius-xl);
  --x-control-border-color: currentColor;
  --x-control-font-weight: var(--font-weight-medium);
  --x-control-font-size: var(--text-base);
  --x-control-background-color: var(--color-body-primary);
  --x-control-color: currentColor;
  --x-control-outline-width: 3px;
  --x-control-placeholder-color: currentColor;
  --x-control-placeholder-opacity: 50%;
}

winduum/src/components/control/props/floating.css

css
:root, :host {
  --x-control-label-translate-y: 0.625rem;
  --x-control-label-scale: 0.8;
}

winduum/src/components/control/props/icon.css

css
:root, :host {
  --x-control-icon-size: 1.25rem;
  --x-control-icon-gap: 0.375rem;
}

winduum/src/components/control/props/select-picker.css

css
:root, :host {
  --x-control-select-picker-border-width: 1px;
  --x-control-select-picker-border-radius: var(--radius-lg);
  --x-control-select-picker-border-color: currentColor;
  --x-control-select-picker-background-color: var(--color-body-primary);
  --x-control-select-picker-margin-block: 0.25rem;
  --x-control-select-picker-padding-block: 0.25rem;
  --x-control-select-picker-padding-inline: 0.25rem;
  --x-control-select-picker-gap: 0.125rem;
  --x-control-select-picker-scrollbar-color: currentColor var(--color-body-primary);
  --x-control-select-option-padding-block: 0.5rem;
  --x-control-select-option-padding-inline: 0.5rem;
  --x-control-select-option-font-size: var(--text-sm);
  --x-control-select-option-font-weight: var(--font-weight-semibold);
  --x-control-select-option-line-height: calc(1em + 0.125rem);
  --x-control-select-option-border-radius: var(--radius-md);
  --x-control-select-option-hocus-background-color: var(--color-body-secondary);
}

winduum/src/components/control/props/select.css

css
:root, :host {
  --x-control-select-icon-size: 1.25rem;
  --x-control-select-icon-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke="currentColor"><path d="M10.998 15.467c.491.71 1.513.71 2.004 0l3.767-5.453c.581-.843 0-2.013-1.002-2.014H8.234C7.232 8 6.65 9.17 7.231 10.014z" /></svg>');
  --x-control-select-icon-margin-inline-end: -0.25rem;
}

winduum/src/components/control/color.css

css
.x-control {
  > :where(input) {
    &::-webkit-color-swatch-wrapper {
      inline-size: var(--x-control-color-swatch-inline-size);
      block-size: var(--x-control-color-swatch-block-size);
      padding: 0;
    }

    &::-moz-color-swatch {
      inline-size: var(--x-control-color-swatch-inline-size);
      block-size: var(--x-control-color-swatch-block-size);
      border-radius: var(--x-control-color-swatch-border-radius);
      margin: 0;
    }

    &::-webkit-color-swatch {
      border-radius: var(--x-control-color-swatch-border-radius);
    }

    &[type="color"] ~ label {
      margin-inline-start: calc(var(--x-control-padding-inline) + var(--x-control-color-swatch-inline-size));
    }
  }
}

winduum/src/components/control/default.css

css
.x-control {
  display: grid;
  block-size: var(--x-control-block-size);
  font-family: var(--x-control-font-family);
  font-weight: var(--x-control-font-weight);
  font-size: var(--x-control-font-size);
  letter-spacing: var(--x-control-letter-spacing);
  color: var(--x-control-color);
  border-radius: var(--x-control-border-radius);
  background-color:
    color-mix(
      in var(--x-control-background-color-space, srgb),
      var(--x-control-background-color) var(--x-control-background-color-opacity, 100%),
      var(--x-control-background-color-mix, transparent)
    );
  border:
    var(--x-control-border-width) solid
    color-mix(
      in var(--x-control-border-color-space, srgb),
      var(--x-control-border-color) var(--x-control-border-color-opacity, 15%),
      var(--x-control-border-color-mix, transparent)
    );
  outline:
    var(--x-control-outline-width) solid
    color-mix(
      in var(--x-control-outline-color-space, srgb),
      var(--x-control-outline-color, var(--color-accent)) var(--x-control-outline-color-opacity, 0%),
      var(--x-control-outline-color-mix, transparent)
    );
  outline-offset: var(--x-control-outline-offset);
  grid-template:
    [x-control-start] calc(var(--x-control-padding-block) - var(--x-control-border-width))
    [x-control-padding] 1fr
    calc(var(--x-control-padding-block) - var(--x-control-border-width)) [x-control-end] /
    [x-control-start] var(--x-control-padding-inline)
    [x-control-padding] 1fr
    var(--x-control-padding-inline) [x-control-end];

  &:has(textarea) {
    height: auto;
  }

  > :where(*) {
    grid-area: x-control-padding;
    align-self: center;
  }

  > :where(input, textarea, select) {
    padding-block: var(--x-control-padding-block-start, var(--x-control-padding-block)) var(--x-control-padding-block-end, var(--x-control-padding-block));
    padding-inline: calc(var(--x-control-padding-inline) + var(--x-control-padding-inline-start, 0px)) calc(var(--x-control-padding-inline) + var(--x-control-padding-inline-end, 0px));
    grid-area: x-control;
    text-overflow: ellipsis;
    overflow: clip;
    align-self: stretch;
    align-items: center;
    border-radius: inherit;
    background-color: inherit;

    &:disabled {
      cursor: not-allowed;
    }
  }

  > :where(input, select) {
    white-space: nowrap;
  }

  > :where(input, textarea) {
    &::placeholder {
      color: var(--x-control-placeholder-color);
      opacity: var(--x-control-placeholder-opacity);
    }
  }

  > :where(textarea) {
    min-block-size: var(--x-control-block-size-textarea);
    resize: vertical;
  }
}

winduum/src/components/control/file.css

css
.x-control:where(:has([type="file"])) {
  overflow: clip;

  > :where([type="file"]) {
    &::file-selector-button {
      all: unset;
      background-color:
        color-mix(
          in var(--x-control-file-background-color-space, srgb),
          var(--x-control-file-background-color, currentColor) var(--x-control-file-background-color-opacity, 15%),
          var(--x-control-file-background-color-mix, transparent)
        );
      block-size: var(--x-control-block-size);
      padding-inline: var(--x-control-padding-inline);
      margin-inline: calc(var(--x-control-padding-inline) * -1) var(--x-control-padding-inline);
      margin-block-start: calc(var(--x-control-padding-block) * -1);
      border-start-end-radius: var(--x-control-border-radius);
      border-end-end-radius: var(--x-control-border-radius);
      transition: var(--transition-background);
      cursor: var(--cursor-pointer, pointer);
    }

    &:is(:hover, :focus) {
      --x-control-file-background-color-opacity: 20%;
    }
  }
}

winduum/src/components/control/floating-interactive.css

css
.x-control:where(:has(:not([type="color"]) ~ label)) {
  --x-control-label-focus-opacity: 50%;
  --x-control-placeholder-color: transparent;

  > :where(input, textarea, select) {
    &:is(:focus, :not(:placeholder-shown)) {
      ~ label {
        transform: translateY(calc(var(--x-control-label-translate-y) * -1)) scale(var(--x-control-label-scale));
        opacity: var(--x-control-label-focus-opacity);
      }
    }
  }
}

winduum/src/components/control/floating.css

css
.x-control:where(:has(:not([type="color"]) ~ label)) {
  > :where(label) {
    margin-inline: var(--x-control-padding-inline-start, 0) var(--x-control-padding-inline-end, 0);
    transition: var(--transition-transform), var(--transition-color), var(--transition-opacity);
    pointer-events: none;
    transform-origin: 0 50%;
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: clip;
    will-change: transform;
  }

  > :where(textarea) {
    padding-block-start: calc(var(--x-control-padding-block) + var(--x-control-label-translate-y) * var(--x-control-label-scale));

    + :where(label) {
      align-self: start;
    }
  }

  > :where(input, select) {
    padding-block: calc(var(--x-control-padding-block) + var(--x-control-label-translate-y) * var(--x-control-label-scale)) calc(var(--x-control-padding-block) - var(--x-control-label-translate-y) * var(--x-control-label-scale));

    &::-webkit-calendar-picker-indicator,
    &::-webkit-search-cancel-button,
    &::-webkit-outer-spin-button,
    &::-webkit-inner-spin-button {
      transform: translateY(calc(var(--x-control-label-translate-y) * var(--x-control-label-scale) * -1));
    }
  }

  &:where(:has(:required)) :where(label)::after {
    color: var(--x-control-required-color, var(--color-error));
    content: " *";
  }
}

winduum/src/components/control/icon.css

css
.x-control {
  --x-control-start: calc(var(--x-control-icon-count-start, 1) * (var(--x-control-icon-size) + var(--x-control-icon-gap)) - var(--x-control-icon-gap));
  --x-control-end: calc(var(--x-control-icon-count-end, 1) * (var(--x-control-icon-size) + var(--x-control-icon-gap)) - var(--x-control-icon-gap));

  &:has(textarea) {
    :where(.me-auto), :where(.ms-auto) {
      align-self: start;
    }
  }

  &:has(.me-auto) {
    --x-control-padding-inline-start: calc(var(--x-control-start) + var(--x-control-padding-inline));
  }

  &:has(.ms-auto) {
    --x-control-padding-inline-end: calc(var(--x-control-end) + var(--x-control-padding-inline));
  }

  &:has(.me-auto > *:nth-child(2)) {
    --x-control-icon-count-start: 2;
  }

  &:has(.ms-auto > *:nth-child(2)) {
    --x-control-icon-count-end: 2;
  }

  :where(.me-auto), :where(.ms-auto) {
    gap: var(--x-control-icon-gap);
    display: flex;
    align-items: center;
  }
}

winduum/src/components/control/interactive.css

css
.x-control {
  transition-property: var(--default-transition-property);
  transition-timing-function: var(--ease-in-out);
  transition-duration: var(--default-transition-duration);

  &:focus-within {
    --x-control-border-color: var(--x-control-focus-border-color, var(--color-accent));
    --x-control-border-color-opacity: var(--x-control-focus-border-color-opacity, 100%);
    --x-control-outline-offset: var(--x-control-focus-outline-offset);
    --x-control-outline-color-opacity: var(--x-control-focus-outline-color-opacity, 20%);
  }

  &.disabled, &:has(:where(input, textarea, select):disabled) {
    --x-control-background-color: var(--x-control-disabled-background-color, currentColor);
    --x-control-background-color-opacity: var(--x-control-disabled-background-color-opacity, 5%);
    --x-control-border-color: var(--x-control-disabled-border-color, currentColor);
    --x-control-border-color-opacity: var(--x-control-disabled-border-color-opacity);
  }
}

winduum/src/components/control/invalid.css

css
.x-control {
  &:has(:user-invalid) {
    --x-control-background-color: var(--x-control-invalid-background-color, var(--color-error));
    --x-control-background-color-opacity: var(--x-control-invalid-background-color-opacity, 10%);
    --x-control-border-color: var(--x-control-invalid-border-color, var(--color-error));
    --x-control-border-color-opacity: var(--x-control-invalid-border-color-opacity, 100%);
    --x-control-outline-color: var(--x-control-invalid-outline-color, var(--color-error));
    --x-control-color: var(--x-control-invalid-color, var(--color-error));
  }
}

winduum/src/components/control/select-multiple.css

css
.x-control:has(select[multiple]) {
  --x-control-block-size: auto;

  select {
    overflow-y: auto;
    scrollbar-width: none;

    &::-webkit-scrollbar {
      display: none;
    }
  }
}

winduum/src/components/control/select-picker.css

css
.x-control:has(select:not([multiple])) {
  &::after {
    will-change: transform;
    transition: var(--transition-transform);
  }

  &:has(select:open) {
    &::after {
      transform: rotate(180deg);
    }
  }

  select {
    &, &::picker(select) {
      appearance: base-select;
    }

    &::picker-icon {
      display: none;
    }

    &::picker(select) {
      border-radius: var(--x-control-select-picker-border-radius);
      border:
        var(--x-control-select-picker-border-width) solid
        color-mix(
          in var(--x-control-select-picker-border-color-space, srgb),
          var(--x-control-select-picker-border-color) var(--x-control-select-picker-border-color-opacity, 15%),
          var(--x-control-select-picker-border-color-mix, transparent)
        );
      background-color: var(--x-control-select-picker-background-color);
      margin-block: var(--x-control-select-picker-margin-block);
      padding-block: var(--x-control-select-picker-padding-block);
      padding-inline: var(--x-control-select-picker-padding-inline);
      gap: var(--x-control-select-picker-gap);
      display: flex;
      flex-direction: column;
      cursor: auto;
      transform-origin: top center;
      scrollbar-width: thin;
      scrollbar-color: var(--x-control-select-picker-scrollbar-color);
      will-change: transform, opacity;
      transition:
        opacity var(--default-transition-duration) var(--ease-in-out),
        transform var(--default-transition-duration) var(--ease-out),
        display var(--default-transition-duration) allow-discrete,
        overlay var(--default-transition-duration) allow-discrete;
    }

    &:not(:open)::picker(select) {
      opacity: 0%;
      transform: scale(0.9);
      display: revert;
    }

    &:open::picker(select) {
      opacity: 100%;
      transform: scale(1);

      @starting-style {
        opacity: 0%;
        transform: scale(0.95);
      }
    }

    option {
      padding-block: var(--x-control-select-option-padding-block);
      padding-inline: var(--x-control-select-option-padding-inline);
      font-size: var(--x-control-select-option-font-size);
      font-weight: var(--x-control-select-option-font-weight);
      line-height: var(--x-control-select-option-line-height);
      border-radius: var(--x-control-select-option-border-radius);
      transition: var(--transition-background), var(--transition-color);

      &::checkmark {
        display: none;
      }

      &:hover, &:focus-visible {
        background-color: var(--x-control-select-option-hocus-background-color);
      }

      &:checked {
        color: var(--x-control-select-option-checked-color, var(--color-accent));
        background-color:
          color-mix(
            in var(--x-control-select-option-checked-background-color-space, srgb),
            var(--x-control-select-option-checked-background-color, var(--color-accent)) var(--x-control-select-option-checked-background-color-opacity, 10%),
            var(--x-control-select-option-checked-background-color-mix, transparent)
          );
      }
    }
  }
}

winduum/src/components/control/select.css

css
.x-control:has(select:not([multiple])) {
  --x-control-padding-inline-end: calc(var(--x-control-end) + var(--x-control-padding-inline) + var(--x-control-select-icon-margin-inline-end));

  &:has(.ms-auto) {
    --x-control-icon-count-end: 2;
  }

  &::after {
    inline-size: var(--x-control-select-icon-size);
    block-size: var(--x-control-select-icon-size);
    mask: var(--x-control-select-icon-mask);
    margin-inline: auto var(--x-control-select-icon-margin-inline-end);
    grid-area: x-control-padding;
    background-color: currentColor;
    pointer-events: none;
    align-self: center;
    content: "";
  }

  :where(.ms-auto) {
    margin-inline-end: var(--x-control-select-icon-size);
  }

  :where(select, option) {
    cursor: var(--cursor-pointer, pointer);
  }
}

Stimulus Actions

stepDown

Triggers native stepDown method on input.

Example

html
<div class="x-control" data-controller="x-control">
    <input type="number" min="0" value="10">
    <div class="me-auto">
        <svg role="button" data-action="click->x-control#stepDown" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
            <path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" />
        </svg>
    </div>
</div>

stepUp

Triggers native stepUp method on input.

Example

html
<div class="x-control" data-controller="x-control">
    <input type="number" min="0" value="0">
    <div class="ms-auto">
        <svg role="button" data-action="click->x-control#stepUp" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
        </svg>
    </div>
</div>

showPicker

Triggers native showPicker method on input.

Example

html
<div class="x-control" data-controller="x-control">
    <input type="date">
    <div class="ms-auto">
        <svg class="size-6" role="button" data-action="click->x-control#showPicker" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />
        </svg>
    </div>
</div>

Released under the MIT License.