Skip to content

[Proposal]: Enhanced animated UI for fan control #17

Description

@LeonGrim2024

User problem

Currently, the fan control interface is quite primitive, relying on standard static buttons. It lacks intuitive visual feedback and an engaging design. Users would greatly benefit from a more colorful, interactive interface that visually reflects the fan's real-world state rather than just displaying flat buttons.

Proposed scope

I propose implementing a more interactive UI layout (or adding it as a visual option for the card). As a proof of concept, I have built a custom Lovelace layout in Home Assistant using custom:button-card.

Key features of this concept:

A dedicated fan image that dynamically rotates using CSS animations when the device is ON.

Dynamic styling (background colors and brightness) that changes based on the currently selected speed.

A compact, intuitive grid grouping speed presets (HI, MED, LOW, STOP) and a LIGHT toggle directly next to the fan animation.

This approach makes the interface much more attractive and user-friendly. I am attaching screenshots of the current vs. proposed look, along with my YAML code for reference.

type: grid
columns: 2
square: false
cards:

  • type: custom:button-card
    entity: input_select.michal_fan_speed
    show_name: false
    show_icon: false
    extra_styles: >-
    @Keyframes rotating { from { transform: rotate(0deg); } to { transform:
    rotate(360deg); } }
    custom_fields:
    fan_image: |
    [[[
    const isOn = entity.state !== 'off';
    const anim = isOn ? 'rotating 2s linear infinite' : 'none';
    const filt = isOn ? 'brightness(1.3)' : 'brightness(0.55) grayscale(0.4)';
    return '';
    ]]]
    speed_label: |
    [[[
    return entity.state.toUpperCase();
    ]]]
    styles:
    card:
    - aspect-ratio: 1 / 1
    - border-radius: 16px
    - padding: 0px
    grid:
    - grid-template-areas: '"fan_image" "speed_label"'
    - grid-template-rows: 1fr auto
    custom_fields:
    fan_image:
    - display: flex
    - justify-content: center
    - align-items: center
    - width: 100%
    speed_label:
    - color: white
    - font-size: 14px
    - text-align: center
    - padding-bottom: 8px
    state:
    • value: 'off'
      styles:
      card:
      - background: rgba(35, 45, 65, 0.9)
      - box-shadow: none
    • value: low
      styles:
      card:
      - background: rgba(0, 60, 35, 0.85)
      - box-shadow: 0 0 30px rgba(0, 230, 118, 0.5)
    • value: med
      styles:
      card:
      - background: rgba(0, 60, 35, 0.85)
      - box-shadow: 0 0 30px rgba(0, 230, 118, 0.5)
    • value: hi
      styles:
      card:
      - background: rgba(0, 60, 35, 0.85)
      - box-shadow: 0 0 30px rgba(0, 230, 118, 0.5)
      tap_action:
      action: none
      hold_action:
      action: more-info
  • type: grid
    columns: 1
    square: false
    cards:
    • type: custom:button-card
      name: HI
      styles:
      card:
      - height: 30px
      - font-size: 16px
      - border-radius: 10px
      - background: |
      [[[
      return states['input_select.michal_fan_speed'].state === 'hi' ? '#0a4a8a' : '#1a2540';
      ]]]
      name:
      - color: white
      tap_action:
      action: call-service
      service: button.press
      target:
      entity_id: button.michal_fan_hi
    • type: custom:button-card
      name: MED
      styles:
      card:
      - height: 30px
      - font-size: 16px
      - border-radius: 10px
      - background: |
      [[[
      return states['input_select.michal_fan_speed'].state === 'med' ? '#0a4a8a' : '#1a2540';
      ]]]
      name:
      - color: white
      tap_action:
      action: call-service
      service: button.press
      target:
      entity_id: button.michal_fan_med
    • type: custom:button-card
      name: LOW
      styles:
      card:
      - height: 30px
      - font-size: 16px
      - border-radius: 10px
      - background: |
      [[[
      return states['input_select.michal_fan_speed'].state === 'low' ? '#0a4a8a' : '#1a2540';
      ]]]
      name:
      - color: white
      tap_action:
      action: call-service
      service: button.press
      target:
      entity_id: button.michal_fan_low
    • type: custom:button-card
      name: STOP
      styles:
      card:
      - height: 30px
      - font-size: 16px
      - border-radius: 10px
      - background: |
      [[[
      return states['input_select.michal_fan_speed'].state === 'off' ? '#8a0a0a' : '#1a2540';
      ]]]
      name:
      - color: white
      tap_action:
      action: call-service
      service: button.press
      target:
      entity_id: button.michal_fan_stop
    • type: custom:button-card
      entity: binary_sensor.michal_lamp_on_off_state_michal_lamp_on_off_state
      name: LIGHT
      icon: mdi:lightbulb
      styles:
      card:
      - height: 30px
      - font-size: 16px
      - border-radius: 10px
      - background: |
      [[[
      return entity.state === 'on' ? '#ffd700' : '#1a2540';
      ]]]
      name:
      - color: white
      icon:
      - color: |
      [[[
      return entity.state === 'on' ? '#1a2540' : '#ffd700';
      ]]]
      tap_action:
      action: call-service
      service: button.press
      target:
      entity_id: button.michal_fan_light

Explicit non-goals

This proposal will not alter the underlying backend logic, API communication, or state management of the integration.

It does not introduce support for new hardware devices or protocols.

It is strictly focused on frontend UI/UX enhancements and does not force this view on users who prefer the default minimal card.

Image Image

Technical approach

Modify the frontend Lovelace card component.

Introduce CSS animations (e.g., @Keyframes rotating) tied to the fan's active state.

Use conditional styling to change colors and brightness based on the current speed attribute.

No backend contracts, integration architecture, or persistent data will be affected.

Verification plan

Manually add the proposed UI card to a Home Assistant test dashboard.

Use Developer Tools -> States to toggle the fan entity state between 'on' and 'off' to confirm the CSS rotation animation starts and stops correctly.

Click each UI button (HI, MED, LOW, STOP, LIGHT) and verify via the Home Assistant Logbook that the correct button.press or service calls are triggered.

Confirm that the active state styling (background colors and brightness) updates correctly when the fan speed attribute changes.

Project boundaries

  • I reviewed the current architecture, interaction and performance documentation.
  • I understand that beta stability work takes priority over new features.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttriageNeeds maintainer classification or scope decision

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions