Guide to DevLink

With DevLink, you can build components visually in Webflow for use in external React projects. The following documentation will center DevLink’s usage specifically on the Next.js React framework.

DevLink users must agree to the Webflow Labs Terms of Service.

The DevLink beta is now closed to new users. Stay tuned for updates on the future of React in Webflow.

Resources

Getting set up with DevLink

In the following instructions we will walk through how to set up a new Next.js project, and connect it to your Webflow project.

Create your Next.js project

  1. Follow the instructions here to set up a new Next.js project
  2. Run npm run dev to make sure your project runs successfully

Connect to local dev

Here’s how to connect your Webflow project with your local dev environment

  1. Create your Webflow project
  2. Create a simple component
    1. Add a button to the canvas
    2. Right click on the component and select Create component to turn it into a Component
    3. You should see the component in the Components menu
  3. In the Components menu click on the Export components button to open the Devlink config
  1. Copy the .webflowrc.js file you see in the modal (Safari users will need to click and select) and paste it into a new file in the root directory of your Next.js project
    1. Replace the “[YOUR API TOKEN]” placeholder with an API access token from your site. You can generate an API key from your project’s integration settings page on the Webflow Dashboard
      1. Alternatively you can use an env variable so that you’ll be able to include the .webflowrc.js file to GitHub without storing the token in plain text
        1. .webflowrc.js
          1module.exports = {
          2 authToken: process.env.WF_AUTH_TOKEN
          3}
        2. .env
          export WF_AUTH_TOKEN="[YOUR API KEY]"
    2. Save the file

Installing the NPM module

To install the Webflow CLI npm module run this command in the terminal in the root directory of your next.js project.

Shell
1npm i @webflow/webflow-cli

Syncing your components

To sync your Webflow components into your project run

  1. Shell
    1npx webflow devlink sync

Note: Starter plans have API limits that may shift during the open beta period.

What’s supported

Supported elements

Here’s a full list of supported Webflow elements that can be exported with DevLink:

Support for hosted capabilities

Form submissions notifications, CMS, e-commerce, memberships and logic are not supported by DevLink.

Features

Interactions

In order to use Webflow interactions in your DevLinked components you’ll need to wrap them with the DevLinkProvider.

In Next.js you can add this to the layout.tsx file so that all pages will have interactions enabled (or _app.tsx on older version of Next.js).

1// layout.tsx
2import "@/devlink/global.css";
3import { DevLinkProvider } from "@/devlink/DevLinkProvider";
4
5export default function RootLayout({
6 children,
7}: {
8 children: React.ReactNode;
9}) {
10 return (
11 <html lang="en">
12 <body>
13 <DevLinkProvider>
14 {children}
15 </DevLinkProvider>
16 </body>
17 </html>
18 );
19}

Page interactions

Interactions with page triggers are supported on DevLink components but with a caveat. Because of how page interactions work in Webflow (i.e. they’re linked to the specific page the component instance is) DevLink will exports only the first page interaction. So if a component uses multiple page interactions on different pages, only one will be exported.

Styling

Global styles

In order for your components to have access to Webflow’s global default styles you’ll need to import a css file. In Next.js you can add this to the layout.tsx file so that all pages will have global styles available (or _app.tsx on older version of Next.js).

1// layout.tsx
2
3import "@/devlink/global.css";

The global.css stylesheet contains all global style definitions of your Webflow site to be used in your DevLink project. It is exported alongside all your custom components CSS modules and contains:

  • Your custom fonts imports (e.g. @import url(...))
  • All global resets from https://github.com/necolas/normalize.css
  • Specific classes from base Webflow CSS used by all DevLink supported elements (e.g. .w-slider and .w-nav-link)
  • Form elements resets such as:
global.css
1button,
2[type='button'],
3[type='reset']{
4 border: 0;
5 cursor: pointer;
6 -webkit-appearance: button;
7}
1:root {
2 --lightPink-afebdd45: hsla(0, 100.00%, 86.00%, 1.00);
3}
  • Custom CSS tag selectors from your site (e.g. “All Links” or “Body (All Pages)” style selector):
