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.
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
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:
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:
styles:
card:
- background: rgba(35, 45, 65, 0.9)
- box-shadow: none
styles:
card:
- background: rgba(0, 60, 35, 0.85)
- box-shadow: 0 0 30px rgba(0, 230, 118, 0.5)
styles:
card:
- background: rgba(0, 60, 35, 0.85)
- box-shadow: 0 0 30px rgba(0, 230, 118, 0.5)
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
columns: 1
square: false
cards:
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
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
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
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
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.
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