A Practical Guide to Progressive Enhancement in 2023

Previously, we discussed the fact that JavaScript likely fails on about 3% of all visits to your website, and how this gradually draws down your users’ “reservoir of goodwill.” I made some big claims about progressive enhancement — in particular, that it’s a form of technical credit that will make your website effectively unbreakable for no additional cost, but that will give you a lot of long-term benefits. Maybe you’d like me to dive into some technical details to back up a claim like that. Or maybe you’re convinced, but you’re just not sure what it means to develop a website this way — and in particular, how to do so with modern JavaScript, when so many of the tutorials on modern JavaScript take the “standard” approach for granted. Either way, this is the article for you.

What’s your favorite Pokémon?

A few months ago, I was applying for a job, and they gave me an assignment with which they’d assess my abilities as a front-end developer. They asked me to create an app on which users could mark their favorite Pokémon. The data came from PokéAPI. To make this really useful, you’d probably want to add authentication so that users can bring up their favorite Pokémon across different devices, but that wasn’t part of the assignment. I just used localStorage.

PokéAPI provides an endpoint that gives us the names of all of our Pokémon (along with their Pokédex numbers), but that’s all it gives us. I wanted something a little meatier: I wanted pictures and types, at the very least. PokéAPI does provide that information, but it’s in a separate endpoint that you call per Pokémon. In its fair use policy, PokéAPI asks us not to slam it with calls. Fair enough, right? So, I used a custom hook that fired when the Pokémon listing appeared on the screen. The first time it did so, I made a request for its data from PokéAPI. I also set it to remember that it had tried once before, so it wouldn’t try again. We still might end up sending thousands of calls, but only if a user was scrolling down the page — and hopefully, at least most of the time, taking some time to actually look at the content would space those calls out.

I’ve uploaded it to my website so that you can try it out for yourself, and the source code is on Github.

So, having done this little React app already, I figured I’d use this for our example of how to make a site that progressively enhances, too. That way, we can compare and contrast the two approaches.

Also, Pokémon totally get progressive enhancement. (Image: Nintendo / The Pokemon Company)

What’s the core experience?

In progressive enhancement, we begin with the core experience. Then, we add everything else as another layer of possible enhancement, which users might or might not receive. That means that the essential question at the beginning of any project where we want to pursue progressive enhancement is: what is the core experience?

Initially, I thought that being able to mark, unmark, and view your favorites was the core of the experience. That led me down a path of how I was going to persist that data. With the original assignment, I could just use localStorage, but to provide that from the back-end would require persisting data, which would also require authentication of one sort or another so the server could tell whose data was whose. This wouldn’t be an apples-to-apples comparison, then, because we’re defining the “core experience” as something more than what the original provides in total.

If you’ve ever been in discussions to define a minimum viable product (MVP), this might sound all too familiar. Product managers often fall into the trap of mistaking “viable” here to mean “commercially viable,” as in, a product that can fully deliver on customers’ expectations. This is where we get MVPs that take months to complete. “Viable” here is more in the sense of “a thing that someone can use,” not “a thing that lots of people would be willing to pay money for.” If we wait until we have the latter, we’ve waited far too long. We could’ve had something simpler out there in the world, in front of users, gathering feedback for months.

I’d fallen for the same trap. Yes, being able to mark, unmark, and view your favorite Pokémon is the value proposition of this app, but that doesn’t necessarily make it the core experience. If that’s an enhancement that some people get, and the core experience is just seeing a list of all the Pokémon with their pictures and types, is that something useful by itself, without the favoriting? Yeah, of course, it is! So that’s our core experience. Our value proposition is an enhancement. That’s going to sound really odd to product designers, but the key here is that enhancement doesn’t mean unimportant. Enhancements can be critically important. The question is, if it doesn’t happen, is there any value at all? If we can provide something of value to our users without this thing, that doesn’t make the thing unimportant, but it does mean it’s not part of the core experience.

API First

Progressive enhancement works really well with API-first development. With big projects, I usually like to create an API first, then a design system, and finally the actual server that users make requests from. At that point in the process, the server is a pretty thin layer that doesn’t do much more than take user requests, make API calls, format responses with markup from the design system, and return that markup to the user (that is, your server just became the first of your new API’s hopefully many consumers).