1a {
2 font-weight: 700;
3 text-decoration: underline;
4}
5
6body {
7 font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
8}
  • If you opt-out of CSS modules, it will also contain all styles for your components.

What global.css does not contain is classes set on elements that are not part of any of your components.

Note: classes applied on the body are not included in the global.css stylesheet. If you want to apply a default style on the body so that all your components inherit from it (for example a custom font) then we recommend applying it on the “Body (all pages)” tag selector.

CSS modules

By default Webflow components use css modules to prevent component’s css classes from colliding with global styles. If you want you can disable css modules and all styles will be defined globally in global.css. You can do that by updating the cssModules flag in .webflowrc.js.

.webflowrc.js
1module.exports = {
2 cssModules: false
3}

Caveats with custom IDs

In Webflow, the Grid and Quick Stack elements rely on HTML IDs for certain styles (highlighted with a pink indicator in the Style Panel). However, it’s also possible to set a custom ID on those elements. If doing so with CSS modules enabled, the custom ID in the exported DevLink component won’t match what’s been set in the Designer. That’s because the ID needs to be adapted to work with the component CSS module. The resulting ID will have this format: <ComponentName>\_<custom-id>\_\_<unique-identifier> so if you need to target it with custom css styles you’ll have to use a wildcard selector like [id*="<ComponentName>_<custom-id>__"] { ... }.

Custom ID
Custom ID

Note: If the custom ID that uses ID styles is also bound to a component property, then there would be no simple way to reference the correct ID CSS selector in the exported DevLink component and apply styles correctly. So instead, by design, component properties are not being bound to an element custom ID if such element relies on ID styles. This is a workaround to make sure elements that rely on ID styles are styled correctly, but also a limitation to be aware of. If you need to reference a specific element with custom code we recommend using custom attributes instead.

Custom ID bound to a component property
Custom ID bound to a component property

Slots

Slots in Webflow DevLink are meant to provide the user the power of composing components within components. This is a common React.js concept that is used frequently in code bases large and small. You can read more about how slots work in React.js here.

The UI settings for slots will appear in the settings tab of the component that supports slots. You’ll be able to set a name for the slot, which is important, as that will become the name of the React.js component after you DevLink it out.

Slot component property
Slot component property

This will turn into:

1export function Layout({
2 as: Component = Block,
3 dashboardContent,
4}) {
5 return (
6 <Component>
7 {dashboardContent ?? (
8 // Default content
9 )}
10 </Component>
11 );
12}

Runtime props

Runtime props can be used to include props that are not covered by Webflow, such as event handlers, or other arbitrary props that, for example, can be provided by third party libraries. By adding a runtime props property to an element, the DevLink compiler will add a new property to your component and spread it in the element it was assigned.

You can include runtime props to your components in the settings tab under the DevLink section. Only certain elements have support for Runtime Props.

You can set Runtime Props in the settings tab after you select a supported element:

Runtime Props component property
Runtime Props component property

This will turn into:

1export function ButtonPrimary({
2 as: Component = Link,
3 buttonProps = {},
4}) {
5 return (
6 <Component button={true} {...buttonProps}>
7 Click me
8 </Component>
9 );
10}

From here, you can set event handlers on the buttonProps property.

Visibility settings

Component visibility properties are supported in DevLink. You can read about how to use them in Webflow University.

Component visibility property
Component visibility property

This will turn into:

1export function MyComponent({
2 as: Component = Block,
3 showDescription = false,
4}) {
5 return (
6 <Component>
7 {showDescription ? (
8 <_Builtin.Paragraph>
9 Lorem ipsum dolor site amet, consectetur adipiscing elit.
10 </_Builtin.Paragraph>
11 ) : null}
12 </Component>
13 );
14}

Types

All components are exported with their own declaration file (.d.ts). Using an IDE such as VSCode, autocomplete and type safety is achieved.

Forms

