Skip to main content

Crafting Reusable Buttons in FlyntWP: A Twig and ACF Pro Combo for Clean UI

One of the biggest frustrations I hear from developers dealing with WordPress is the bloat and performance drain introduced by many low-code page builders.

One of the biggest frustrations I hear from developers dealing with WordPress is the bloat and performance drain introduced by many low-code page builders. They promise speed, but often deliver an unholy mess of nested divs and inline styles, crippling Core Web Vitals and making custom styling a nightmare. That’s why I’m such a proponent of FlyntWP. It’s not about avoiding code; it’s about writing better code, programmatically defining your content structures and UI with a focus on speed, modularity, and maintainability. Today, I want to walk you through a foundational technique: building a truly reusable button component using Twig and ACF Pro.

The magic of FlyntWP lies in its component-driven architecture. Instead of dragging and dropping pre-built, rigid modules, you define your own “blocks” – self-contained units of UI and functionality. Let’s say we need a button that we can drop into various places: a hero section, a call-to-action block, or even a footer. We want it to be configurable with a label, a URL, and perhaps an optional target.

First, we create a new component folder for our reusable button. Following FlyntWP’s convention, this might live under Components/PartialButton. Inside this folder, our core UI will be in index.twig. This Twig file will accept data as “props,” much like a modern JavaScript component. For a button, it’s quite simple:

{% set url = data.link.url|default('#') %}
{% set label = data.link.title|default('Learn More') %}
{% set target = data.link.target|default('_self') %}
{% set classes = data.classes|default('') %}

<a href="{{ url }}" target="{{ target }}" class="button {{ classes }}">
  {{ label }}
</a>

Notice how clean that is. It expects a data object, specifically a link object (which is how ACF Pro’s Link field returns data) and optional classes. If data.link isn’t provided, it falls back to sensible defaults. This is crucial for reusability. The button class will be styled by our main theme’s CSS, bundled efficiently by Vite.

Now, how do we get this data into our Twig file from the WordPress admin? This is where ACF Pro shines with programmatic field group definitions. Inside the same Components/PartialButton folder, we’ll create a functions.php file. This file will register an ACF field group specifically for our button. While FlyntWP blocks are typically registered using acf_register_block_type, for a partial component like our button, which isn’t a standalone block but rather a piece within other blocks, we just need the field group.

<?php

namespace Flynt\Components\PartialButton;

use ACFComposer\ACFComposer;
use Flynt\FieldVariables;

function register_field_group()
{
    ACFComposer::registerFieldGroup([
        'name' => 'PartialButton',
        'title' => 'Partial: Button',
        'fields' => [
            FieldVariables::setLink('link', [
                'label' => __('Button Link', 'flynt'),
                'required' => 1,
            ]),
            FieldVariables::addText('classes', [
                'label' => __('Additional Classes', 'flynt'),
                'instructions' => __('Add custom CSS classes for styling.', 'flynt'),
                'wrapper' => [
                    'width' => '50',
                ],
            ]),
        ],
        'location' => [
            [
                [
                    'param' => 'post_type',
                    'operator' => '==',
                    'value' => 'post', // This is just a placeholder; typically used in a parent component.
                ],
            ],
        ],
        'hide_on_screen' => '',
    ]);
}

The FieldVariables::setLink() and FieldVariables::addText() are FlyntWP helpers that streamline ACF field definitions, but it’s essentially acf_add_local_field_group under the hood. The location here is a bit tricky for a partial component because it doesn’t appear directly in the editor. Instead, we use this field group definition when embedding the button into a parent block, like a BlockHero component.

For example, if we had a BlockHero component, its functions.php would include our PartialButton‘s fields like this:

// ... in BlockHero/functions.php
use Flynt\Components\PartialButton\register_field_group as PartialButton; // Alias it
// ...
ACFComposer::registerFieldGroup([
    'name' => 'BlockHeroFields',
    // ... other fields
    'fields' => [
        // ... hero specific fields
        PartialButton::getGroup(['name' => 'buttonLink']), // Embed the button fields
    ],
    // ...
]);

Then, in BlockHero/index.twig, we’d simply include our button:

{% include 'Components/PartialButton/index.twig' with { data: fields.buttonLink } %}

This pattern is incredibly powerful. We’ve defined our button’s UI and its ACF fields once. We can then reuse it across our entire site, confident that its markup is consistent, semantic, and free of extraneous divs. When the client updates the button’s content in the admin, it’s reflected everywhere it’s used. This programmatic approach ensures a lean DOM, lightning-fast build speeds with Vite, and ultimately, flawless Core Web Vitals, all while giving content editors the control they need, but without the page builder bloat. It’s the best of both worlds, truly.