Created Jun 2025
Kanban Task Board in CSS Grid (Part 2)
Based on Kanban Task Board in CSS Grid
...but with a dynamic set of states and columns.
- A PHP array contains the names and values
- Uses a
select
element to take less space (thanks Marius Gundersen for the suggestion) :checked
for inputs to store state can cover either a hidden input or an option with the value- A SASS map contains the names and colors
- SASS functions generate the selectors to generate the
:has()
conditions - Specific accessibility accommodations during state changes are not in this demo at this time. Moving elements on user change while that element is focused constitutes a potential usability and accessibility issue.
- Still no JavaScript, but to store to the backend one would need to add some form actions
On Deck
In Progress
Blocked
Review
Done
Task 1
Task 2
Task 3
Task 4
Task 5
Task 6
Task 7
Task 8
Task 9
Task 10
Task 11
Task 12
Task 13
Task 14
Task 15
Task 16
Task 17
Task 18
Task 19
Task 20
Task 21
Task 22
Task 23
Task 24
Task 25
Task 26
Task 27
Task 28
Task 29
Task 30
HTML
<main class="task-board-2">
<?php
$states = [
[
'name' => 'On Deck',
'class' => 'on-deck',
],
[
'name' => 'In Progress',
'class' => 'in-progress',
],
[
'name' => 'Blocked',
'class' => 'blocked',
],
[
'name' => 'Review',
'class' => 'review',
],
[
'name' => 'Done',
'class' => 'done',
]
];
// Print each task state as a header
foreach ($states as $state) {
printf(
'<h2 class="%s">%s</h2>',
htmlspecialchars($state['class'], ENT_QUOTES, 'UTF-8'),
htmlspecialchars($state['name'], ENT_QUOTES, 'UTF-8')
);
}
$number_of_tasks = 30;
$selectOptions = '';
foreach ($states as $state) {
$selectOptions .= sprintf(
'<option value="%s">%s</option>',
htmlspecialchars($state['class'], ENT_QUOTES, 'UTF-8'),
htmlspecialchars($state['name'], ENT_QUOTES, 'UTF-8')
);
}
// Print each task as a div with a form and select with options
for ($i = 1; $i <= $number_of_tasks; $i++) {
$select = '<select name="task-' . $i . '">';
$select .= $selectOptions;
$select .= '</select>';
printf(
'<div>
Task %d
<form>
%s
</form>
</div>', $i, $select
);
}
?>
</main>
SCSS
@use "sass:map";
@use "sass:math";
$states: (
"on-deck": #ffbaba,
"in-progress": #ffff00,
"blocked": #ffd17b,
"review": #ceffce,
"done": #d0d0ff
);
:root {
--color-default: map.get($states, "on-deck");
@each $name, $color in $states {
--color-#{$name}: #{$color};
}
}
body {
font-family: system-ui, sans-serif;
}
code {
font-size: 1rem;
}
main.task-board-2 {
display: grid;
gap: 1.2rem;
padding: 0.5rem;
grid-template-columns: 1fr;
& > form {
}
h2 {
display: none;
}
div {
--color: var(--color-default);
border: 1px solid oklch(from var(--color) 0.7 c h);
border-radius: 0.5em;
background: var(--color);
padding: 1rem;
@each $name, $color in $states {
&:has([value="#{$name}"]:checked) {
--color: var(--color-#{$name});
}
}
form {
margin: 1rem 0 0 auto;
opacity: 0.8;
display: flex;
gap: 0;
border: 1px solid oklch(from var(--color) 0.7 c h);
border-radius: 0.2rem;
width: min-content;
label {
cursor: pointer;
font-size: small;
white-space: nowrap;
padding: 0.2rem 0.5rem;
--actionColor: #0000;
background: color-mix(in srgb, var(--actionColor), var(--color));
@each $name, $color in $states {
&:has(input[value="#{$name}"]) {
--actionColor: var(--color-#{$name});
}
}
&:has(input:checked) {
opacity: 0.5;
}
input {
display: none;
}
}
}
}
}
@media only screen and (min-width: 800px) {
main.task-board-2 {
// sass to dynamically set the number of columns
grid-template-columns: repeat(length($states), 1fr);
grid-auto-flow: column;
$color_states: '';
// assemble a dynamic gradient for the background
@each $name, $color in $states {
$color_mix_with_name_and_percentage: color-mix(in srgb, var(--color-#{$name}), #0000 60%) 0;
$percentage: math.div(100%, length($states)) * index($states, ($name $color));
$color_states: #{$color_states}, #{$color_mix_with_name_and_percentage} #{$percentage},
}
background: linear-gradient(
90deg,
$color_states
);
h2 {
display: block;
margin: 0;
border: solid;
padding: 0 0 0.2ch;
border-width: 0 0 0.2ch 0;
@each $name, $color in $states {
&.#{$name} {
grid-column: index(($states), ($name $color));
}
}
}
div {
grid-column: 1;
@each $name, $color in $states {
&:has([value="#{$name}"]:checked) {
grid-column: index(($states), ($name $color));
}
}
form {
label {
width: 100%;
}
}
}
}
}
Shoutout
...to carrvo over on the indieweb for inspiring my investigation of this layout.