Webflow forms are supported with DevLink in two ways:

  • You can export an entire form as a single component and work with it as an uncontrolled React form. You will have access to the same set of functionalities as the standard form of any Webflow published site: Three simple states (Normal, Error, and Success) and simple HTML validations. We recommend this approach for simple forms.

    • To use forms this way, add a Runtime Prop to the root element of your form component and pass an onSubmit prop containing your handling function. Consider the following example with a Runtime Prop property called formProps:
      NativeFormComponent.tsx
      1import { NativeFormComponent } from "../devlink";
      2
      3export function PostsPage() {
      4 return (
      5 <NativeFormComponent
      6 // Runtime prop
      7 formProps={{
      8 /* If an exception is raised by this function,
      9 the Error state is automatically rendered.
      10 Otherwise, Success state is rendered. */
      11 onSubmit: (e: React.FormEvent<HTMLFormElement>) => {
      12 e.preventDefault();
      13 fetch("/posts", {
      14 body: new FormData(e.target),
      15 method: "POST",
      16 });
      17 },
      18 }}
      19 />
      20 );
      21}
  • Instead of exporting the entire form as a single component, you can also split it into several reusable components and have finer control over state, validation, and submission. To achieve such a result, it is necessary to use several of the previously mentioned DevLink features (Runtime Props, Slots, and Visibility Props). This approach is recommended for complex and highly interactive forms.

    • Let’s look at an example, using the react-hook-form library:
    LoginForm.tsx
    1import { Controller, useForm } from "react-hook-form";
    2import { yupResolver } from "@hookform/resolvers/yup";
    3import * as yup from "yup";
    4
    5import { Input, Button, FormWrapper } from "../devlink";
    6
    7// Define custom schema for validation
    8const schema = yup
    9 .object({
    10 email: yup.string().email().required(),
    11 password: yup.string().min(8).required(),
    12 })
    13 .required();
    14
    15export function LoginForm() {
    16 const {
    17 handleSubmit,
    18 control,
    19 formState: { errors },
    20 } = useForm({
    21 resolver: yupResolver(schema),
    22 });
    23
    24 return (
    25 // We can use the native HTML form element now that our state is controlled
    26 <form
    27 onSubmit={handleSubmit((data) => {
    28 try {
    29 await fetch("/login", {
    30 body: data,
    31 method: "POST",
    32 });
    33 alert("Success!");
    34 } catch (err) {
    35 alert("Error!");
    36 }
    37 })}
    38 >
    39 <FormWrapper
    40 // Slot
    41 formBody={
    42 <>
    43 <Controller
    44 name="email"
    45 control={control}
    46 render={({ field }) => (
    47 <Input
    48 // Text prop
    49 label="Email"
    50 // Visibility prop
    51 isError={!!errors.email}
    52 // Text prop
    53 helperMessage={errors.email?.message}
    54 // Runtime prop
    55 inputProps={{
    56 ...field,
    57 placeholder: "Your email",
    58 type: "email",
    59 "aria-invalid": errors.email ? "true" : "false",
    60 }}
    61 />
    62 )}
    63 />
    64 <Controller
    65 name="password"
    66 control={control}
    67 render={({ field }) => (
    68 <Input
    69 label="Password"
    70 isError={!!errors.password}
    71 helperMessage={errors.password?.message}
    72 inputProps={{
    73 ...field,
    74 placeholder: "Your password",
    75 type: "password",
    76 "aria-invalid": errors.password ? "true" : "false",
    77 }}
    78 />
    79 )}
    80 />
    81 <Button />
    82 </>
    83 }
    84 />
    85 </form>
    86 );
    87}

Advanced features

Certain frameworks like Next.js provide specific components that you can use in your applications like Link and Image. If you want to override DevLink’s builtin Link and Image components with your custom ones you can pass them to the DevLinkProvider with the renderLink and renderImage props.

1// app/layout.tsx
2import "@/devlink/global.css";
3import { DevLinkProvider } from "@/devlink/DevLinkProvider";
4import { LinkRenderer, ImageRenderer } from "@/components/renderers"; // My custom components
5
6export default function RootLayout({
7 children,
8}: {
9 children: React.ReactNode;
10}) {
11 return (
12 <html lang="en">
13 <body>
14 <DevLinkProvider renderLink={LinkRenderer} renderImage={ImageRenderer}>
15 {children}
16 </DevLinkProvider>
17 </body>
18 </html>
19 );
20}

