'buttons in a grid with arrows in gaps
I have buttons that are displayed on a css grid. I would like to put arrows in the gaps that will be like a flow chart. If not possible using css grid can think of something that can implement using another technique. This is stackblitz which will show my implementation without the arrows in between. https://stackblitz.com/edit/angular-wipq2r-miu8fl?file=src%2Fapp%2Fproduct-list%2Fproduct-list.component.html
Solution 1:[1]
I would create classes that can be applied to each "step" depending on which way the arrow needs to point (up, down, left, right) and then use an ::after pseudo selector to create the arrow element on each class, styling as required.
See below.
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
grid-gap: 20px;
}
.step {
background-color: blue;
color: white;
padding: 10px;
position: relative;
}
.arrow-right::after {
color: black;
content: '?';
font-size: 20px;
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
}
.arrow-down::after {
color: black;
content: '?';
font-size: 16px;
position: absolute;
left: 50%;
top: 100%;
transform: translateX(-50%);
}
.arrow-up::after {
color: black;
content: '?';
font-size: 16px;
position: absolute;
left: 50%;
bottom: 100%;
transform: translateX(-50%);
}
.arrow-left::after {
color: black;
content: '?';
font-size: 20px;
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
}
<div class="grid">
<div class="step arrow-right">Step X</div>
<div class="step arrow-down">Step X</div>
<div class="step">Step X</div>
<div class="step">Step X</div>
<div class="step arrow-down">Step X</div>
<div class="step">Step X</div>
<div class="step arrow-up">Step X</div>
<div class="step arrow-left">Step X</div>
<div class="step">Step X</div>
</div>
Solution 2:[2]
As the arrows are just visual clues we can use pseudo elements to depict them.
You can 'draw' the lines and arrow heads by putting a pseudo after element on an item with background color black and cutting it using clip-path.
We need two clip-paths, one for the horizontal lines/arrows and one for the one going from the end item in each row to the first of the next row.
This snippet uses settings of some CSS variables to make it easier to change to suit a particular use case.
Here is the result:
* {
margin: 0;
padding;
0;
box-sizing: border-box;
}
.container {
--gap: 10vmin;
/* set this to what you want the gap to be - absolute units */
display: grid;
gap: var(--gap);
grid-template-columns: 1fr 1fr 1fr;
width: 100vw;
/* set this to whatever you want */
padding: var(--gap);
}
.container>* {
border: solid 1px black;
width: 100%;
--ar: 3 / 1;
/* set this to what you want the aspect ratio of a button to be */
aspect-ratio: var(--ar);
position: relative;
}
.container>*::after {
content: '';
width: var(--gap);
--arrow: 20px;
/* set this to the height of the arrowhead */
--ha: calc(var(--arrow) / 2);
/* half the height of an arrowhead */
--line: 2px;
/* set this to the width (height) of the lines */
--hl: calc(var(--line) / 2);
/* half the height of a line */
height: var(--arrow);
background-color: black;
clip-path: polygon(0 calc(var(--ha) - var(--hl)), calc(100% - var(--ha)) calc(var(--ha) - var(--hl)), calc(100% - var(--ha)) 0, 100% 50%, calc(100% - var(--ha)) 100%, calc(100% - var(--ha)) calc(var(--ha) + var(--hl)), 0 calc(var(--ha) + var(--hl)));
position: absolute;
top: 50%;
transform: translateY(-50%);
right: calc(-1 * var(--gap));
}
.container>*:nth-child(3n)::after {
width: calc(200% + (2 * var(--gap)) + var(--line));
height: var(--gap);
transform: translateY(0);
top: 100%;
right: calc(50% - (var(--hl)));
clip-path: polygon( 100% 0, 100% calc(50% + var(--hl)), calc(var(--ha) + var(--hl)) calc(50% + var(--hl)), calc(var(--ha) + var(--hl)) calc(100% - var(--ha)), var(--arrow) calc(100% - var(--ha)), var(--ha) 100%, 0 calc(100% - var(--ha)), calc(var(--ha) - var(--hl)) calc(100% - var(--ha)), calc(var(--ha) - var(--hl)) calc(50% - var(--hl)), calc(100% - var(--line)) calc(50% - var(--hl)), calc(100% - var(--line)) 0);
}
.container>*:last-child::after {
display: none;
}
<div class="container">
<button></button><button></button><button></button><button></button><button></button><button></button><button></button><button></button><button></button>
</div>
Solution 3:[3]
Another approach is use SVG
Update 2 When we calculate the position we need susbstrat from element.getBoundingClientRect(), the getBoundingClientRect().top and getBoundingClientRect().left of the "wrapper"
The idea is put all under a wrapper div
<div #wrapper class="wrapper">
<svg [attr.width]="size.width" [attr.height]="size.height"
xmlns="http://www.w3.org/2000/svg">
<path *ngFor="let path of paths" [attr.d]="path" />
</svg>
<div #bt *ngFor="let item of procDesc; let i = index" class="step">
..your buttons..
</div>
</div>
You get the elements using viewChild and ViewChildren and declare an array of "paths" and an object with the size of the wrapper
@ViewChildren('bt') items:QueryList<ElementRef>
@ViewChild('wrapper') wrapper:ElementRef
paths:string[]=[]
size={width:0,height:0}
Then, when you resize (I use fromEvent rxjs operator in the stackblitz )
ngOnInit()
{
this.subscription=fromEvent(window,'resize').pipe(
startWith(null),
debounceTime(200)
).subscribe(_=>{
setTimeout(()=>{
this.paths=this.createPath()
const rect=this.wrapper.nativeElement.getBoundingClientRect()
this.size={width:rect.width,height:rect.height}
})
})
}
ngOnDestroy(){
this.subscription.unsubscribe()
}
The function createPath is like
createPath()
{
const path:string[]=[]
const add=.5; //if stroke-width is even use add=.5 else use add=0
//get the position of "wrapper"
const wrapper=this.wrapper.nativeElement.getBoundingClientRect()
this.items.forEach((x,i)=>{
if (i)
{
/* replace this lines
const ini=this.items.find((_,index)=>index==i-1)
.nativeElement.getBoundingClientRect()
const fin=x.nativeElement.getBoundingClientRect()
*/
//by
const _ini=this.items.find((_,index)=>index==i-1)
.nativeElement.getBoundingClientRect()
const _fin=x.nativeElement.getBoundingClientRect()
const ini={width:_ini.width,height:_ini.height,
left:_ini.left+window.scrollX,top:_ini.top+window.scrollY}
const fin={width:_fin.width,height:_fin.height,
left:_fin.left+window.scrollX,top:_fin.top+window.scrollY}
const _ini=this.items.find((_,index)=>index==i-1)
.nativeElement.getBoundingClientRect()
const _fin=x.nativeElement.getBoundingClientRect()
const ini={width:_ini.width,height:_ini.height,
left:_ini.left-wrapper.left,top:_ini.top-wrapper.top}
const fin={width:_fin.width,height:_fin.height,
left:_fin.left-wrapper.left,top:_fin.top-wrapper.top}
if (ini.top==fin.top)
{
path.push(`M${ini.left+ini.width+add} ${ini.top+ini.height/2+add}
H${fin.left-add}
M${fin.left-7-add} ${fin.top+fin.height/2-4-add}
L${fin.left-add} ${fin.top+fin.height/2+add}
M${fin.left-7-add} ${fin.top+fin.height/2+4+add}
L${fin.left-add} ${fin.top+fin.height/2+add}`)
}
else
{
const step=6; //(fin.top-ini.top-ini.height)/2
path.push(`M${ini.left+ini.width/2+add} ${ini.top+ini.height+add}
V${ini.top+ini.height+step+add}
H${fin.left+fin.width/2+add}
V${fin.top-add}
M${fin.left+fin.width/2+4+add} ${fin.top-7-add}
L${fin.left+fin.width/2+add} ${fin.top-add}
M${fin.left+fin.width/2-4-add} ${fin.top-7-add}
L${fin.left+fin.width/2+add} ${fin.top-add}`
)
}
}
})
return path
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | cam |
| Solution 2 | A Haworth |
| Solution 3 |