In this case, we’re pulling all of our data from PokéAPI, but we still have PokéAPI’s fair use policy. We can’t do just-in-time loading as users scroll, because our server will be providing a full page. PokéAPI asks that we cache our requests, so if we were building this out as a big project, we might write an API with robust caching, which fetches the data from PokéAPI under the right conditions, and then serves that data in a format more amenable to our needs.

This project is closer to a toy than an enterprise product, and we’ve got a limited set of data to consider, so I wrote a script that collected the data I wanted from PokéAPI and dumped it into a JSON file. Is this cheating? Maybe, kinda, but the thing is, if we were to go to the trouble of writing an API, the main thing we’d want from it would be an endpoint that provides precisely this data. This isn’t a tutorial about building API’s, though, it’s a tutorial on writing websites that progressively enhance, and here’s an important lesson for that: there is no such thing as cheating. The goal is to provide the best experience for users, and if there’s an easier way to do that, so much the better.

Where to Build

In both versions, I used Typescript and test-driven development (TDD). I linted with ts-standard to enforce JavaScript Standard Style. I wrote a lot of tiny functions so that they were more modular and easy to test. I hate CSS-in-JS, so in both, I set up Sass stylesheets to provide proper separation of concerns. In both, I strived for the most concise, semantic markup that I could write. That didn’t change between the two approaches — those are the things they both had in common.

With the progressively enhanced version, I did have to think about where I was going to build things. We already talked about the biggest topic, defining the core experience. With that decision made, the server side was relatively straightforward. I used Express and EJS to serve one page that sorted Pokémon by Pokédex, and another that sorted them by name.

For the enhancements, I built a structure that would work well as a general-purpose progressive enhancement loader, though it might be a bit overkill for this project:

In /client/src/index.ts, you’ll find:

import { selector as faveSelector } from './enhancements/fave/index'

(async (): Promise<void> => {
const enhancements = {
fave: Array.from(document.querySelectorAll(faveSelector))
}

if (enhancements.fave.length > 0) {
const init = await import('./enhancements/fave/index')
init.default(enhancements.fave)
}
})().then(() => {}).catch(() => {})

This expects enhancements to each have a primary module which exports a selector and an initialization function (as its default export). The selector should search for elements on the page that signal that the enhancement is called for. If any exist, then the initialization function is dynamically imported and called. This helps with code splitting when we use tools like webpack. On large projects, we may have hundreds of enhancements, but any given page may only use one or two of them. Dynamic imports means that each page will only load the enhancements that it actually needs, so we’re not wasting the user’s time or bandwidth with all of the enhancements that a page never called for.

In this project, we only have one enhancement, and every page calls for it, so this is a bit of overkill, but it doesn’t cost us much and it provides an example of how you’d usually want to do this.

If you dig into that initialization script, you can see that it sets up everything for favoriting, including adding a star to each Pokémon. Here’s a great example of choosing where to build things. We could have included the stars in our EJS templates, after all. If JavaScript fails, the stars won’t do anything, since we decided to build favoriting as a client-side enhancement. If a user clicks on it, nothing will happen. That’s likely to cause confusion and frustration.

The initialization function can only fire after most of the dangers that could disrupt our JavaScript have passed. When it adds a star to each Pokémon, it also creates the event listener and sets up everything we need to follow through on user interaction. We also bundle the “Faves Only” link with the initializer for the same reason. It only appears in the navigation if it will do something useful. We bundle all of it together so that it stands or falls together as a logical unit. You’ll never get broken pieces of an experience; for each enhancement, you either get it or you don’t.

You won’t find any code that’s been copied for front- and back-end use. In fact, you won’t find any shared code, either. You might end up using some (Typescript interfaces spring to mind as particularly likely suspects), so I wouldn’t say it’s necessarily a bad practice, but it does have a bit of a smell. Client-side JavaScript should, as a rule, be operating on the markup. If you need to share code between the front-end and the back-end, is it possible that’s because there’s something that you should’ve put in the markup but didn’t?

Like the React version, I’ve uploaded a static rendering of the progressively enhanced version to my website, and you can look through the source code on Github.

Comparison

Since we started by talking about what happens when JavaScript fails, here’s a side-by-side comparison of what the two versions look like when that happens:

What the two versions look like when JavaScript fails. React version on the left, progressive enhancement version on the right.