Examples

In this example we created custom link and image components in a new file /components/renderers.tsx.

renderers.tsx
1"use client";
2
3import Image from "next/image";
4import Link from "next/link";
5import { RenderLink, RenderImage } from "@/devlink";
6
7export const LinkRenderer: RenderLink = ({
8 href,
9 className,
10 children,
11 ...props,
12}) => (
13 <Link href={href} className={className} {...props}>
14 {children}
15 </Link>
16);
17
18export const ImageRenderer: RenderImage = ({
19 src,
20 alt,
21 height,
22 width,
23 loading,
24 className,
25 ...props,
26}) => {
27 const imgProps = {
28 loading,
29 className,
30 src: src || "",
31 alt: alt || "",
32 width: width === "auto" ? undefined : (width as number),
33 height: height === "auto" ? undefined : (height as number),
34
35 // Note: this will fill the image to its parent element container
36 // so you'll need to style the parent container with the desired size.
37 fill: width === "auto" || height === "auto",
38 ...props,
39 };
40
41 return <Image {...imgProps} />;
42};

For the Next.js Image component to work with external urls we also have to update next.config.js (more details on their official docs).

next.config.js
1module.exports = {
2 images: {
3 images: {
4 domains: ["uploads-ssl.webflow.com"],
5 },
6 },
7};

Advanced settings

Sync a subset of your components

If you have a site with a lot of components you don’t need in your React application, you can specify a list of components to sync. Only the components matching your settings will be downloaded when running the sync command. If you have other components from previous syncs they will be deleted (essentially the whole devlink module is overwritten at every sync command).

You can either pass an allowlist of exact component names:

.webflowrc.js
1module.exports = {
2 components: ["MyComponent1", "MyComponent2"]
3}

Or a regular expression:

.webflowrc.js
1module.exports = {
2 components: "/component/i"
3}

Sync specific components

You can also pass a list of exact component names as an argument when running the sync command.

Shell
1npx webflow devlink sync MyComponent1 MyComponent2

When syncing a specific component like this, the devlink module is not overwritten and all other components will not be deleted.

Crash reports

When something goes wrong and the sync command fails, you will be prompt in the terminal and you’ll have the chance to send us a crash report. If you agree to persist your preferences, going forward all crash reports will be sent out automatically without further terminal promps. You can opt-in/out of it by setting the allowTelemetry flag in .webflowrc.js.

.webflowrc.js
1module.exports = {
2 allowTelemetry: true
3}

File extensions

By default all DevLink component files use the .js extension. If you want to use .jsx you can configure it in the .webflowrc.js file.

.webflowrc.js
1module.exports = {
2 fileExtensions: {
3 js: "jsx"
4 }
5}

Similarly, you can configure your css file extensions. By default they are .css but you could override it to .less for example.

Env variables

Some Webflow elements require an API key to function correctly:

  • Maps
  • Recaptcha

DevLink does not export your API keys set on your Webflow site, therefore you’ll need to provide them via env variables.

.env
export DEVLINK_ENV_GOOGLE_MAPS_API_KEY='<your api key>'
export DEVLINK_ENV_GOOGLE_RECAPTCHA_API_KEY='<your api key>'

If you already have those keys set up in your project and don’t want to duplicate them or rename them, you can also provide a mapping in the .webflowrc.js file.

.webflowrc.js
1module.exports = {
2 envVariables: {
3 "GOOGLE_MAPS_API_KEY": "MY_GOOGLE_MAPS_API_KEY",
4 "GOOGLE_RECAPTCHA_API_KEY": "MY_RECAPTCHA_API_KEY"
5 }
6}
next.config.js
1module.exports = {
2 images: {
3 images: {
4 domains: ["uploads-ssl.webflow.com"],
5 },
6 },
7};

Skipping global CSS rules

