Technology 2024-02-17 • By Sanjeev Kumar

The Complete Guide to convert HTML elements to Flutter Widgets

The Complete Guide to convert HTML elements to Flutter Widgets

In one of my apps, I was consuming WordPress REST APIs to display data. WordPress stores blog data in the form of HTML. So to render that data into my Flutter app I was required to use an HTML renderer package. So I choose to go with the https://pub.dev/packages/flutter_html/versions/3.0.0-beta.2/install

so installed it as suggested under description of package.

HTML content can contain any HTML tags like Images, Videos, YouTube links, tweets, and even a gallery of images. So it is good practice to render all in our app for the best experience.

So First I created a stateless widget, in which HTML content was passed, Here is the code below

class HtmlBody extends StatelessWidget {
              const HtmlBody({
                super.key,
                required this.content,
              });
              final String content;
            
              @override
              Widget build(BuildContext context) {
                return Html(
                  shrinkWrap: true,
                  data: content,
                  style: {
                    "body": Style(
                      margin: Margins.zero,
                      padding: HtmlPaddings.all(8.0),
                      fontSize: FontSize(18.0),
                      lineHeight: const LineHeight(1.5),
                      whiteSpace: WhiteSpace.normal,
                      fontWeight: FontWeight.w400,
                      color: const Color(0xFF292929),
                    ),
                    "p,h1,h2,h3,h4,h5,h6": Style(margin: Margins.all(2.00)),
                    "figure": Style(margin: Margins.zero, padding: HtmlPaddings.all(8.0)),
                  },
                  onLinkTap: (
                    String? url,
                    _,
                    __,
                  ) async {
                    if (url != null) URLLauncherUtil().launchUrl(url);
                  },
                  extensions: [
                    AppHtmlImageExtension(),
                    AppHtmlGalleryRender(),
                    AppHtmlIframeExtension(),
                  ],
                );
              }
            }
            

Handling anchor tags (links to another webpage) is pretty easy and can be handled using the url_launcher package. But the trick is to render images, galleries, tweets, and YouTube links. so here come the extensions for the rescue.

If want to handle image, pass an extension to render image and so on.

I have passed three extension, AppHtmlImageExtension(), AppHtmlGalleryRender(), AppHtmlIframeExtension() to handle image, gallery and Iframe respectively.

You can create any extension by extending the HtmlExtension class. Here is the full code of all extensions used above.

class AppHtmlIframeExtension extends HtmlExtension {
              @override
              Set<String> get supportedTags => {'iframe'};
            
              @override
              InlineSpan build(ExtensionContext context) {
                return WidgetSpan(child: returnView(context));
              }
            
              Widget returnView(ExtensionContext context) {
                final String videoSource = context.element!.attributes['src'].toString();
                if (videoSource.contains('youtube')) {
                  return GestureDetector(
                    onTap: () {
                      if (context.buildContext != null) {
                        URLLauncherUtil().launchUrl(videoSource);
                      }
                    },
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Stack(
                        alignment: Alignment.center,
                        children: [
                          CachedNetworkImage(
                            imageUrl: getYoutubeThumbnail(videoSource),
                          ),
                          SizedBox(
                              height: 80,
                              width: 120.00,
                              child: Image.asset(
                                'assets/images/youtube.png',
                                fit: BoxFit.contain,
                              )),
                        ],
                      ),
                    ),
                  );
                } else {
                  return const SizedBox.shrink();
                }
              }
            
              String getYoutubeThumbnail(String videoUrl) {
                String? videoID = getYoutubeVideoIdFromUrl(videoUrl);
                return videoID == null ? '' : 'https://img.youtube.com/vi/$videoID/0.jpg';
              }
            
              String? getYoutubeVideoIdFromUrl(String url, {bool trimWhitespaces = true}) {
                if (!url.contains("http") && (url.length == 11)) return url;
                if (trimWhitespaces) url = url.trim();
            
                for (var exp in [
                  RegExp(
                      r"^https:\/\/(?:www\.|m\.)?youtube\.com\/watch\?v=([_\-a-zA-Z0-9]{11}).*$"),
                  RegExp(
                      r"^https:\/\/(?:music\.)?youtube\.com\/watch\?v=([_\-a-zA-Z0-9]{11}).*$"),
                  RegExp(
                      r"^https:\/\/(?:www\.|m\.)?youtube\.com\/shorts\/([_\-a-zA-Z0-9]{11}).*$"),
                  RegExp(
                      r"^https:\/\/(?:www\.|m\.)?youtube(?:-nocookie)?\.com\/embed\/([_\-a-zA-Z0-9]{11}).*$"),
                  RegExp(r"^https:\/\/youtu\.be\/([_\-a-zA-Z0-9]{11}).*$")
                ]) {
                  Match? match = exp.firstMatch(url);
                  if (match != null && match.groupCount >= 1) return match.group(1);
                }
            
                return null;
              }
            }
            
            class AppHtmlImageExtension extends HtmlExtension {
              @override
              Set<String> get supportedTags => {'img'};
            
              @override
              InlineSpan build(ExtensionContext context) {
                return WidgetSpan(child: returnView(context));
              }
            
              Widget returnView(ExtensionContext context) {
                final src = context.element?.attributes['src'].toString();
                return GestureDetector(
                  onTap: () {
                    if (context.buildContext != null) {
                      Navigator.push(
                          context.buildContext!,
                          MaterialPageRoute(
                              builder: (context) => CachedPhotoView(
                                    url: src,
                                  )));
                    }
                  },
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: CachedNetworkImage(
                      imageUrl: src!,
                      placeholder: (context, url) => AspectRatio(
                        aspectRatio: 16 / 9,
                        child: Stack(
                          children: [
                            Container(
                              color: Colors.grey[400],
                            ),
                            const Center(child: CircularProgressIndicator())
                          ],
                        ),
                      ),
                    ),
                  ),
                );
              }
            }
            
            
            class AppHtmlGalleryRender extends HtmlExtension {
              @override
              Set<String> get supportedTags => {'wp-block-gallery'};
            
              @override
              InlineSpan build(ExtensionContext context) {
                return WidgetSpan(child: returnView(context));
              }
            
              @override
              bool matches(ExtensionContext context) {
                return context.element?.classes.contains('wp-block-gallery') ?? false;
              }
            
              Widget returnView(ExtensionContext context) {
                List<String> imagesUrl = [];
                final src = context.element?.children ?? [];
                imagesUrl =
                    src.map((e) => e.children.first.attributes['src'] ?? '').toList();
            
                return PostGalleryRenderer(imagesUrl: imagesUrl);
              }
            }