Neither one provides you with the ability to mark, unmark, or view your favorite Pokémon, so in either case, a JavaScript failure means losing our app’s primary value proposition. I would venture that the React version is quite a bit worse, though. The blank screen is confusing. A user who receives this page (and again, we can expect this to be what our users see about 3% of the time) is left without any indication of what’s going on or why. The progressively-enhanced version doesn’t deliver our primary value proposition, but it does provide some value: a complete list of Pokémon, with names, types, and pictures. Even without the primary draw that we designed for, this is still a useful page.

I wouldn’t say that simply counting lines of code is by any means a perfect way of assessing how complex a code base is, but it is a general indicator for a reason — so what about here? I wrote both projects, with the same idiosyncrasies and personal preferences in both, which should rule out a number of variables that normally confound such comparisons. How many lines of code are in each?

The React version is 34,864 lines of code; the progressive enhancement version is 18,601. So by lines of code, the progressive enhancement version is almost half (53%) the size of the React version.

Both versions were a little shy of a full day’s work (though in both cases I broke those hours up across a few days). I think a straight side-by-side comparison of time spent would be unfair in the progressively enhanced version’s favor, simply because I did it second. Every project is research more than anything else; we learn how to do this specific project. And by doing the progressively-enhanced version second, I did inevitably take lessons learned from the first attempt with React, which of course made the second attempt faster.

The “Faves Only” link flashing in with the progressively-enhanced version is annoying. Were I to do it again, I’d probably load a disabled version of the link in the template, and have the enhancement activate it. The React version is annoying in the flash of the Pokéball default and untyped color when each new Pokémon scrolls into view as we wait for its request to the PokéAPI to load. Plus, if you scroll too fast, lots of PokéAPI requests can be dropped, leaving many Pokémon in a permanent state. So they both have rough edges that could definitely be sanded off.

Conclusions

I think these two projects can provide some great examples for developers used to working with React or similar JavaScript frameworks of the similarities and differences when working with progressive enhancement. We see a good illustration here that, when we compare apples to apples in terms of good code and architecture, progressive enhancement is at least no greater an investment of development time and effort than the “standard” approach, while the benefits are substantial and ongoing. We’ve also seen some great examples of some of the primary principles of developing for progressive enhancement, including:

  • The core experience is all of the value that you can provide when everything else breaks. For most webpages, if the server breaks, you’re not going to be providing anything. What if the server is the only thing working, and everything else breaks? How many things do you need to work in order to provide any value at all? Whatever value you can provide when that’s all you have, that’s your core experience.
  • Enhancements are important! Your primary value proposition might be an enhancement! That doesn’t mean it’s not important. It means that there’s value that you can provide even if this part doesn’t work for some reason. Providing some value is better than providing none, even when it isn’t everything you’d hope for.
  • If it can be pushed to the server, it’s probably better there. A server does the computation once, and probably with better hardware, rather than asking all of your users to do the same computations on every load on whatever device they happen to be bringing to the task.
  • Bundle enhancements into logical groups. Enhancements should load themselves onto the page with everything they need, so if something goes wrong, users aren’t left with a junkyard of detritus for enhancements they’re not getting.
  • Progressive enhancement doesn’t change any of the other rules of good software engineering: TDD, DRY, loosely-coupled components — all those things are still important. If you’re rewriting code for the front- or back-end, something has gone very wrong. That usually means that it’s something that should be on the back-end (see note above), perhaps in some form that the front-end can call on (like an API endpoint).

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

Learn more


A Practical Guide to Progressive Enhancement in 2023 was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Bits and Pieces - Medium and was authored by Jason Godesky

Previously, we discussed the fact that JavaScript likely fails on about 3% of all visits to your website, and how this gradually draws down your users’ “reservoir of goodwill.” I made some big claims about progressive enhancement — in particular, that it’s a form of technical credit that will make your website effectively unbreakable for no additional cost, but that will give you a lot of long-term benefits. Maybe you’d like me to dive into some technical details to back up a claim like that. Or maybe you’re convinced, but you’re just not sure what it means to develop a website this way — and in particular, how to do so with modern JavaScript, when so many of the tutorials on modern JavaScript take the “standard” approach for granted. Either way, this is the article for you.

What’s your favorite Pokémon?

A few months ago, I was applying for a job, and they gave me an assignment with which they’d assess my abilities as a front-end developer. They asked me to create an app on which users could mark their favorite Pokémon. The data came from PokéAPI. To make this really useful, you’d probably want to add authentication so that users can bring up their favorite Pokémon across different devices, but that wasn’t part of the assignment. I just used localStorage.