By default DevLink exports a global.css stylesheet containing opinionated rules and selectors (such as html and h1). If you need to disable this behavior and only include the bare minimum in order for DevLink components to work, you should activate the skipTagSelectors in you .webflowrc configuration file.

.webflowrc.js
1module.exports = {
2 skipTagSelectors: true,
3}

Caveats

  • <Navbar/> and <Dropdown/> rely on the viewport having sufficient height or width to function properly. When disabling global tags, make sure that the parent elements of your DevLink components have a defined height/width OR your <html/> and <body/> have height: 100%;.

Best practices and examples

There are different ways of using DevLink. Many will try to convert an existing full page application into the requisite components to be exported via DevLink. Alternatively many could take a more “design systems” approach, of building components in isolation, creating a library of composable components.

We do not have a specific pedagogical approach to how users use DevLink. We hope to iterate quickly based on user feedback and build functionality that can be most applicable to as many users and workflows as possible.

Component size & structure

A good approach to begin with is to define what type of components you want to build depending on your use case:

  • Large “complex” components (like whole sections, navbars, pricing tables, etc) are good when you want to reuse components between your marketing site (made in Webflow) and a web app (coded).
  • Small “atomic” components (like buttons, headings, etc.) are best when you want to build a design system in Webflow to be used in a web app.

Remember that DevLink components cannot be “enhanced” with custom code on the dev side (or they can, but when syncing again it’ll override any changes) so we recommend structuring your components so that they can be used only as UI “presentation” components. However, you can pass it your own custom data with component properties (aka. props).

Component properties

Basic properties

Define what parts of your components should always be the same and what can be overridden in each instance of the component being used. For example, a heading or button text might need to be dynamic and be overridden in each instance of the component, so you should bind them to a component property.

Examples of basic component properties are: any text within your component, images, links, videos.

Visibility properties

Use visibility properties if you want to show/hide certain parts of your component dynamically, for example a button component with or without icon.

Runtime Props

Use runtime props on all elements that might require a developer to hook in an event handler like onClick. For example, it’s best for all buttons to have runtime props.

You can also use runtime props to override specific styles. For example if I add a wrapperProps property to my Hero component and bind it to the div container that wraps the content inside the component, then I can pass a specific backgroundColor to override the existing style.

page.tsx
1<Hero wrapperProps={{ style: { backgroundColor: "#e7ffe6" } }} />;

Or alternatively I could pass it a className (note: this will make it lose any pre-existing classes defined in Webflow so it’s not recommended unless you know what you’re doing)

page.tsx
1<Hero wrapperProps={{ className: "my-global-class" }} />;

Using runtime props is essentially an escape hatch that lets you pass any custom property to the HTML element downstream. You can pass it:

  • a style object to set specific inline styles
  • a className to override the component class
  • event handlers like onClick to handle custom logic

Refs

Additionally, runtime props can be used to employ components refs, allowing direct access and manipulation of the underlying DOM elements for more advanced customization.

page.tsx
1<Input inputProps={{ ref: inputRef }} />;

Slots

Use slots when you want to leave a placeholder for a developer to include a custom React component within your Webflow component. For example you can build a PageLayout component with a top navbar and a left sidebar built in Webflow, but you want to leave the content of the page to be added by the developer with a custom React component. Then you can create a wrapper Div and bind it to a Slot property. Whatever the developer decides to include in the Slot will be rendered inside the wrapper Div.

Component variants

Having multiple style variants of the same component (e.g. a button component with primary and secondary styles) is not currently possible in Webflow. A workaround is to create a different component for each variant and then in React write your own wrapper component that returns the correct variant based on a prop

Button.tsx
1import { ButtonPrimary, ButtonSecondary } from "@/devlink";
2import React from "react";
3
4type Variant = "primary" | "secondary";
5
6const Button = ({ variant, ...props }: { variant: Variant }) => {
7 const BUTTON_VARIANTS: Record<Variant, React.ReactNode> = {
8 primary: <ButtonPrimary {...props} />,
9 secondary: <ButtonSecondary {...props} />,
10 };
11
12 return BUTTON_VARIANTS[variant];
13};
14
15export default Button;

Accessibility