Render YouTube Videos Links

For the Iframe part, I handled YouTube video links. First I extracted the video id then based upon that Video ID I rendered an image and play icon on top of that to give an illusion of it as a video. I hope the code is quite self-explanatory. In case you are not getting it properly I recommend this post.

Render Images

In extension we need to extend the HtmlExtension class and override some methods like supportedTags and need to pass img. (<img src=’Image link’ />)

Then we need to get the src means source a.k.a link of the image that we want to render.

After getting the link of the image we can display it using NetworkImage or CachedNetwork widgets. Then we can handle when the Image is clicked by getting the BuildContext from the current context.

class AppHtmlImageExtension extends HtmlExtension {
              @override
              Set<String> get supportedTags => {'img'};
            
              @override
              InlineSpan build(ExtensionContext context) {
                return WidgetSpan(child: returnView(context));
              }
            
              Widget returnView(ExtensionContext context) {
                final src = context.element?.attributes['src'].toString();
                return GestureDetector(
                  onTap: () {
                    if (context.buildContext != null) {
                      Navigator.push(
                          context.buildContext!,
                          MaterialPageRoute(
                              builder: (context) => CachedPhotoView(
                                    url: src,
                                  )));
                    }
                  },
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: CachedNetworkImage(
                      imageUrl: src!,
                      placeholder: (context, url) => AspectRatio(
                        aspectRatio: 16 / 9,
                        child: Stack(
                          children: [
                            Container(
                              color: Colors.grey[400],
                            ),
                            const Center(child: CircularProgressIndicator())
                          ],
                        ),
                      ),
                    ),
                  ),
                );
              }
            }

To Render the Image gallery provided by WordPress Block Editor We need to create the extension in the same manner but this time we need to look for ‘wp-block-gallery’ tag and fetch the list of images and render them in a gridview.

class AppHtmlGalleryRender extends HtmlExtension {
              @override
              Set<String> get supportedTags => {'wp-block-gallery'};
            
              @override
              InlineSpan build(ExtensionContext context) {
                return WidgetSpan(child: returnView(context));
              }
            
              @override
              bool matches(ExtensionContext context) {
                return context.element?.classes.contains('wp-block-gallery') ?? false;
              }
            
              Widget returnView(ExtensionContext context) {
                List<String> imagesUrl = [];
                final src = context.element?.children ?? [];
                imagesUrl =
                    src.map((e) => e.children.first.attributes['src'] ?? '').toList();
            
                return PostGalleryRenderer(imagesUrl: imagesUrl);
              }
            }
SK
Written by

Sanjeev Kumar

Founder & Lead Engineer at PrepNew. Building cross-platform Flutter applications, serverless AI backends, and full-stack Dart web architectures.

Continue Reading

Related Posts

Serverless AI with Dart :  Bringing Full-Stack Power to Flutter Developers
Technology 2026-04-30

Serverless AI with Dart : Bringing Full-Stack Power to Flutter Developers

This post introduces a full-stack approach for Flutter developers by utilizing Firebase Cloud Functions written entirely in Dart. By migrating backend logic to the server, developers achieve end-to-end type safety, eliminate frontend-backend language switching

PrepNew Team Read Guide →
Innovating Fast: Ditching the Dedicated Server for Mobile App Features
Technology 2025-11-27

Innovating Fast: Ditching the Dedicated Server for Mobile App Features

As mobile app developers, we often face a dilemma: setting up and maintaining a server for a single, crucial feature can [...]

PrepNew Team Read Guide →
Make WordPress safe under 1 minute: Crafting a Script to Prep New defence Against Dummy Email Registrations
Technology 2024-03-03

Make WordPress safe under 1 minute: Crafting a Script to Prep New defence Against Dummy Email Registrations

In this tutorials We are going to talk about how to prep new defence against dummy email registrations on a [...]

PrepNew Team Read Guide →
← Explore More Technical Guides