Thinking on ways to solve TOASTS
Aug 15, 2023
Thinking on ways to solve TOASTS
In today’s GUI Challenge, @AdamArgyleInk shares thinking on a way to build toasts, a non-interactive and passive component for UI to provide user feedback. Explore the [output] element, CSS grid, animations and the FLIP technique which helps build an adaptive and accessible component. Chapters: 0:00 - Introduction 0:23 - Overview 2:02 - [output] 3:00 - Layout 4:41 - Animation 5:50 - JavaScript Part 1 6:55 - Reduced Motion 7:43 - Keyframes 8:09 - JavaScript Part 2 10:48 - F.L.I.P. 14:18 - Outro Resources: Read along → https://goo.gle/3GghBfI Try a demo → https://goo.gle/3EHsuXG Get the source → https://goo.gle/3oykceU FLIP → https://goo.gle/3oz8P6D Paul Lewis (from SuperCharged) → https://goo.gle/31IOFy0 Watch more GUI Challenges → https://goo.gle/GUIchallenges Subscribe to Google Chrome Developers → https://goo.gle/ChromeDevs #GUIchallenges #CSS #ChromeDeveloper
Content
0.16 -> welcome to another episode of gui
1.52 -> challenges where i build interface is my
3.52 -> way then i challenge you to do it your
5.44 -> way because with our creative minds
7.12 -> combined we will find multiple ways to
8.88 -> solve these interfaces and expand the
10.8 -> diversity of our skills
13.04 -> yo lucas hit in that intro
17.83 -> [Music]
23.359 -> thank you for that robo intro today's
25.6 -> episode is all about toasts here we are
28 -> oh here let's cast more spells ice four
30.16 -> ice three dark five uh these are just
32.239 -> fun that's like just to show that you
34 -> know you're gonna want to cast more than
35.28 -> one spell if you're casting spells and
37.04 -> so toasts better be able to handle that
39.44 -> uh you're gonna cast spells until you
40.8 -> run out of magic points right i mean
42 -> that's that's what i do um but here
43.68 -> here's some more realistic toasts oh
45.52 -> well that's not one multi-line support
47.039 -> is just a proof of like value but look
49.12 -> there was saved okay yeah that's nice
50.8 -> add it to favorites i've definitely seen
52.48 -> that toast before add it to cart that's
54.719 -> nice this is good stuff to just sort of
56.16 -> like passively tell the user a little
57.6 -> bit of feedback coming from the um
60.399 -> the system that you've built let's test
62.559 -> out the light theme the light theme you
64.479 -> know of course it handles the same stuff
66.96 -> it's a little bit transparent as you can
68.479 -> see there notice that it doesn't get
70 -> interrupted here so i can't click them
71.52 -> i'm using pointer events none to do that
73.52 -> i'm using that on the container did you
75.04 -> see where the container was hey here's
76.96 -> body what is this doing up here above
79.6 -> the body what is that oh we'll talk
81.28 -> about that here soon let's first reduce
83.28 -> some motion
84.479 -> got some reduced motion and we fade in
87.04 -> and we fade out
88.96 -> we fade in
90.24 -> and we fade out
92.72 -> also if we look in the console
94.64 -> we can see that we're getting an event
96.24 -> well we're console logging once it's all
98 -> done so here if i cast a spell lit four
101.6 -> ah it goes away and we console logs so
103.92 -> that's because we have we're returning a
105.52 -> promise from the toast and we can await
107.68 -> that promise for when it resolves and
109.36 -> know that a toast has shown and go away
112.24 -> right super cool let's go back to
115.119 -> uh motion yeah we want some motion and
117.119 -> let's go back to the dark mode
119.68 -> cool okay i want to show you a couple
121.119 -> more things
122.64 -> if we come into the elements panel like
124 -> we're looking at before hit escape to
125.6 -> close that console let's cast a bunch of
127.6 -> spells
128.879 -> go to the animations panel and pause
130.479 -> them now we can go back to the elements
132.56 -> panel and see all of these without them
134.48 -> disappearing ah isn't that just neat and
137.599 -> look at what i have chosen here i've
139.12 -> chosen the output element
141.28 -> the output element i learned from user
143.36 -> feedback in previous gui challenges that
145.2 -> this will be announced to screen readers
146.64 -> by default and furthermore if you add
148.879 -> role status onto it you ensure that
150.879 -> older browsers that don't implicitly
153.04 -> give an output the role status will put
155.04 -> role status on this element anyway and
156.879 -> announce it to users so you kind of get
158.16 -> a win-win old new browsers all with one
160.319 -> element
161.28 -> so now whenever you throw an output
163.12 -> element onto the page it will be
164.8 -> announced to the user also if you change
166.879 -> the value of one it'll be announced to
168.319 -> the user as well
169.68 -> so that was the way that i chose to
172.239 -> choose my semantic markup interact with
174.8 -> screen readers was all through this one
176.56 -> element and let's see what else can we
178.48 -> talk about in here and while we have it
180.239 -> frozen let's look at our layout so we
181.92 -> have this gui toast group it's position
183.84 -> fixed so you can see it's pinned down to
185.76 -> the bottom of the viewport we again are
187.68 -> above the body so we only need to use
189.76 -> z-index one we shouldn't really be
191.68 -> competing with other elements our only
193.2 -> other sibling to compute events is the
195.12 -> body tag and are you putting z-index
197.36 -> really big on that i don't know maybe
198.72 -> you are at which point it would compete
200.72 -> but anyway kind of nice it's in this
202.319 -> like safety zone where it's just above
204.159 -> the body and it's in a really nice page
205.76 -> level location don't have to battle with
207.68 -> a whole bunch of z index we pin it to
209.44 -> the bottom with inset block end we fill
211.36 -> the width with um
213.44 -> inset inline so it pins the left and the
215.28 -> right to zeros give it some padding on
217.04 -> the bottom there so that's why all of
218.319 -> them are padded the same way as they
220.4 -> enter into the viewport and then we set
222.48 -> justify items to justify content center
224.72 -> so if i take out just five items you can
226.48 -> see that the content is centered but
228 -> each item isn't individually they're
229.519 -> stretching to fit well it says hey go go
231.44 -> ahead and stretch to or
233.04 -> you know bring your intrinsic size here
234.879 -> and let's just align into the center and
236.319 -> justify content yeah it puts the content
238.239 -> in the center there
239.519 -> we have gap because that's just nice to
241.28 -> have between all of our toasts and we're
242.72 -> doing all of that layout positioning
244.239 -> with grid and then as each toast comes
246.799 -> in let's look at a toast
248.959 -> there's just a regular font family set
250.879 -> on it a background color and a text
252.72 -> color this is what we flip in the dark
255.04 -> and the light theme so that we have a
256.239 -> dark and a light version pretty simple
257.6 -> version uh max inline size using a
259.84 -> calculation here so it never goes above
261.68 -> 25 characters or never above 90 of the
264.479 -> viewport width and that's kind of nice
266.32 -> is now that even like a tiny mobile if
268.16 -> it's using a really wide toast you're
270.32 -> not going to have a bad layout we give
272 -> it a little bit of padding on blocks so
273.36 -> that's like top and bottom padding in
274.72 -> line like left and right border radius
276.479 -> so three pixels so just a subtle little
278.8 -> curve on there as you can see border a
280.96 -> font size of one rim and then we have an
282.72 -> animation that specifies three
284.8 -> animations in a comma separated list we
286.8 -> have fade in over 0.3 seconds slide in
289.6 -> over 0.3 seconds and a fade out that
291.68 -> waits a duration and the duration is 3
294.479 -> seconds and so that's why we fade in and
296.24 -> slide in right away and then we wait
298.32 -> three seconds and then we fade out so
300.08 -> that entire toast life cycle is handled
302.24 -> on this toast element itself and the
304.24 -> gooey toast group is the one that slides
306.08 -> up and down as we make room for other
307.919 -> elements
308.96 -> so we'll go ahead and play that out and
310.08 -> let those go right so here's the slide
312.16 -> in we can even see here that the gooey
314.16 -> toast animation well let's uh
316.8 -> let's play and pause well i wanted to
318.479 -> grab the handle but anyway we can see
320.479 -> that that's the animation handling oh
322.4 -> that's not really what i want is it so
324.32 -> like let's check here
325.759 -> there's a handle i want oh i can't see
327.919 -> the animation play but that's okay you
329.199 -> can see that our timeline here shows the
330.639 -> fade and slide in a weight of three
332.72 -> seconds and then a play of fade out this
335.199 -> particular animation on section gui
336.72 -> toast is coming from web animations api
338.56 -> and that's what slides up the whole
339.759 -> container meanwhile a toast is going to
342.8 -> fade in and slide in itself and it gives
344.8 -> us that cool little look
346.32 -> as we do this
347.84 -> all right kind of neat so um let's dive
350.72 -> into some of the javascript that powers
353.84 -> this and a little bit more of the css
356.08 -> in my code editor this is the html we've
358.72 -> got two buttons and that's it and notice
360.88 -> there's no element here between the head
362.24 -> and the body yet that's going to get
363.68 -> inserted from javascript when javascript
366.96 -> imports toast from toast and we have our
369.28 -> spells button here looking for clicks
370.96 -> casting a random spell here's our
372.56 -> console log proof so look we're waiting
374.56 -> for a click we have an asynchronous
376.72 -> callback function that can then await
378.56 -> the promise return from a toast and
380.639 -> console log when it's all done
382.56 -> super nice little setup and here's one
384.08 -> that just sends them all asynchronously
385.6 -> and doesn't do anything uh about the
387.44 -> weight and these are just the random
388.96 -> action and a couple assistive functions
391.12 -> down here to make that sort of random as
392.88 -> we're pushing buttons
394.639 -> up here in the styles here's our gooey
396.24 -> toast group our gooey toast that we were
397.919 -> just looking at but let's look at gooey
399.6 -> toast a little bit closer because
401.199 -> there's some cool tricks in here so we
402.88 -> saw that the duration was being used to
405.759 -> save how long the toast was being shown
408.16 -> bg lightness is how we're flipping the
410.319 -> values of the background colors we're
412.16 -> just toggling the lightness in here it's
413.759 -> kind of nice and travel distance this
415.6 -> one's really
416.72 -> fun the travel distance is initially set
418.639 -> to zero we're assuming that motion is
420.56 -> not okay and if motion is okay we allow
423.84 -> it to travel a distance and that's so
425.759 -> that this keyframe down here no matter
427.599 -> what it's going to run called slide in
430.319 -> but maybe it doesn't go anywhere if
432.16 -> there is no travel if travel distance is
434.16 -> set to zero which is the default it
435.84 -> won't slide up at all we set a
438.16 -> value here of 10 pixels in case like
439.759 -> this property didn't get set but anyway
441.36 -> that's not really super important what's
442.72 -> important here is that the keyframes
444.639 -> have no idea whether or not motion is on
446.88 -> or not they don't really care what we're
448.72 -> going to do is we're going to toggle how
450.08 -> far it moves and it if it's okay to move
452.88 -> stuff right if motion is okay we'll move
455.039 -> stuff
455.84 -> and i thought that was a really nice way
456.88 -> to handle reduced motion is to just kind
458.88 -> of bake it in a way that nothing really
460.8 -> knows that it's being toggled and i
462.479 -> thought that was kind of nice here's our
464.4 -> animation that we're looking at earlier
466.24 -> fade in slide in and fade out and here's
469.12 -> the keyframes fade in from opacity zero
473.84 -> fade out to opacity zero
476.16 -> and slide in from translate y so maybe
478.639 -> slide in from your same position of zero
481.12 -> or maybe slide in from five viewport
483.68 -> heights away and we get a nice kind of
485.68 -> exaggerated slide in that way so i
487.44 -> thought that was neat i wanted to share
488.879 -> those pieces but essentially the rest of
491.52 -> this conversation needs to be in the
493.68 -> javascript at a high level we have an
495.759 -> initialization function which is going
497.44 -> to run when the module is used we have a
500.08 -> create toast function an add toast
502.08 -> function the main toast function that we
504.4 -> export here as default and then a
506.24 -> function called flip toast which is not
508.479 -> uh like flipping toast well you know
510.24 -> like on a griddle or something like that
511.919 -> this is flip first last invert play and
516.24 -> let's we'll dive into that i'm excited
517.839 -> about that all right let's just look
519.279 -> here at a high level we have our main
520.719 -> function that we're exporting so what
522.159 -> happens when somebody like over here
524.48 -> calls toast and they put a random spell
526.72 -> string in there that string comes in as
529.04 -> text here
530.16 -> we create a toast so let's go look at
532.32 -> create toast it creates an element
534.959 -> called an output gives it the text adds
537.04 -> a class and sets the attribute role
539.36 -> status returns the element okay so we've
541.68 -> created a toast it's now here as this
543.519 -> toast variable we add that toast all
545.68 -> right let's look at add toast checks to
547.519 -> see if motion is okay so we're looking
549.2 -> at the user preference then we say here
551.8 -> toaster.children.length so is this
553.839 -> greater than zero is there children in
556.24 -> the container and is motion okay if
558.56 -> that's cool we're gonna flip that toast
560.56 -> otherwise just append that toast uh to
562.64 -> the group and it's all good and we'll
565.12 -> talk a little bit more about this but
566.32 -> essentially this is going to be where
567.76 -> we're choosing to animate if it's okay
570.48 -> uh the the
572 -> appearance of the grid you know making
574.48 -> room for the new toast in there anyway
576.32 -> we'll get into that and that's all
577.6 -> handled here in add toast
579.6 -> and next is we return a new promise from
581.839 -> this toast function and we're going to
584.56 -> have an asynchronous callback where
586.32 -> we're going to get two of the promis
588.399 -> functions given to us which is resolve
590.08 -> and reject and we're going to call
591.839 -> resolve here at the very end but what
593.92 -> we're going to do next is so we have an
595.36 -> asynchronous function we're going to
596.72 -> await the animations on this toast right
599.92 -> we have three keyframe animations fade
602 -> in fade out and slide in once those have
604.48 -> all completed we want to know so
607.44 -> we're going to say promise.all settled
609.44 -> which takes an array of promises we're
611.44 -> going to map each of the animations on a
614.079 -> toast
615.12 -> here's each animation we're going to
616.8 -> return the finished promise so this
618.64 -> essentially returns an array of three
620.399 -> finish promises given for the three
622.48 -> keyframe animations in there and we're
624.88 -> awaiting that all settled so when all
626.88 -> three are done and resolved we can
628.959 -> safely move on to the next line of code
630.56 -> here where we remove the child toast
632.48 -> from the dom and then we resolve the
634.16 -> promise at which point we come back into
636.24 -> our code over here and we can console
638 -> log poof because we awaited the promise
640.079 -> that came from them
642.24 -> that was pretty intense that's a lot of
643.839 -> the
644.64 -> bread and butter of what's going on here
646.32 -> but let's talk next about flip toast
648.399 -> because that was a really fun
650.079 -> feature of this
651.92 -> toast system here so hopefully you know
654.32 -> who paul lewis is he's been
656.399 -> a very famous front end developer at
658.079 -> google for a long time and he came up
660 -> with this flip technique where you can
661.92 -> essentially
663.839 -> animate things from an old position to a
666.16 -> new position in a scenario where it's
668.399 -> very dynamic so right now we're adding
670.16 -> toasts let's go back to our code really
672.16 -> quick
673.04 -> we're adding toasts and they can be
674.64 -> multiline ah thank you that was really
676.24 -> aptly timed
677.44 -> now that means we can't assume we know
679.12 -> the height of these which also means we
680.72 -> can't just animate the height of
682 -> something and we don't have height 100
683.92 -> animations and so what we really need to
685.92 -> do is
687.36 -> slide up the entire group from the
689.68 -> original position it's like here
692.32 -> let me use actually pause let's see if i
693.839 -> can just pause these and try to project
695.839 -> a little bit what's happening here so
696.959 -> here's the first position
699.2 -> right let's go back to the code as well
701.76 -> when the flip toast
703.519 -> function is called it's given a toast
705.2 -> element and it goes and grabs the
707.36 -> current position of the toaster and
709.6 -> let's go here that would be this group
711.68 -> so notice its height and its position
713.519 -> and it's it's got a
715.12 -> position globally in the page and we go
717.44 -> ask for that this offset height
719.519 -> then we add the element into the
721.519 -> container which is going to make the
722.959 -> grid resize and
725.04 -> instantly snap to a new location
727.279 -> at which point we go ask for its offset
730.24 -> height again the last position so this
732.72 -> is its new position this is its old
734.48 -> position what we do here is we find the
736.24 -> delta so we look for the invert we take
738.16 -> the last minus the first what's the
740.079 -> difference between its old location and
741.839 -> the new location and we create an
743.279 -> animation from the old one
745.6 -> to the new one which is just zero it's
747.279 -> just in its current place that it's now
749.36 -> at right we set it here we gave it a new
751.76 -> uh layout
753.04 -> we've measured and now we're going to
754.48 -> create an animation in that same frame
756.24 -> that takes it from an old position to a
758.88 -> new position over a duration of 150
760.8 -> seconds milliseconds and ease out and we
763.12 -> set the start time equal to document
764.959 -> timeline.current time this fixes a
766.959 -> firefox bug but what this specifically
768.88 -> does is this tells the animation to
770.32 -> begin immediately don't wait for the
772 -> next frame and that's important because
773.68 -> we're kind of playing a little game here
775.6 -> with our first last invert these things
777.92 -> offset height causes a reflow so this is
780.48 -> a kind of expensive property to ask for
782.16 -> and the browser has to do a little bit
783.44 -> of work to get there and you pay a price
785.279 -> which is like a frame it has to draw the
787.2 -> page and we append the child just going
789.279 -> to draw well this also draws so we just
791.519 -> cause like three draws um
793.76 -> and then we're going to do some math and
794.959 -> cause an animation that we don't want to
796.56 -> wait a frame we want it to happen right
797.92 -> away so we set the start time to the
800 -> current timeline in the document and
802.079 -> that is how flip works so essentially we
804.16 -> take oh let's go back to our example
805.44 -> here here's our first position
807.6 -> let's let it play through or add another
809.2 -> one and then we have a new position here
812.16 -> and what we did is i wonder if these
813.76 -> animations will even show us
815.44 -> i don't think so really
816.88 -> we faked it so we we immediately put the
819.2 -> element into the new position and then
820.56 -> scooted it back and slid it back into
822.639 -> its new spot it's very tricky um in
825.36 -> terms of like the illusion that it
826.72 -> creates but it is not quite
828.959 -> too much to handle over here i thought
830.32 -> this was pretty simple code to follow
832.16 -> and we got a really cool effect so that
833.839 -> allowed us to dynamically slide from the
836.88 -> um old layout of grid to a new layout
839.519 -> and you know right grid is managing the
841.199 -> gap grid is doing all the hard work of
842.639 -> the layout and we don't have to do any
844 -> of that all we need to do is tie into
845.92 -> its sort of life cycle and make sure it
847.92 -> has the appearance of sliding up and
849.92 -> back out
850.959 -> and i thought that was kind of cool so
852.399 -> that's how that is handled and that's
854.32 -> why it also respects reduced motion and
856.48 -> all that sort of good stuff
858.32 -> have fun casting spells have fun making
860.56 -> your own toasts i hope you enjoyed this
862.56 -> episode of gooey challenges and i'll see
864.399 -> you later y'all
866.58 -> [Music]
876.88 -> you
Source: https://www.youtube.com/watch?v=R75ZVW4LW5o