DevLink components are as accessible as the underlying Webflow elements. To make all elements fully keyboard accessible we recommend following this guide.

Repeatable content / lists / tables

If you need to render a repeatable lists of data (e.g. a table or list) we recommend dividing your component into two separate components:

  • A wrapper “list” component containing a Slot where you want to render your list of data
  • An “item” component to render each individual item in the list

For example here we want to use our a UserTable component to render a list of users. We wrapped the list with a User Table Content div that is connected to a Table Content Slot property. Then in the Designer we showcase a list of fake users with the UserTableRow component.

Using slots for repeatable lists

In code we can use the UserTable component to render the outer table and then pass it a list of UserTableRow components (hydrated with our own data) with the tableContent prop.

Users.tsx
1import { UserTable, UserTableRow } from "@/devlink";
2import { useQuery } from "react-query";
3
4// Replace this with your own data
5const fetchUsers = () =>
6 Promise.resolve([
7 {
8 id: 1,
9 image: "https://d1otoma47x30pg.cloudfront.net/64669c4daefff35dc8536828/64669c4daefff35dc85368b4_sophie-moore-avatar-dashflow-webflow-template-p-130x130q80.jpg",
10 name: "Sophie Moore",
11 email: "sophie@dashflow.com",
12 jobTitle: "CTO & Co-Founder",
13 active: true,
14 company: "BRIX Templates",
15 role: "Member",
16 },
17 {
18 id: 2,
19 image: "https://d1otoma47x30pg.cloudfront.net/64669c4daefff35dc8536828/64669c4daefff35dc85368b6_andy-smith-avatar-dashflow-webflow-template-p-130x130q80.jpg",
20 name: "Andy Smith",
21 email: "andy@dashflow.com",
22 jobTitle: "VP of Marketing",
23 active: false,
24 company: "BRIX Templates",
25 role: "Member",
26 },
27 ]);
28
29const Users = () => {
30 const { data: users } = useQuery({
31 queryKey: ["users"],
32 queryFn: fetchUsers,
33 });
34
35 return (
36 <UserTable
37 tableContent={
38 <>
39 {(users ?? []).map(({ active, id, ...user }) => (
40 <UserTableRow
41 {...user}
42 key={id}
43 greenBadgeVisibility={active}
44 redBadgeVisibility={!active}
45 greenBadgeText="ACTIVE"
46 redBadgeText="INACTIVE"
47 />
48 ))}
49 </>
50 }
51 />
52 );
53};
54
55export default Users;

Reusing Webflow classes in custom components

There’s different ways to reuse some of the Webflow component classes in any custom element outside of the components, depending on your setup. For example let’s say I want to use my primary and secondary Webflow button classes in any other non-Webflow button.

First I’ll create a Button component that contains all of my button variants within a wrapper Div.

Button component

Then in my custom buttons I can use the primary and secondary classes.

If using css modules (enabled by default in DevLink) then I can import the button css module and use it to access the classes.

Buttons.tsx
1import buttonModule from "@/devlink/Button.module.css";
2
3const MyPrimaryButton = () =>
4 <a className={"w-button " + buttonModule["button-primary"]}>Primary button</a>;
5
6const MySecondaryButton = () =>
7 <a className={"w-button " + buttonModule["button-secondary"]}>Secondary button</a>;

Note: besides the component class I’m also adding the w-button class which contains Webflow’s default button styles.

Without css modules

If not using css modules then all the component classes will be defined globally in the global.css stylesheet so I can simply import that stylesheet and use Webflow classes directly in my buttons.

Buttons.tsx
1import "@/devlink/global.css";
2
3const MyPrimaryButton = () =>
4 <a className={"w-button button-primary"]}>Primary button</a>;
5
6const MySecondaryButton = () =>
7 <a className={"w-button button-secondary"]}>Secondary button</a>;

Note: this is not recommended because component styles can easily bleed into the rest of your application because they’re not namespaced.

If possible, the best option would be to use DevLink Builtin elements directly so that you don’t need to worry about setting Webflow default classes and you’ll get all of Webflow’s logic out-of-the-box (e.g. preload functionality).