PokéAPI provides an endpoint that gives us the names of all of our Pokémon (along with their Pokédex numbers), but that’s all it gives us. I wanted something a little meatier: I wanted pictures and types, at the very least. PokéAPI does provide that information, but it’s in a separate endpoint that you call per Pokémon. In its fair use policy, PokéAPI asks us not to slam it with calls. Fair enough, right? So, I used a custom hook that fired when the Pokémon listing appeared on the screen. The first time it did so, I made a request for its data from PokéAPI. I also set it to remember that it had tried once before, so it wouldn’t try again. We still might end up sending thousands of calls, but only if a user was scrolling down the page — and hopefully, at least most of the time, taking some time to actually look at the content would space those calls out.

I’ve uploaded it to my website so that you can try it out for yourself, and the source code is on Github.

So, having done this little React app already, I figured I’d use this for our example of how to make a site that progressively enhances, too. That way, we can compare and contrast the two approaches.

Also, Pokémon totally get progressive enhancement. (Image: Nintendo / The Pokemon Company)

What’s the core experience?

In progressive enhancement, we begin with the core experience. Then, we add everything else as another layer of possible enhancement, which users might or might not receive. That means that the essential question at the beginning of any project where we want to pursue progressive enhancement is: what is the core experience?

Initially, I thought that being able to mark, unmark, and view your favorites was the core of the experience. That led me down a path of how I was going to persist that data. With the original assignment, I could just use localStorage, but to provide that from the back-end would require persisting data, which would also require authentication of one sort or another so the server could tell whose data was whose. This wouldn’t be an apples-to-apples comparison, then, because we’re defining the “core experience” as something more than what the original provides in total.

If you’ve ever been in discussions to define a minimum viable product (MVP), this might sound all too familiar. Product managers often fall into the trap of mistaking “viable” here to mean “commercially viable,” as in, a product that can fully deliver on customers’ expectations. This is where we get MVPs that take months to complete. “Viable” here is more in the sense of “a thing that someone can use,” not “a thing that lots of people would be willing to pay money for.” If we wait until we have the latter, we’ve waited far too long. We could’ve had something simpler out there in the world, in front of users, gathering feedback for months.

I’d fallen for the same trap. Yes, being able to mark, unmark, and view your favorite Pokémon is the value proposition of this app, but that doesn’t necessarily make it the core experience. If that’s an enhancement that some people get, and the core experience is just seeing a list of all the Pokémon with their pictures and types, is that something useful by itself, without the favoriting? Yeah, of course, it is! So that’s our core experience. Our value proposition is an enhancement. That’s going to sound really odd to product designers, but the key here is that enhancement doesn’t mean unimportant. Enhancements can be critically important. The question is, if it doesn’t happen, is there any value at all? If we can provide something of value to our users without this thing, that doesn’t make the thing unimportant, but it does mean it’s not part of the core experience.

API First

Progressive enhancement works really well with API-first development. With big projects, I usually like to create an API first, then a design system, and finally the actual server that users make requests from. At that point in the process, the server is a pretty thin layer that doesn’t do much more than take user requests, make API calls, format responses with markup from the design system, and return that markup to the user (that is, your server just became the first of your new API’s hopefully many consumers).

In this case, we’re pulling all of our data from PokéAPI, but we still have PokéAPI’s fair use policy. We can’t do just-in-time loading as users scroll, because our server will be providing a full page. PokéAPI asks that we cache our requests, so if we were building this out as a big project, we might write an API with robust caching, which fetches the data from PokéAPI under the right conditions, and then serves that data in a format more amenable to our needs.

This project is closer to a toy than an enterprise product, and we’ve got a limited set of data to consider, so I wrote a script that collected the data I wanted from PokéAPI and dumped it into a JSON file. Is this cheating? Maybe, kinda, but the thing is, if we were to go to the trouble of writing an API, the main thing we’d want from it would be an endpoint that provides precisely this data. This isn’t a tutorial about building API’s, though, it’s a tutorial on writing websites that progressively enhance, and here’s an important lesson for that: there is no such thing as cheating. The goal is to provide the best experience for users, and if there’s an easier way to do that, so much the better.

Where to Build

In both versions, I used Typescript and test-driven development (TDD). I linted with ts-standard to enforce JavaScript Standard Style. I wrote a lot of tiny functions so that they were more modular and easy to test. I hate CSS-in-JS, so in both, I set up Sass stylesheets to provide proper separation of concerns. In both, I strived for the most concise, semantic markup that I could write. That didn’t change between the two approaches — those are the things they both had in common.

With the progressively enhanced version, I did have to think about where I was going to build things. We already talked about the biggest topic, defining the core experience. With that decision made, the server side was relatively straightforward. I used Express and EJS to serve one page that sorted Pokémon by Pokédex, and another that sorted them by name.

For the enhancements, I built a structure that would work well as a general-purpose progressive enhancement loader, though it might be a bit overkill for this project:

In /client/src/index.ts, you’ll find:

import { selector as faveSelector } from './enhancements/fave/index'

(async (): Promise<void> => {
const enhancements = {
fave: Array.from(document.querySelectorAll(faveSelector))
}

if (enhancements.fave.length > 0) {
const init = await import('./enhancements/fave/index')
init.default(enhancements.fave)
}
})().then(() => {}).catch(() => {})

This expects enhancements to each have a primary module which exports a selector and an initialization function (as its default export). The selector should search for elements on the page that signal that the enhancement is called for. If any exist, then the initialization function is dynamically imported and called. This helps with code splitting when we use tools like webpack. On large projects, we may have hundreds of enhancements, but any given page may only use one or two of them. Dynamic imports means that each page will only load the enhancements that it actually needs, so we’re not wasting the user’s time or bandwidth with all of the enhancements that a page never called for.

In this project, we only have one enhancement, and every page calls for it, so this is a bit of overkill, but it doesn’t cost us much and it provides an example of how you’d usually want to do this.

If you dig into that initialization script, you can see that it sets up everything for favoriting, including adding a star to each Pokémon. Here’s a great example of choosing where to build things. We could have included the stars in our EJS templates, after all. If JavaScript fails, the stars won’t do anything, since we decided to build favoriting as a client-side enhancement. If a user clicks on it, nothing will happen. That’s likely to cause confusion and frustration.

The initialization function can only fire after most of the dangers that could disrupt our JavaScript have passed. When it adds a star to each Pokémon, it also creates the event listener and sets up everything we need to follow through on user interaction. We also bundle the “Faves Only” link with the initializer for the same reason. It only appears in the navigation if it will do something useful. We bundle all of it together so that it stands or falls together as a logical unit. You’ll never get broken pieces of an experience; for each enhancement, you either get it or you don’t.

You won’t find any code that’s been copied for front- and back-end use. In fact, you won’t find any shared code, either. You might end up using some (Typescript interfaces spring to mind as particularly likely suspects), so I wouldn’t say it’s necessarily a bad practice, but it does have a bit of a smell. Client-side JavaScript should, as a rule, be operating on the markup. If you need to share code between the front-end and the back-end, is it possible that’s because there’s something that you should’ve put in the markup but didn’t?

Like the React version, I’ve uploaded a static rendering of the progressively enhanced version to my website, and you can look through the source code on Github.

Comparison

Since we started by talking about what happens when JavaScript fails, here’s a side-by-side comparison of what the two versions look like when that happens:

What the two versions look like when JavaScript fails. React version on the left, progressive enhancement version on the right.

Neither one provides you with the ability to mark, unmark, or view your favorite Pokémon, so in either case, a JavaScript failure means losing our app’s primary value proposition. I would venture that the React version is quite a bit worse, though. The blank screen is confusing. A user who receives this page (and again, we can expect this to be what our users see about 3% of the time) is left without any indication of what’s going on or why. The progressively-enhanced version doesn’t deliver our primary value proposition, but it does provide some value: a complete list of Pokémon, with names, types, and pictures. Even without the primary draw that we designed for, this is still a useful page.

I wouldn’t say that simply counting lines of code is by any means a perfect way of assessing how complex a code base is, but it is a general indicator for a reason — so what about here? I wrote both projects, with the same idiosyncrasies and personal preferences in both, which should rule out a number of variables that normally confound such comparisons. How many lines of code are in each?

The React version is 34,864 lines of code; the progressive enhancement version is 18,601. So by lines of code, the progressive enhancement version is almost half (53%) the size of the React version.