Buttons.tsx
1import { _Builtin } from "@/devlink";
2import buttonModule from "@/devlink/Button.module.css";
3
4const MyPrimaryButton = () => (
5 <_Builtin.Link
6 className={buttonModule["button-primary"]}
7 button={true}
8 options={{
9 href: "https://webflow.com",
10 target: "_blank"
11 }}
12 >
13 Primary Button
14 </_Builtin.Link>
15);
16
17const MySecondaryButton = () => (
18 <_Builtin.Link
19 className={buttonModule["button-secondary"]}
20 button={true}
21 options={{
22 href: "https://webflow.com",
23 preload: "prefetch"
24 }}
25 >
26 Secondary Button
27 </_Builtin.Link>
28);

CSS tweaks with custom code

A common pattern for many Webflow users is to incorporate custom CSS tweaks with custom code and using CSS selectors.

Page custom code
1<style>
2 [class="my-class"] {
3 // css styles
4 }
5</style>

This works within Webflow but can easily break when exporting components with DevLink for two reasons:

  1. Page custom code is not exported with components
  2. When using CSS modules (enabled by default with DevLink and recommended) your class selectors will change because they’re namespaced with the component name

So in order to make this working with DevLink components you’ll need to:

  1. Use an Embed element inside the component itself (this will be exported)
  2. Update your CSS selectors to use the pattern [class*="<ComponentName>_<class-name>__"] { ... }

So for example here I have a component called Hero Section and in it an element with a class** Hero Heading Center**:

Hero Section component

So I can use the selector [class*="HeroSection_hero-heading-center__"] to target the namespaced class, which will stay consistent as long as the component name or class name don’t change.

Dark mode with color swatches and css overrides

Webflow doesn’t currently support style variants on components so it’s not possible to build different color themes of the same component natively in Webflow (instead you’d need to build separate components, one for each color theme e.g. ComponentLight and ComponentDark).

However, it’s rather simple to setup DevLink components with different color themes by using swatches and css overrides. Here’s how:

  1. Use color swatches. For all the colors that need to be overridden in your component when switching themes (e.g. dark mode) create a swatch in Webflow. Once exported with DevLink, all color swatches are defined as css variables in the global.css stylesheet.
Using a swatch in Webflow
Using a swatch in Webflow
Color swatches in global.css
Color swatches in global.css
  1. Setup theme overrides. In a separate stylesheet (e.g. /styles/globals.css) create a theme selector (e.g. dark theme) to override the color swatches values. Make sure to import that stylesheet in your page (e.g. in layout.tsx or _app.tsx if you’re using Next.js). You can have as many themes as you wish.

    Note: theme color overrides are defined in your own custom CSS and cannot be defined in Webflow.

Dark theme color overrides
Dark theme color overrides
  1. Use a custom attribute bound to a component prop. In the root element wrapper of your component add a custom attribute data-theme and bind its value to a component prop (e.g. theme).
Using data-theme custom attribute
Using data-theme custom attribute
  1. Toggle different themes. When using your component you can now pass it a theme property to define what theme to use. You can make that dynamic by using a React state variable.
Using a 'tag' custom attribute
Using a 'tag' custom attribute
page.tsx
1import React from "react";
2import {MyComponent} from "@/devlink";
3
4export default function Home() {
5 const [theme, setTheme] = React.useState("light");
6 const toggleTheme = () => {
7 setTheme(theme === "light" ? "dark" : "light");
8 };
9 return (
10 <>
11 <MyComponent theme={theme} />
12 <ThemeToggle onClick={toggleTheme} />
13 </>
14 );
15}

Following these simple steps you should be able to implement a dark mode toggle quite easily on your components.

In Next.js

If you’re using Next.js there are open source libraries that can help you set this up in an even simpler way. For example by using next-themes you can skip steps 3 and 4 and use the ThemeProvider and useTheme hook to enable dark mode on all your pages without having to manually set a custom attribute on all your components. We recommend reading their documentation for more details.

Override element tag with custom attributes