Both versions were a little shy of a full day’s work (though in both cases I broke those hours up across a few days). I think a straight side-by-side comparison of time spent would be unfair in the progressively enhanced version’s favor, simply because I did it second. Every project is research more than anything else; we learn how to do this specific project. And by doing the progressively-enhanced version second, I did inevitably take lessons learned from the first attempt with React, which of course made the second attempt faster.

The “Faves Only” link flashing in with the progressively-enhanced version is annoying. Were I to do it again, I’d probably load a disabled version of the link in the template, and have the enhancement activate it. The React version is annoying in the flash of the Pokéball default and untyped color when each new Pokémon scrolls into view as we wait for its request to the PokéAPI to load. Plus, if you scroll too fast, lots of PokéAPI requests can be dropped, leaving many Pokémon in a permanent state. So they both have rough edges that could definitely be sanded off.

Conclusions

I think these two projects can provide some great examples for developers used to working with React or similar JavaScript frameworks of the similarities and differences when working with progressive enhancement. We see a good illustration here that, when we compare apples to apples in terms of good code and architecture, progressive enhancement is at least no greater an investment of development time and effort than the “standard” approach, while the benefits are substantial and ongoing. We’ve also seen some great examples of some of the primary principles of developing for progressive enhancement, including:

  • The core experience is all of the value that you can provide when everything else breaks. For most webpages, if the server breaks, you’re not going to be providing anything. What if the server is the only thing working, and everything else breaks? How many things do you need to work in order to provide any value at all? Whatever value you can provide when that’s all you have, that’s your core experience.
  • Enhancements are important! Your primary value proposition might be an enhancement! That doesn’t mean it’s not important. It means that there’s value that you can provide even if this part doesn’t work for some reason. Providing some value is better than providing none, even when it isn’t everything you’d hope for.
  • If it can be pushed to the server, it’s probably better there. A server does the computation once, and probably with better hardware, rather than asking all of your users to do the same computations on every load on whatever device they happen to be bringing to the task.
  • Bundle enhancements into logical groups. Enhancements should load themselves onto the page with everything they need, so if something goes wrong, users aren’t left with a junkyard of detritus for enhancements they’re not getting.
  • Progressive enhancement doesn’t change any of the other rules of good software engineering: TDD, DRY, loosely-coupled components — all those things are still important. If you’re rewriting code for the front- or back-end, something has gone very wrong. That usually means that it’s something that should be on the back-end (see note above), perhaps in some form that the front-end can call on (like an API endpoint).

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

Learn more


A Practical Guide to Progressive Enhancement in 2023 was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Bits and Pieces - Medium and was authored by Jason Godesky


Print Share Comment Cite Upload Translate Updates
APA

Jason Godesky | Sciencx (2023-03-13T07:27:28+00:00) A Practical Guide to Progressive Enhancement in 2023. Retrieved from https://www.scien.cx/2023/03/13/a-practical-guide-to-progressive-enhancement-in-2023/

MLA
" » A Practical Guide to Progressive Enhancement in 2023." Jason Godesky | Sciencx - Monday March 13, 2023, https://www.scien.cx/2023/03/13/a-practical-guide-to-progressive-enhancement-in-2023/
HARVARD
Jason Godesky | Sciencx Monday March 13, 2023 » A Practical Guide to Progressive Enhancement in 2023., viewed ,<https://www.scien.cx/2023/03/13/a-practical-guide-to-progressive-enhancement-in-2023/>
VANCOUVER
Jason Godesky | Sciencx - » A Practical Guide to Progressive Enhancement in 2023. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/03/13/a-practical-guide-to-progressive-enhancement-in-2023/
CHICAGO
" » A Practical Guide to Progressive Enhancement in 2023." Jason Godesky | Sciencx - Accessed . https://www.scien.cx/2023/03/13/a-practical-guide-to-progressive-enhancement-in-2023/
IEEE
" » A Practical Guide to Progressive Enhancement in 2023." Jason Godesky | Sciencx [Online]. Available: https://www.scien.cx/2023/03/13/a-practical-guide-to-progressive-enhancement-in-2023/. [Accessed: ]
rf:citation
» A Practical Guide to Progressive Enhancement in 2023 | Jason Godesky | Sciencx | https://www.scien.cx/2023/03/13/a-practical-guide-to-progressive-enhancement-in-2023/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.