In Webflow it’s not possible to bind an element tag (e.g. <h1>, <h2>, etc. for Heading or <div>, <section>, etc. for Div Block) to a component property. However, sometimes you might want to build reusable and versatile components that can be used in different scenarios with different tags (e.g. a section that can function as a hero or middle-page segment, requiring distinct heading levels).

With DevLink you can do that by using the tag custom attribute and bind it to a component property.

Using a 'tag' custom attribute
page.tsx
1<MyComponent divTag="section"> // this will render a <section> instead of a <div>

Avoid importing from index.js

Importing from @/devlink may seem convenient, but it can lead to bloated bundles and performance issues. Bundlers may include all components from the directory, even unused ones.

It’s better to import specific components directly to avoid this. For example, use import { DevLinkProvider } from "@/devlink/DevLinkProvider" instead of import { DevLinkProvider } from "@/devlink".

Additionally, note that importing from index.js might be deprecated in the future.

Supported frameworks

React

Currently DevLink only supports React and the minimum version required is v16.8.0 (although we recommend using v18 for better performance).

Next.js

DevLink components work out-of-the-box on Next.js. On v13 Next.js introduced the usage of server components, however all DevLink components are client components and use the “use client” directive.

Vite

Vite does not allow usage of JSX syntax within .js files by default so we recommend configuring the jsx file extension in .webflowrc. Additionally, if you’re using create-vite by default it configures your project to use ES modules, so you should change your .webflowrc file to use export default { ... } syntax or to use .cjs extension.

1export default {
2 fileExtensions: {
3 js: "jsx"
4 }
5}

Create React App

Create React App by default doesn’t support relative imports outside of the /src folder so we recommend moving your DevLink components inside /src.

.webflowrc.js
1module.exports = {
2 rootDir: "./src/devlink",
3}

Additionally, the default ESLint rules won’t play well with DevLink components so you might want to disable ESLint for the whole /devlink folder by adding ./src/devlink/** to .eslintignore and adding EXTEND_ESLINT=true to your .env file.

Gatsby

If you wish to use DevLink components with CSS modules in your Gatsby application, follow these steps:

  1. Make sure the the ./devlink folder is ignored by ESLint by adding ./devlink/** to .eslintignore.
  2. Create a ./gatsby-node.js file at the root of your project and add the following code:
1const path = require("path");
2
3exports.onCreateWebpackConfig = ({ actions, rules, getConfig, stage }) => {
4 const config = getConfig();
5
6 const cssRulesPredicate = (rule) =>
7 !rule.oneOf?.findIndex(
8 ({ test }) => String(test) !== String(/\.module\.css$/)
9 ) > 0;
10
11 config.module.rules = [
12 ...config.module.rules.filter(cssRulesPredicate),
13 {
14 oneOf: [
15 {
16 use: [
17 "style-loader",
18 {
19 loader: "css-loader",
20 options: {
21 importLoaders: 1,
22 modules: {
23 localIdentContext: path.resolve(__dirname, "devlink"),
24 localIdentName: stage.includes("build")
25 ? "[hash:base64]"
26 : "[name]_[local]__[hash:hex:5]",
27 },
28 },
29 },
30 ],
31 test: /devlink\/.*\.module\.css$/,
32 },
33 {
34 ...rules.cssModules(),
35 exclude: (modulePath) => /devlink\/.*\.module\.css$/.test(modulePath),
36 },
37 rules.css(),
38 ],
39 },
40 ];
41
42 actions.replaceWebpackConfig(config);
43};
  1. Note: If you’re exporting your DevLink components to a different folder than ./devlink make sure to update these configurations.

Tailwind CSS

Some rules from global.css might conflict with the defaults set by Tailwind CSS. If you wish to use DevLink and Tailwind CSS on the same project we recommend enabling skipTagSelectors.

Limits

For safety reasons, DevLink syncs have the following limits per site/project, currently:

  • 80 components on free workspace plans
  • 500 components on upgraded workspace plans

If these limits are hit, the webflow-cli will display the following message: You exceeded the limit of components and some components were not exported.

Reporting issues

If you encounter any bugs or have feature requests, please let us know here.